fix: launch Pi CLI from Windows shims

This commit is contained in:
lucaronin
2026-06-07 12:04:55 +02:00
parent 8e3f8fbb77
commit 2556907f36
2 changed files with 105 additions and 13 deletions

View File

@@ -18,8 +18,12 @@ pub(crate) fn build_command(
request.permission_mode, request.permission_mode,
)?; )?;
let mut command = crate::hidden_command(binary); let target = crate::cli_agent_runtime::command_target_avoiding_windows_cmd_shim(binary)?;
let mut command = crate::hidden_command(&target.program);
crate::cli_agent_runtime::configure_agent_command_environment(&mut command, binary); crate::cli_agent_runtime::configure_agent_command_environment(&mut command, binary);
if let Some(first_arg) = target.first_arg {
command.arg(first_arg);
}
command command
.args(build_args()) .args(build_args())
.arg(build_prompt(request)) .arg(build_prompt(request))
@@ -300,10 +304,15 @@ mod tests {
} }
fn assert_pi_json_mode_args(args: &[String]) { fn assert_pi_json_mode_args(args: &[String]) {
assert_eq!(args[0], "--mode"); assert_eq!(
assert_eq!(args[1], "json"); (
assert!(args.contains(&"--no-session".to_string())); args.first().map(String::as_str),
assert!(args.contains(&"--extension".to_string())); args.get(1).map(String::as_str),
args.iter().any(|arg| arg == "--no-session"),
args.iter().any(|arg| arg == "--extension"),
),
(Some("--mode"), Some("json"), true, true)
);
} }
#[test] #[test]
@@ -324,10 +333,20 @@ mod tests {
} }
fn assert_command_identity(command: &std::process::Command, actual_args: &[&OsStr]) { fn assert_command_identity(command: &std::process::Command, actual_args: &[&OsStr]) {
assert_eq!(command.get_program(), OsStr::new("pi")); assert_eq!(
assert_eq!(actual_args[0], OsStr::new("--mode")); (
assert_eq!(actual_args[1], OsStr::new("json")); command.get_program(),
assert_eq!(actual_args.last(), Some(&OsStr::new("Rename the note"))); actual_args.first().copied(),
actual_args.get(1).copied(),
actual_args.last().copied(),
),
(
OsStr::new("pi"),
Some(OsStr::new("--mode")),
Some(OsStr::new("json")),
Some(OsStr::new("Rename the note")),
)
);
} }
fn assert_command_vault_scope( fn assert_command_vault_scope(
@@ -340,6 +359,50 @@ mod tests {
assert!(agent_dir.join("mcp.json").exists()); assert!(agent_dir.join("mcp.json").exists());
} }
#[test]
fn command_avoids_windows_cmd_shim_for_prompt_args() {
let _env_lock = PI_AGENT_ENV_LOCK.lock().unwrap();
let source_agent_dir = tempfile::tempdir().unwrap();
let agent_dir = tempfile::tempdir().unwrap();
let _guard = EnvGuard::set("PI_CODING_AGENT_DIR", source_agent_dir.path());
let shim = agent_dir.path().join("pi.cmd");
let launcher = agent_dir
.path()
.join("node_modules")
.join("@withpi")
.join("pi")
.join("bin")
.join("pi.exe");
std::fs::create_dir_all(launcher.parent().unwrap()).unwrap();
std::fs::write(&launcher, "native pi launcher").unwrap();
std::fs::write(
&shim,
r#"@ECHO off
"%~dp0\node_modules\@withpi\pi\bin\pi.exe" %*
"#,
)
.unwrap();
let command = build_command(&shim, &request(), agent_dir.path()).unwrap();
let actual_args = command.get_args().collect::<Vec<_>>();
assert_eq!(
(
command.get_program() != shim.as_os_str(),
command.get_program(),
actual_args.first().copied(),
actual_args.last().copied(),
),
(
true,
launcher.as_os_str(),
Some(OsStr::new("--mode")),
Some(OsStr::new("Rename the note")),
),
"Pi npm .cmd shims cannot be spawned directly on Windows"
);
}
#[test] #[test]
fn command_seeds_temp_agent_dir_from_existing_pi_config() { fn command_seeds_temp_agent_dir_from_existing_pi_config() {
let _env_lock = PI_AGENT_ENV_LOCK.lock().unwrap(); let _env_lock = PI_AGENT_ENV_LOCK.lock().unwrap();
@@ -429,10 +492,20 @@ mod tests {
} }
fn assert_base_mcp_config(json: &serde_json::Value) { fn assert_base_mcp_config(json: &serde_json::Value) {
assert_eq!(json["settings"]["toolPrefix"], "none"); assert_eq!(
assert_eq!(json["mcpServers"]["tolaria"]["command"], "node"); (
assert_eq!(json["mcpServers"]["tolaria"]["lifecycle"], "lazy"); &json["settings"]["toolPrefix"],
assert_eq!(json["mcpServers"]["tolaria"]["directTools"], true); &json["mcpServers"]["tolaria"]["command"],
&json["mcpServers"]["tolaria"]["lifecycle"],
&json["mcpServers"]["tolaria"]["directTools"],
),
(
&serde_json::json!("none"),
&serde_json::json!("node"),
&serde_json::json!("lazy"),
&serde_json::json!(true),
)
);
} }
fn assert_tolaria_mcp_env(json: &serde_json::Value) { fn assert_tolaria_mcp_env(json: &serde_json::Value) {

View File

@@ -88,12 +88,19 @@ fn path_from_successful_output(output: &std::process::Output) -> Option<PathBuf>
} }
fn first_existing_path(stdout: &str) -> Option<PathBuf> { fn first_existing_path(stdout: &str) -> Option<PathBuf> {
first_existing_path_for_platform(stdout, cfg!(windows))
}
fn first_existing_path_for_platform(stdout: &str, windows: bool) -> Option<PathBuf> {
stdout.lines().find_map(|line| { stdout.lines().find_map(|line| {
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
return None; return None;
} }
let candidate = PathBuf::from(trimmed); let candidate = PathBuf::from(trimmed);
if windows && !crate::cli_agent_runtime::has_windows_cli_extension(&candidate) {
return None;
}
candidate.exists().then_some(candidate) candidate.exists().then_some(candidate)
}) })
} }
@@ -358,6 +365,18 @@ mod tests {
assert_eq!(first_existing_path(&stdout), Some(pi)); assert_eq!(first_existing_path(&stdout), Some(pi));
} }
#[test]
fn first_existing_windows_path_skips_extensionless_npm_wrapper() {
let dir = tempfile::tempdir().unwrap();
let wrapper = dir.path().join("pi");
let shim = dir.path().join("pi.cmd");
std::fs::write(&wrapper, "#!/bin/sh\n").unwrap();
std::fs::write(&shim, "@ECHO off\n").unwrap();
let stdout = format!("{}\n{}\n", wrapper.display(), shim.display());
assert_eq!(first_existing_path_for_platform(&stdout, true), Some(shim));
}
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn command_path_from_shell_finds_pi_from_interactive_login_shell() { fn command_path_from_shell_finds_pi_from_interactive_login_shell() {