From cf18a0d9daf1b8ceb49967d90e309796c7d05390 Mon Sep 17 00:00:00 2001 From: Dani Lucewicz Date: Thu, 23 Jul 2026 20:38:26 +0300 Subject: [PATCH] Fix Windows ACP install: spawn powershell.exe directly, not via bash On Windows, CLI install commands for Claude and Codex are PowerShell one-liners. Wrapping them in `bash -l -c "..."` breaks nested quoting in the -Command argument and requires Git Bash to be present before the CLI itself is installed. Add `build_install_command` which detects `powershell.exe ...` commands and spawns them directly via `powershell.exe`, bypassing Git Bash for that step. npm adapter commands continue through the existing bash path. This also eliminates the `bash -l` login-shell startup overhead that caused installs to hang for 45+ minutes on some Windows machines. Fixes #2401, fixes #2407 --- .../src-tauri/src/commands/agent_discovery.rs | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index acc8f30ad3..2ec441c097 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -705,8 +705,73 @@ fn annotate_retry_attempts(mut result: InstallStepResult, attempts: u32) -> Inst result } +/// На Windows PowerShell-команды запускаем напрямую, не через bash -l -c. +/// Исправляет #2401 и #2407: bash ломал вложенные кавычки в -Command, +/// а флаг -l делал установку зависающей на 45+ минут. +fn build_install_command(command: &str) -> Result { + #[cfg(windows)] + if let Some(cmd) = powershell_command(command) { + return Ok(cmd); + } + install_shell_command(command) +} + +#[cfg(windows)] +fn powershell_command(command: &str) -> Option { + let trimmed = command.trim(); + if !trimmed.to_ascii_lowercase().starts_with("powershell.exe") { + return None; + } + let mut parts = trimmed.splitn(2, "-Command"); + let flags_part = parts.next().unwrap_or("").trim(); + let body = parts.next().map(|s| s.trim().trim_matches('"')).unwrap_or(""); + let mut cmd = std::process::Command::new("powershell.exe"); + for flag in flags_part.split_whitespace().skip(1) { + cmd.arg(flag); + } + if !body.is_empty() { + cmd.arg("-Command"); + cmd.arg(body); + } + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + Some(cmd) +} + +#[cfg(all(test, windows))] +mod powershell_tests { + use super::powershell_command; + + #[test] + fn detects_powershell_cli_install() { + let cmd = powershell_command( + r#"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex""#, + ); + assert!(cmd.is_some()); + } + + #[test] + fn ignores_npm_adapter_install() { + let cmd = powershell_command("npm install -g @agentclientprotocol/claude-agent-acp"); + assert!(cmd.is_none()); + } + + #[test] + fn ignores_curl_pipe_bash() { + let cmd = powershell_command("curl -fsSL https://example.com/install.sh | bash"); + assert!(cmd.is_none()); + } + + #[test] + fn matches_powershell_case_insensitively() { + let cmd = powershell_command(r#"PowerShell.exe -NoProfile -Command "echo test""#); + assert!(cmd.is_some()); + } +} + fn run_install_command(step: &str, command: &str) -> InstallStepResult { - let mut cmd = match install_shell_command(command) { + let mut cmd = match build_install_command(command) { Ok(cmd) => cmd, Err(hint) => { return InstallStepResult {