fix(onboarding): Windows PATH 兜底补 lefthook.cmd shim 解析
All checks were successful
Push & PR Check / check (push) Successful in 48s

pnpm add -g lefthook 安装的是 .cmd shim,CreateProcess 对裸名
"lefthook" 只解析 .exe,导致 PATH 兜底永远 missing_binary
(批量接入 6 项目实测踩坑)。兜底顺序改为逐候选尝试:
本地 .bin → 全局 lefthook → 全局 lefthook.cmd(仅 Windows)。

Rules-Applied: R10
This commit is contained in:
lanrtop 2026-07-04 11:33:11 +09:00
parent 85c79631b9
commit f68d172687
2 changed files with 45 additions and 29 deletions

View File

@ -116,9 +116,9 @@
- acceptance: [Tauri command] apply_onboarding_pack(project_id) 分发 lefthook.yml 后检测并激活 hook——找到 lefthook 可执行node_modules/.bin/ 或 PATH→ 执行 lefthook installOnboardingReport 新增 hook_activated 三态activated / missing_binary / failedmissing_binary 时附安装提示;[MCP tool] verify_onboarding(project_id) → 返回检查链 JSON模板管理块版本到位 / .git/hooks/pre-commit 含 lefthook / origin 指向 Gitea / CI workflow 文件存在 / commit_metrics 近 30 天有该项目数据),每项 pass|fail + detail无 error 字段;[cargo test] 检查链判定逻辑纯函数化,覆盖 hook 已挂/未挂、origin 匹配/不匹配R06/R07 - acceptance: [Tauri command] apply_onboarding_pack(project_id) 分发 lefthook.yml 后检测并激活 hook——找到 lefthook 可执行node_modules/.bin/ 或 PATH→ 执行 lefthook installOnboardingReport 新增 hook_activated 三态activated / missing_binary / failedmissing_binary 时附安装提示;[MCP tool] verify_onboarding(project_id) → 返回检查链 JSON模板管理块版本到位 / .git/hooks/pre-commit 含 lefthook / origin 指向 Gitea / CI workflow 文件存在 / commit_metrics 近 30 天有该项目数据),每项 pass|fail + detail无 error 字段;[cargo test] 检查链判定逻辑纯函数化,覆盖 hook 已挂/未挂、origin 匹配/不匹配R06/R07
- notes: 背景2026-07-04 侦察确认)——接入包写 lefthook.yml 后从不激活,文件在 hook 死 → conventional 不强制 → 飞轮数据脏;炼境自身未踩纯因手动装过。本卡补两件事:① 分发即激活;② "消费者角色"机器化验证(分发成功 ≠ 功能生效,对应 agent-button-system.md 消费者驱动验证)。产出直接供 B4 健康诊断遵守度消费。同日发现的姊妹雷cicd.rs PR CI 模板 pnpm lint --if-present 必挂语法已修dc501f7回归断言驻军 - notes: 背景2026-07-04 侦察确认)——接入包写 lefthook.yml 后从不激活,文件在 hook 死 → conventional 不强制 → 飞轮数据脏;炼境自身未踩纯因手动装过。本卡补两件事:① 分发即激活;② "消费者角色"机器化验证(分发成功 ≠ 功能生效,对应 agent-button-system.md 消费者驱动验证)。产出直接供 B4 健康诊断遵守度消费。同日发现的姊妹雷cicd.rs PR CI 模板 pnpm lint --if-present 必挂语法已修dc501f7回归断言驻军
### 📋 T2. git-workflow 模板:分支策略升级为复杂度感知 ### 🔵 T2. git-workflow 模板:分支策略升级为复杂度感知
- complexity: M - complexity: M
- status: todo - status: in_progress
- files: src-tauri/resources/onboarding/git-workflow.md.tmpl - files: src-tauri/resources/onboarding/git-workflow.md.tmpl
- acceptance: [Tauri command] 对已接入项目重跑接入包 → git-workflow.md ONBOARDING_MANAGED 块升至 v1.3.0,内容含「简单改动直推 main/master功能性改动开 feat/<name> 分支 + PR」策略PR 创建 MCP 示例 head 字段从 "dev" 改为 "feat/<name>"docs/templates/agents-meta-rules.md 不含分支策略无需同步 - acceptance: [Tauri command] 对已接入项目重跑接入包 → git-workflow.md ONBOARDING_MANAGED 块升至 v1.3.0,内容含「简单改动直推 main/master功能性改动开 feat/<name> 分支 + PR」策略PR 创建 MCP 示例 head 字段从 "dev" 改为 "feat/<name>"docs/templates/agents-meta-rules.md 不含分支策略无需同步
- notes: 当前模板强制 dev→main 双分支,假设了任意项目都维护常设 dev 分支,实为过度约束。复杂度感知策略(小改直推 main/master功能性改动开 feat/* + PR与炼境自身策略一致、与飞轮目标对齐PR body 是意图层数据),且对单人小项目更友好——不必维护无意义的常设 dev 分支。「简单/功能性」由 agent 根据改动文件数/性质自判,无需外部蓝图标签 - notes: 当前模板强制 dev→main 双分支,假设了任意项目都维护常设 dev 分支,实为过度约束。复杂度感知策略(小改直推 main/master功能性改动开 feat/* + PR与炼境自身策略一致、与飞轮目标对齐PR body 是意图层数据),且对单人小项目更友好——不必维护无意义的常设 dev 分支。「简单/功能性」由 agent 根据改动文件数/性质自判,无需外部蓝图标签
@ -138,7 +138,8 @@
是否返工: 否 是否返工: 否
有效策略: 立卡前先侦察实锤grep 确认 lefthook install 全仓库零调用、cicd.rs 模板同款 lint 语法雷acceptance 直接写成可执行验证路径使实现无歧义;检查链判定与 IO/DB 分离(纯函数 + conn 注入in-memory DB 全链集成测试把"空项目全 fail → 铺齐全 pass"变成回归资产模板期望版本从内嵌模板自省extract_managed_block而非硬编码模板升版验收逻辑零维护 有效策略: 立卡前先侦察实锤grep 确认 lefthook install 全仓库零调用、cicd.rs 模板同款 lint 语法雷acceptance 直接写成可执行验证路径使实现无歧义;检查链判定与 IO/DB 分离(纯函数 + conn 注入in-memory DB 全链集成测试把"空项目全 fail → 铺齐全 pass"变成回归资产模板期望版本从内嵌模板自省extract_managed_block而非硬编码模板升版验收逻辑零维护
顺手修的旧债: 前端 TemplateResult 类型缺 updated/appended 两态T1 时只修了组件层样式映射类型层漏了AGENTS.md/agent-button-system.md 写死"21 个工具"计数(防腐铁律违例,改为指向 tools_list_result 自省) 顺手修的旧债: 前端 TemplateResult 类型缺 updated/appended 两态T1 时只修了组件层样式映射类型层漏了AGENTS.md/agent-button-system.md 写死"21 个工具"计数(防腐铁律违例,改为指向 tools_list_result 自省)
遗留: MCP 工具实调需炼境升级重启后终验(运行实例是旧 buildWSL UNC 路径下 lefthook install 的 current_dir 行为未实测,失败会如实进 failed 态不会误报 遗留: WSL UNC 路径下 lefthook install 的 current_dir 行为未实测,失败会如实进 failed 态不会误报
追记2026-07-04 批量接入实战): MCP 实调已终验——8 项目跑通 apply+verifyenterprise-system/autoforphone/炼境自身 6/6 全绿。踩坑Windows PATH 兜底对 pnpm 全局 shim 失效pnpm -g 装的是 lefthook.cmdCreateProcess 裸名只解析 .exe→ 兜底追加尝试 "lefthook.cmd" 修复;本轮 6 个项目由 agent 手动 lefthook install 补齐(与按钮同路径)
auto_applied_rules: R01, R06, R07, R13 auto_applied_rules: R01, R06, R07, R13
rule_effectiveness: R01=有效commands.ts 同步时暴露类型层旧债), R06=有效conn 注入使全链集成测试可行), R07=有效6 条新测试), R13=有效(集成测试即真实走通的验收路径) rule_effectiveness: R01=有效commands.ts 同步时暴露类型层旧债), R06=有效conn 注入使全链集成测试可行), R07=有效6 条新测试), R13=有效(集成测试即真实走通的验收路径)

View File

@ -158,6 +158,8 @@ pub struct HookActivation {
} }
/// 执行 lefthook install优先项目本地 node_modules/.bin回退全局 PATH。 /// 执行 lefthook install优先项目本地 node_modules/.bin回退全局 PATH。
/// Windows 的 PATH 兜底须额外尝试 "lefthook.cmd"——pnpm 全局安装的是 .cmd shim
/// 而 CreateProcess 对裸名 "lefthook" 只解析 .exe2026-07-04 批量接入实测踩坑)。
fn run_lefthook_install(root: &Path) -> HookActivation { fn run_lefthook_install(root: &Path) -> HookActivation {
let mut candidates: Vec<std::path::PathBuf> = Vec::new(); let mut candidates: Vec<std::path::PathBuf> = Vec::new();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -169,33 +171,46 @@ fn run_lefthook_install(root: &Path) -> HookActivation {
candidates.push(root.join("node_modules/.bin/lefthook")); candidates.push(root.join("node_modules/.bin/lefthook"));
let local = candidates.into_iter().find(|p| p.exists()); let local = candidates.into_iter().find(|p| p.exists());
let program = local let mut attempts: Vec<(String, &str)> = Vec::new();
.as_ref() match &local {
.map(|p| p.to_string_lossy().to_string()) Some(p) => attempts.push((p.to_string_lossy().to_string(), "项目本地")),
.unwrap_or_else(|| "lefthook".to_string()); None => {
let source = if local.is_some() { "项目本地" } else { "全局 PATH" }; attempts.push(("lefthook".to_string(), "全局 PATH"));
#[cfg(target_os = "windows")]
attempts.push(("lefthook.cmd".to_string(), "全局 PATH(.cmd shim)"));
}
}
match crate::silent_cmd(&program).arg("install").current_dir(root).output() { for (program, source) in &attempts {
Ok(out) if out.status.success() => HookActivation { match crate::silent_cmd(program).arg("install").current_dir(root).output() {
Ok(out) if out.status.success() => {
return HookActivation {
status: "activated".into(), status: "activated".into(),
detail: format!("lefthook install 完成({source}"), detail: format!("lefthook install 完成({source}"),
}, }
Ok(out) => HookActivation { }
Ok(out) => {
return HookActivation {
status: "failed".into(), status: "failed".into(),
detail: format!( detail: format!(
"lefthook install 退出码 {:?}{}", "lefthook install 退出码 {:?}{}",
out.status.code(), out.status.code(),
String::from_utf8_lossy(&out.stderr).trim() String::from_utf8_lossy(&out.stderr).trim()
), ),
}, }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => HookActivation { }
status: "missing_binary".into(), Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
detail: "未找到 lefthooknode_modules/.bin 与 PATH 均无。安装pnpm add -D lefthook 后重跑接入包".into(), Err(e) => {
}, return HookActivation {
Err(e) => HookActivation {
status: "failed".into(), status: "failed".into(),
detail: format!("执行失败: {e}"), detail: format!("执行失败: {e}"),
}, }
}
}
}
HookActivation {
status: "missing_binary".into(),
detail: "未找到 lefthooknode_modules/.bin 与 PATH 均无。安装pnpm add -g lefthook 或项目内 pnpm add -D lefthook 后重跑接入包".into(),
} }
} }