fix(toolchain): 修复 Windows wrapper 版本探测
优先选择 .exe/.cmd/.bat 路径并直接执行版本命令,补充含空格路径测试;运行态 MCP 已确认 Enterprise 的 Node 与 pnpm 版本可正确识别。 Rules-Applied: R07,R08,R10,R13
This commit is contained in:
parent
ead35ad12a
commit
8376bcbfd2
@ -90,7 +90,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
||||
- depends: T1. Node/pnpm 工具链契约解析器
|
||||
- files: src-tauri/src/commands/toolchain.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
||||
- acceptance: "[Tauri command] get_toolchain_status(project_id) → 返回 declared/actual/executable/source/resolver/status/recommendation;项目不存在或路径越界时显式报错;实际版本匹配但无 resolver 时为 compliant_unmanaged,版本不符时为 mismatch。"
|
||||
- done_note: "2026-09-22:新增依赖注入业务函数与薄 Tauri command,只接受已登记项目的 Windows 路径,同时解析 executable path 和 `.cmd` 版本输出;WSL 按用户决策明确不支持。复盘待炼境 MCP 在本会话可用后回填。"
|
||||
- done_note: "2026-09-22:新增依赖注入业务函数与薄 Tauri command,只接受已登记项目的 Windows 路径,同时解析 executable path 和版本输出;运行态 MCP 自检发现 `where pnpm` 优先返回无扩展 shim 且 cmd 引号不稳,已改为优先 `.exe/.cmd/.bat` 并分别使用直接执行或 `cmd /D /S /C`。WSL 按用户决策明确不支持。复盘待炼境 MCP 在本会话可用后回填。"
|
||||
|
||||
### ✅ T3. MCP 只读诊断投影与契约文档
|
||||
- status: done
|
||||
@ -106,7 +106,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
||||
- depends: T2. 只读工具链状态 Tauri command
|
||||
- files: src-tauri/src/commands/toolchain.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
||||
- acceptance: "[Tauri command] prepare_toolchain(project_id, confirmed) → confirmed=false、未知项目、根路径越界或 contract=conflict 时零执行;confirmed=true 时只执行参数化 `mise install`,不运行项目 task/hook,不修改 package.json/pnpm-lock.yaml,并返回 before/after 状态与命令证据。"
|
||||
- done_note: "2026-09-22:新增 Tauri/MCP 受控 prepare_toolchain;resolver 就绪后通过 mise exec 探测项目版本,拒绝未确认、冲突、缺版本、缺 mise 和含自定义 task/hook/plugin 的配置;返回 verified 区分命令成功与二次诊断 ready_managed,13 个定向测试全绿,未实际下载安装。复盘待炼境 MCP 在本会话可用后回填。"
|
||||
- done_note: "2026-09-22:新增 Tauri/MCP 受控 prepare_toolchain;resolver 就绪后通过 mise exec 探测项目版本,拒绝未确认、冲突、缺版本、缺 mise 和含自定义 task/hook/plugin 的配置;返回 verified 区分命令成功与二次诊断 ready_managed,定向测试全绿,未实际下载安装。复盘待炼境 MCP 在本会话可用后回填。"
|
||||
|
||||
### 🔴 T5. 项目环境 UI 工具链契约卡
|
||||
- status: blocked
|
||||
@ -114,7 +114,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
||||
- depends: T2. 只读工具链状态 Tauri command, T4. 受控 mise 工具链准备能力
|
||||
- files: src/components/dashboard/ProjectEnvModal.tsx, src/lib/commands.ts
|
||||
- acceptance: "agent_verify: [pnpm typecheck] ProjectEnvModal 消费 ToolchainStatus 类型且无类型错误;[Tauri command mock/fixture] ready_managed、compliant_unmanaged、mismatch、conflict 分别渲染准确状态与可用动作,prepare 必须经过确认;human_verify: 环境工具弹窗能看懂期望/实际版本、来源路径和准备结果。"
|
||||
- blocked_reason: "代码接线与 pnpm typecheck 已通过;本项目按规则不为 React 组件临时引入测试框架,且隔离 worktree 未启动 Tauri UI,尚缺 human_verify。需合并/启动该分支后在环境工具弹窗终验四种状态与确认按钮。"
|
||||
- blocked_reason: "代码接线与 pnpm typecheck 已通过;隔离分支已启动,运行态 MCP 对 Enterprise 返回 Node 24.19.0、pnpm 9.15.0 且均为 compliant_unmanaged,并由此发现并修复 Windows `.cmd` wrapper 版本探测;当前 Computer Use 未暴露 Tauri 原生窗口,本项目也不为 React 组件临时引入测试框架,仍缺 human_verify。需在环境工具弹窗终验状态卡与确认按钮。"
|
||||
|
||||
### ✅ T6. Onboarding 与 Codex Skill 条件式工具链指引
|
||||
- status: done
|
||||
@ -130,7 +130,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
||||
- depends: T3. MCP 只读诊断投影与契约文档, T4. 受控 mise 工具链准备能力, T6. Onboarding 与 Codex Skill 条件式工具链指引
|
||||
- files: E:/code-project/enterprise-system/mise.toml, E:/code-project/enterprise-system/AGENTS.md, E:/code-project/enterprise-system/.gitea/workflows/ci.yml
|
||||
- acceptance: "agent_verify: [CLI] `mise exec -- node --version` → v24.19.0,`mise exec -- pnpm --version` → 9.15.0,`mise exec -- pnpm check:toolchain` → exit 0;[Gitea CI] 从 `.nvmrc` 与 packageManager 安装精确版本并在业务检查前通过 check:toolchain;package.json 与 pnpm-lock.yaml 无非预期改动;human_verify: 新 Codex 会话进入 Enterprise 后不再因 bundled pnpm 11 阻塞。"
|
||||
- blocked_reason: "消费者试点依赖本分支先完成审查、合并并重建炼境,使新 onboarding/MCP tool 真正可调用;Enterprise 当前另一个会话还有未提交 T3 migration/蓝图改动,现阶段不跨项目写入或混合验证。"
|
||||
- blocked_reason: "运行态只读试点已确认 Enterprise 的 Node 24.19.0、pnpm 9.15.0 声明与本机版本匹配,但 resolver=unavailable,因此仍不是跨 Agent 可复现的 ready_managed。写入试点依赖本分支先完成审查、合并并重建炼境;Enterprise 当前另一个会话还有未提交 T3 migration/蓝图改动,现阶段不跨项目写入或混合验证。"
|
||||
|
||||
## 执行顺序
|
||||
|
||||
|
||||
@ -320,6 +320,31 @@ pub fn parse_toolchain_contract(root: &Path) -> Result<ToolchainContract, String
|
||||
})
|
||||
}
|
||||
|
||||
fn select_executable_path(output: &[u8]) -> Option<String> {
|
||||
let paths = String::from_utf8_lossy(output)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(native_wrapper) = paths.iter().find(|path| {
|
||||
matches!(
|
||||
Path::new(path)
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.map(|extension| extension.to_ascii_lowercase())
|
||||
.as_deref(),
|
||||
Some("exe" | "cmd" | "bat")
|
||||
)
|
||||
}) {
|
||||
return Some(native_wrapper.clone());
|
||||
}
|
||||
|
||||
paths.into_iter().next()
|
||||
}
|
||||
|
||||
fn locate_executable(tool: &str) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = crate::silent_cmd("where.exe").arg(tool).output().ok()?;
|
||||
@ -329,25 +354,22 @@ fn locate_executable(tool: &str) -> Option<String> {
|
||||
output
|
||||
.status
|
||||
.success()
|
||||
.then(|| {
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|line| !line.is_empty())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
.filter(|path| !path.is_empty())
|
||||
.then(|| select_executable_path(&output.stdout))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn executable_version(path: &str) -> Option<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = {
|
||||
let escaped = path.replace('"', "\"\"");
|
||||
crate::silent_cmd("cmd")
|
||||
.args(["/D", "/C", &format!("\"{escaped}\" --version")])
|
||||
.output()
|
||||
.ok()?
|
||||
let extension = Path::new(path)
|
||||
.extension()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or_default();
|
||||
if extension.eq_ignore_ascii_case("exe") {
|
||||
crate::silent_cmd(path).arg("--version").output().ok()?
|
||||
} else {
|
||||
crate::silent_cmd(path).arg("--version").output().ok()?
|
||||
}
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let output = crate::silent_cmd(path).arg("--version").output().ok()?;
|
||||
@ -789,6 +811,30 @@ mod tests {
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_executable_selection_prefers_native_wrapper() {
|
||||
let output = b"C:\\nvm4w\\nodejs\\pnpm\r\nC:\\nvm4w\\nodejs\\pnpm.cmd\r\n";
|
||||
assert_eq!(
|
||||
select_executable_path(output).as_deref(),
|
||||
Some("C:\\nvm4w\\nodejs\\pnpm.cmd")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn probes_windows_cmd_wrapper_with_spaces_in_path() {
|
||||
let root = fixture().join("path with spaces");
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let wrapper = root.join("tool.cmd");
|
||||
std::fs::write(&wrapper, "@echo 9.15.0\r\n").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
executable_version(wrapper.to_str().unwrap()).as_deref(),
|
||||
Some("9.15.0")
|
||||
);
|
||||
std::fs::remove_dir_all(root.parent().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
fn probe(version: Option<&str>, executable: Option<&str>) -> RuntimeProbe {
|
||||
RuntimeProbe {
|
||||
version: version.map(str::to_string),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user