Файли
openidea/crates/ide-run/src/which.rs
T
arookieofcтаa-rookie-of-C-language 94d209c99c fix(agents): resolve agent commands through PATH x PATHEXT on Windows (#273)
On Windows std/tokio Command only tries .exe for a bare program name, so agents installed as npm shims (codex.cmd, claude.cmd) failed with "program not found". Bare names are now resolved through PATH x PATHEXT before spawning.

- ide-core::process gains find_executable / find_executable_in (moved from ide-run::which, which re-exports them).
- ide-jsonrpc Peer::spawn_framed resolves against the child's PATH (caller env, login shell, then ours): covers ACP agents and the native Codex driver.
- ide-agent newest_install builds candidates with executable_names, so the native Claude driver finds .cmd shims and multi-install selection works on Windows.
- ide-agent replay recorder resolves the same way.

Co-authored-by: a-rookie-of-C-language <[email protected]>
2026-10-29 23:00:01 +08:00

50 рядки
2.3 KiB
Rust

//! Executable lookup on a search path. On Windows a bare name resolves through
//! `PATHEXT` (`kotlinc` → `kotlinc.bat`, `npx` → `npx.cmd`), which
//! `std::process::Command` does not do (it only appends `.exe`).
pub use ide_core::process::{executable_names, find_executable, find_executable_in};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unix_uses_the_name_as_is() {
assert_eq!(executable_names("kotlinc", false, Some(".BAT")), ["kotlinc"]);
}
#[test]
fn windows_appends_pathext_extensions() {
assert_eq!(
executable_names("kotlinc", true, None),
["kotlinc.com", "kotlinc.exe", "kotlinc.bat", "kotlinc.cmd"]
);
assert_eq!(executable_names("npx", true, Some(".EXE;.CMD;;")), ["npx.exe", "npx.cmd"]);
assert_eq!(executable_names("npx", true, Some(" ")), executable_names("npx", true, None));
// Already has an executable extension (any case): only that file.
assert_eq!(executable_names("rustc.EXE", true, None), ["rustc.EXE"]);
assert_eq!(executable_names("gradlew.bat", true, None), ["gradlew.bat"]);
// A dot that is not an executable extension (`clang++`, `python3.12`).
assert_eq!(executable_names("python3.12", true, Some(".EXE")), ["python3.12.exe"]);
}
#[test]
fn finds_batch_files_and_skips_extensionless_scripts_on_windows() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
std::fs::create_dir_all(&a).unwrap();
std::fs::create_dir_all(&b).unwrap();
// `node_modules/.bin` style: a POSIX script next to its `.cmd` shim.
std::fs::write(a.join("tsx"), "#!/bin/sh\n").unwrap();
std::fs::write(b.join("tsx.cmd"), "@echo off\n").unwrap();
std::fs::write(b.join("kotlinc.bat"), "@echo off\n").unwrap();
let search = std::env::join_paths([&a, &b]).unwrap();
assert_eq!(find_executable_in("tsx", &search, true, None), Some(b.join("tsx.cmd")));
assert_eq!(find_executable_in("kotlinc", &search, true, None), Some(b.join("kotlinc.bat")));
assert_eq!(find_executable_in("tsx", &search, false, None), Some(a.join("tsx")));
assert_eq!(find_executable_in("kotlinc", &search, false, None), None);
assert_eq!(find_executable_in("nope", &search, true, None), None);
}
}