From 7bd9f6500db85a4f6b0e680c6676f2bace818c95 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 24 Apr 2026 13:55:05 +0200 Subject: [PATCH] fix(coding-agent): avoid cmd.exe for git installs on windows closes #3642 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/package-manager.ts | 23 +++++- .../coding-agent/test/package-manager.test.ts | 14 ++++ pi-test.ps1 | 70 +++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 pi-test.ps1 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7386bf49..73b9e85c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,6 +11,7 @@ - Fixed `/copy` to avoid unbounded OSC 52 writes and clipboard races that could break terminal rendering or panic the native clipboard addon ([#3639](https://github.com/badlogic/pi-mono/issues/3639)) - Fixed extension flag docs to show `pi.getFlag()` using registered flag names without the CLI `--` prefix ([#3614](https://github.com/badlogic/pi-mono/issues/3614)) - Fixed provider retry/timeout settings wiring by adding `retry.provider.{timeoutMs,maxRetries,maxRetryDelayMs}`, migrating legacy `retry.maxDelayMs`, and forwarding provider controls into `streamSimple` request options ([#3627](https://github.com/badlogic/pi-mono/issues/3627)) +- Fixed Windows git package installs to bypass `cmd.exe` for native git commands, so install paths containing spaces no longer break `pi install git:...` with `fatal: Too many arguments` ([#3642](https://github.com/badlogic/pi-mono/issues/3642)) ## [0.70.0] - 2026-04-23 diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index b386bf7b..a8e996ae 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2323,11 +2323,28 @@ export class DefaultPackageManager implements PackageManager { }; } + private shouldUseWindowsShell(command: string): boolean { + if (process.platform !== "win32") { + return false; + } + const commandName = basename(command).toLowerCase(); + return ( + commandName === "npm" || + commandName === "npx" || + commandName === "pnpm" || + commandName === "yarn" || + commandName === "yarnpkg" || + commandName === "corepack" || + commandName.endsWith(".cmd") || + commandName.endsWith(".bat") + ); + } + private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess { return spawn(command, args, { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", - shell: process.platform === "win32", + shell: this.shouldUseWindowsShell(command), }); } @@ -2339,7 +2356,7 @@ export class DefaultPackageManager implements PackageManager { return spawn(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], - shell: process.platform === "win32", + shell: this.shouldUseWindowsShell(command), env: options?.env ? { ...process.env, ...options.env } : process.env, }); } @@ -2406,7 +2423,7 @@ export class DefaultPackageManager implements PackageManager { const result = spawnSync(command, args, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", - shell: process.platform === "win32", + shell: this.shouldUseWindowsShell(command), }); if (result.status !== 0) { throw new Error(`Failed to run ${command} ${args.join(" ")}: ${result.stderr || result.stdout}`); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index f116cc74..0df310ae 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -460,6 +460,20 @@ Content`, }); }); + describe("windows command spawning", () => { + it("should avoid the shell for git so Windows paths with spaces stay single arguments", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const managerWithInternals = packageManager as unknown as { + shouldUseWindowsShell(command: string): boolean; + }; + + expect(managerWithInternals.shouldUseWindowsShell("git")).toBe(false); + expect(managerWithInternals.shouldUseWindowsShell("npm")).toBe(true); + expect(managerWithInternals.shouldUseWindowsShell("pnpm")).toBe(true); + expect(managerWithInternals.shouldUseWindowsShell("C:/Program Files/nodejs/npm.cmd")).toBe(true); + }); + }); + describe("npmCommand", () => { it("should use npmCommand argv for npm installs", async () => { settingsManager = SettingsManager.inMemory({ diff --git a/pi-test.ps1 b/pi-test.ps1 new file mode 100644 index 00000000..2bbe35bd --- /dev/null +++ b/pi-test.ps1 @@ -0,0 +1,70 @@ +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$noEnv = $false +$forwardArgs = New-Object System.Collections.Generic.List[string] + +foreach ($arg in $args) { + if ($arg -eq "--no-env") { + $noEnv = $true + } else { + $forwardArgs.Add($arg) + } +} + +if ($noEnv) { + $envVarsToUnset = @( + "ANTHROPIC_API_KEY", + "ANTHROPIC_OAUTH_TOKEN", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GROQ_API_KEY", + "CEREBRAS_API_KEY", + "XAI_API_KEY", + "OPENROUTER_API_KEY", + "ZAI_API_KEY", + "MISTRAL_API_KEY", + "MINIMAX_API_KEY", + "MINIMAX_CN_API_KEY", + "AI_GATEWAY_API_KEY", + "OPENCODE_API_KEY", + "COPILOT_GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "GOOGLE_APPLICATION_CREDENTIALS", + "GOOGLE_CLOUD_PROJECT", + "GCLOUD_PROJECT", + "GOOGLE_CLOUD_LOCATION", + "AWS_PROFILE", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_BEARER_TOKEN_BEDROCK", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AZURE_OPENAI_API_KEY", + "AZURE_OPENAI_BASE_URL", + "AZURE_OPENAI_RESOURCE_NAME" + ) + + foreach ($name in $envVarsToUnset) { + Remove-Item -Path "Env:$name" -ErrorAction SilentlyContinue + } + + Write-Host "Running without API keys..." +} + +$tsxBin = Join-Path $scriptDir "node_modules/.bin/tsx.cmd" +if (-not (Test-Path -LiteralPath $tsxBin)) { + throw "tsx not found at $tsxBin. Run npm install from the repo root first." +} + +$cliPath = Join-Path $scriptDir "packages/coding-agent/src/cli.ts" +& $tsxBin $cliPath @forwardArgs +$exitCode = $LASTEXITCODE +if ($exitCode -ne 0) { + exit $exitCode +}