diff --git a/src-tauri/src/claude_cli.rs b/src-tauri/src/claude_cli.rs index 91d6a0a5..59a72bd2 100644 --- a/src-tauri/src/claude_cli.rs +++ b/src-tauri/src/claude_cli.rs @@ -118,20 +118,33 @@ fn claude_path_from_shell(shell: &Path) -> Option { } fn path_from_successful_output(output: &std::process::Output) -> Option { - if !output.status.success() { - return None; + if output.status.success() { + first_existing_path(&String::from_utf8_lossy(&output.stdout)) + } else { + None + } +} + +fn first_existing_path(stdout: &str) -> Option { + first_existing_path_for_platform(stdout, cfg!(windows)) +} + +fn first_existing_path_for_platform(stdout: &str, windows: bool) -> Option { + let mut paths = stdout.lines().filter_map(existing_path); + if windows { + return paths.find(|path| crate::cli_agent_runtime::has_windows_cli_extension(path)); } - String::from_utf8_lossy(&output.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) - }) + paths.next() +} + +fn existing_path(line: &str) -> Option { + let trimmed = line.trim(); + if trimmed.is_empty() { + return None; + } + let candidate = PathBuf::from(trimmed); + candidate.exists().then_some(candidate) } fn claude_binary_candidates() -> Vec { @@ -710,6 +723,22 @@ mod tests { assert!(candidates.contains(&claude), "missing {}", claude.display()); } + #[test] + fn windows_path_lookup_prefers_cmd_shim_over_extensionless_npm_script() { + let dir = tempfile::tempdir().unwrap(); + let shell_script = dir.path().join("claude"); + let cmd_shim = dir.path().join("claude.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) + ); + } + #[test] fn unsupported_claude_flag_errors_are_detected() { assert!(is_unsupported_claude_flag_error(ClaudeStderr( diff --git a/src-tauri/src/codex_cli.rs b/src-tauri/src/codex_cli.rs index e558118b..2995b4b8 100644 --- a/src-tauri/src/codex_cli.rs +++ b/src-tauri/src/codex_cli.rs @@ -93,14 +93,25 @@ fn path_from_successful_output(output: &std::process::Output) -> Option } fn first_existing_path(stdout: &str) -> Option { - 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 { + 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 { + 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 { @@ -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() {