Agent chat on Windows: start npm agents through their .cmd shim

npm puts a Unix shell script with the bare name (codex-acp,
claude-code-acp, gemini) beside the .cmd shim. The lookup tried the bare
name first, so every npm-installed agent failed with "not a valid Win32
application (os error 193)". Windows now only considers .exe/.cmd/.bat,
and a command saved with the bare path runs the shim beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-15 21:49:46 +00:00
1 parent a13124acdd
commit 60ea8c69d2
2 files changed
+43 -4

No files matched your search

+39 -3
View File
@@ -184,13 +184,30 @@ pub fn search_dirs() -> Vec<PathBuf> {
dirs
}
/// The first `program` in `dirs` (on Windows also with `.exe`/`.cmd`).
/// The first `program` in `dirs` (on Windows with `.exe`/`.cmd`/`.bat`).
pub fn find_in(program: &str, dirs: &[PathBuf]) -> Option<PathBuf> {
let names: Vec<String> =
if cfg!(windows) { ["", ".exe", ".cmd", ".bat"].iter().map(|e| format!("{program}{e}")).collect() } else { vec![program.to_string()] };
let names = names_for(program, cfg!(windows));
dirs.iter().flat_map(|d| names.iter().map(move |n| d.join(n))).find(|p| p.is_file())
}
/// The file names `program` can have. On Windows only ones it can run: npm puts a Unix shell
/// script with the bare name next to the `.cmd` shim, and starting that fails with "not a valid
/// Win32 application".
fn names_for(program: &str, windows: bool) -> Vec<String> {
if windows { [".exe", ".cmd", ".bat"].iter().map(|e| format!("{program}{e}")).collect() } else { vec![program.to_string()] }
}
/// A Windows command saved without an extension (`…\npm\codex-acp`, from an older version's
/// lookup): the `.exe`/`.cmd`/`.bat` beside it, if there is one.
pub fn runnable_sibling(program: &Path) -> Option<PathBuf> {
if program.extension().is_some() {
return None;
}
let dir = program.parent()?;
let name = program.file_name()?.to_str()?;
names_for(name, true).into_iter().map(|n| dir.join(n)).find(|p| p.is_file())
}
/// `program --version`'s first line, or `None` after `limit` (the probe is then stopped).
fn version(program: &Path, limit: Duration) -> Option<String> {
let mut cmd = if cfg!(windows) && program.extension().is_some_and(|e| e.eq_ignore_ascii_case("cmd") || e.eq_ignore_ascii_case("bat")) {
@@ -279,6 +296,25 @@ mod tests {
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn on_windows_only_runnable_shims_are_candidates() {
// npm's bare `codex-acp` is a shell script; Windows can't start it (os error 193).
assert_eq!(names_for("codex-acp", true), ["codex-acp.exe", "codex-acp.cmd", "codex-acp.bat"]);
assert_eq!(names_for("codex-acp", false), ["codex-acp"]);
}
#[test]
fn a_saved_bare_npm_path_runs_its_cmd_shim() {
let root = std::env::temp_dir().join(format!("sc-agent-shim-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("codex-acp"), "#!/bin/sh").unwrap();
std::fs::write(root.join("codex-acp.cmd"), "@echo off").unwrap();
assert_eq!(runnable_sibling(&root.join("codex-acp")), Some(root.join("codex-acp.cmd")));
assert_eq!(runnable_sibling(&root.join("codex-acp.cmd")), None);
assert_eq!(runnable_sibling(&root.join("gemini")), None);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn the_version_number_is_picked_out_of_the_line() {
assert_eq!(version_of("@agentclientprotocol/codex-acp 2.2.2"), "2.2.2");
+4 -1
View File
@@ -51,8 +51,11 @@ impl ProcessHandle {
}
}
/// On Windows, npm installs `.cmd` shims, which only `cmd.exe` runs.
/// On Windows, npm installs `.cmd` shims, which only `cmd.exe` runs. A command saved as the bare
/// npm path (a shell script there) runs the shim beside it.
fn command_for(program: &str) -> Command {
let shim = if cfg!(windows) { super::discover::runnable_sibling(std::path::Path::new(program)) } else { None };
let program = shim.as_deref().and_then(|p| p.to_str()).unwrap_or(program);
let lower = program.to_ascii_lowercase();
if cfg!(windows) && (lower.ends_with(".cmd") || lower.ends_with(".bat")) {
let mut c = Command::new("cmd");