From 8f484c54cef435f535fcbe654e351661d3fed177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BF=B7=E6=B8=A1?= Date: Wed, 1 Jul 2026 11:33:40 +0800 Subject: [PATCH] fix(detect): dedupe Windows Codex npm shims (#4782) --- src-tauri/src/commands/misc.rs | 56 +++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index 90ed3b28a..77fa0c955 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -1445,11 +1445,15 @@ fn opencode_extra_search_paths( fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec { #[cfg(target_os = "windows")] { - vec![ + let extensionless = dir.join(tool); + let mut candidates = vec![ dir.join(format!("{tool}.cmd")), dir.join(format!("{tool}.exe")), - dir.join(tool), - ] + ]; + if windows_runnable_sibling_for_extensionless_tool(&extensionless).is_none() { + candidates.push(extensionless); + } + candidates } #[cfg(not(target_os = "windows"))] @@ -1619,6 +1623,18 @@ fn is_windows_command_script(path: &Path) -> bool { .unwrap_or(false) } +#[cfg(target_os = "windows")] +fn windows_runnable_sibling_for_extensionless_tool(path: &Path) -> Option { + if path.extension().is_some() { + return None; + } + + ["cmd", "exe"] + .iter() + .map(|ext| path.with_extension(ext)) + .find(|candidate| candidate.is_file()) +} + #[cfg(target_os = "windows")] fn run_windows_tool_version_command( tool_path: &Path, @@ -1821,7 +1837,10 @@ fn resolve_path_default(tool: &str) -> Option { if first.is_empty() { return None; } - std::fs::canonicalize(first).ok() + let path = Path::new(first); + let preferred = + windows_runnable_sibling_for_extensionless_tool(path).unwrap_or_else(|| path.to_path_buf()); + std::fs::canonicalize(preferred).ok() } /// 枚举工具在系统中的所有安装(不短路)。与 `scan_cli_version` 共用 @@ -5063,6 +5082,35 @@ mod tests { ); } + #[cfg(target_os = "windows")] + #[test] + fn tool_executable_candidates_windows_skips_shadowed_npm_unix_shim() { + let dir = tempfile::tempdir().expect("temp dir should be created"); + let extensionless = dir.path().join("codex"); + let cmd = dir.path().join("codex.cmd"); + std::fs::write(&extensionless, "").expect("extensionless shim should be created"); + std::fs::write(&cmd, "").expect("cmd shim should be created"); + + let candidates = tool_executable_candidates("codex", dir.path()); + + assert_eq!(candidates, vec![cmd.clone(), dir.path().join("codex.exe")]); + assert!(!candidates.contains(&extensionless)); + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_runnable_sibling_prefers_cmd_over_extensionless_tool() { + let dir = tempfile::tempdir().expect("temp dir should be created"); + let extensionless = dir.path().join("codex"); + let cmd = dir.path().join("codex.cmd"); + std::fs::write(&extensionless, "").expect("extensionless shim should be created"); + std::fs::write(&cmd, "").expect("cmd shim should be created"); + + let preferred = windows_runnable_sibling_for_extensionless_tool(&extensionless); + + assert_eq!(preferred.as_deref(), Some(cmd.as_path())); + } + #[test] fn resolve_launch_cwd_accepts_existing_directory() { let resolved =