fix: prefer .cmd shim over POSIX wrapper for claude/codex on Windows

Fixes #695

`where claude` on Windows returns both the npm-generated POSIX shell
wrapper (extensionless) and the Windows batch shim (`claude.cmd`)
side-by-side. The existing `path_from_successful_output` helpers in
`claude_cli.rs` and `codex_cli.rs` picked the first existing line,
which gave the POSIX wrapper - Windows tried to load it as a PE32,
`CreateProcessW` returned ERROR_BAD_EXE_FORMAT (193), and the AI chat
panel surfaced "%1 is not a valid Win32 application."

Mirror the fix that landed for Gemini in `gemini_discovery.rs`: when
`cfg!(windows)`, filter candidates through the existing shared utility
`cli_agent_runtime::has_windows_cli_extension` so .bat/.cmd/.com/.exe
win over extensionless POSIX scripts. Non-Windows behavior is unchanged.

Added a regression test in each file matching the
`windows_path_lookup_prefers_cmd_shim_over_extensionless_npm_script`
pattern from gemini_discovery.rs.
This commit is contained in:
Matt Van Horn
2026-05-19 23:28:16 -07:00
parent 6cf77f9b94
commit 663a7d9a3f
2 changed files with 76 additions and 20 deletions

View File

@@ -93,14 +93,25 @@ fn path_from_successful_output(output: &std::process::Output) -> Option<PathBuf>
}
fn first_existing_path(stdout: &str) -> Option<PathBuf> {
stdout.lines().find_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
let candidate = PathBuf::from(trimmed);
candidate.exists().then_some(candidate)
})
first_existing_path_for_platform(stdout, cfg!(windows))
}
fn first_existing_path_for_platform(stdout: &str, windows: bool) -> Option<PathBuf> {
let mut paths = stdout.lines().filter_map(existing_path);
if windows {
return paths.find(|path| crate::cli_agent_runtime::has_windows_cli_extension(path));
}
paths.next()
}
fn existing_path(line: &str) -> Option<PathBuf> {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
let candidate = PathBuf::from(trimmed);
candidate.exists().then_some(candidate)
}
fn codex_binary_candidates() -> Vec<PathBuf> {
@@ -1003,6 +1014,22 @@ printf '%s\n' '{"type":"item.completed","item":{"id":"msg_1","type":"agent_messa
assert_eq!(first_existing_path(&stdout), Some(codex));
}
#[test]
fn windows_path_lookup_prefers_cmd_shim_over_extensionless_npm_script() {
let dir = tempfile::tempdir().unwrap();
let shell_script = dir.path().join("codex");
let cmd_shim = dir.path().join("codex.cmd");
std::fs::write(&shell_script, "#!/bin/sh\n").unwrap();
std::fs::write(&cmd_shim, "@ECHO off\n").unwrap();
let stdout = format!("{}\n{}\n", shell_script.display(), cmd_shim.display());
assert_eq!(
first_existing_path_for_platform(&stdout, true),
Some(cmd_shim)
);
}
#[cfg(unix)]
#[test]
fn command_path_from_shell_finds_codex_from_login_shell() {