fix(coding-agent): avoid cmd.exe for git installs on windows closes #3642

This commit is contained in:
Mario Zechner
2026-04-24 13:55:05 +02:00
parent c06750410a
commit 7bd9f6500d
4 changed files with 105 additions and 3 deletions

View File

@@ -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

View File

@@ -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}`);

View File

@@ -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({

70
pi-test.ps1 Normal file
View File

@@ -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
}