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 工具链契约解析器
|
- depends: T1. Node/pnpm 工具链契约解析器
|
||||||
- files: src-tauri/src/commands/toolchain.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
- 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。"
|
- 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 只读诊断投影与契约文档
|
### ✅ T3. MCP 只读诊断投影与契约文档
|
||||||
- status: done
|
- status: done
|
||||||
@ -106,7 +106,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
|||||||
- depends: T2. 只读工具链状态 Tauri command
|
- 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
|
- 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 状态与命令证据。"
|
- 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 工具链契约卡
|
### 🔴 T5. 项目环境 UI 工具链契约卡
|
||||||
- status: blocked
|
- status: blocked
|
||||||
@ -114,7 +114,7 @@ Enterprise System 已精确声明 Node `24.19.0` 与 pnpm `9.15.0`,而 Codex b
|
|||||||
- depends: T2. 只读工具链状态 Tauri command, T4. 受控 mise 工具链准备能力
|
- depends: T2. 只读工具链状态 Tauri command, T4. 受控 mise 工具链准备能力
|
||||||
- files: src/components/dashboard/ProjectEnvModal.tsx, src/lib/commands.ts
|
- 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: 环境工具弹窗能看懂期望/实际版本、来源路径和准备结果。"
|
- 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 条件式工具链指引
|
### ✅ T6. Onboarding 与 Codex Skill 条件式工具链指引
|
||||||
- status: done
|
- 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 条件式工具链指引
|
- 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
|
- 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 阻塞。"
|
- 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> {
|
fn locate_executable(tool: &str) -> Option<String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let output = crate::silent_cmd("where.exe").arg(tool).output().ok()?;
|
let output = crate::silent_cmd("where.exe").arg(tool).output().ok()?;
|
||||||
@ -329,25 +354,22 @@ fn locate_executable(tool: &str) -> Option<String> {
|
|||||||
output
|
output
|
||||||
.status
|
.status
|
||||||
.success()
|
.success()
|
||||||
.then(|| {
|
.then(|| select_executable_path(&output.stdout))
|
||||||
String::from_utf8_lossy(&output.stdout)
|
.flatten()
|
||||||
.lines()
|
|
||||||
.map(str::trim)
|
|
||||||
.find(|line| !line.is_empty())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
.filter(|path| !path.is_empty())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executable_version(path: &str) -> Option<String> {
|
fn executable_version(path: &str) -> Option<String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let output = {
|
let output = {
|
||||||
let escaped = path.replace('"', "\"\"");
|
let extension = Path::new(path)
|
||||||
crate::silent_cmd("cmd")
|
.extension()
|
||||||
.args(["/D", "/C", &format!("\"{escaped}\" --version")])
|
.and_then(|value| value.to_str())
|
||||||
.output()
|
.unwrap_or_default();
|
||||||
.ok()?
|
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"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let output = crate::silent_cmd(path).arg("--version").output().ok()?;
|
let output = crate::silent_cmd(path).arg("--version").output().ok()?;
|
||||||
@ -789,6 +811,30 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(root).unwrap();
|
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 {
|
fn probe(version: Option<&str>, executable: Option<&str>) -> RuntimeProbe {
|
||||||
RuntimeProbe {
|
RuntimeProbe {
|
||||||
version: version.map(str::to_string),
|
version: version.map(str::to_string),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user