From b86f788ae77e5e7174d11c4a69ced0744d8fe087 Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:33:04 +0000 Subject: [PATCH] fix(utils): support PowerShell as custom shellPath --- packages/utils/CHANGELOG.md | 4 ++++ packages/utils/src/procmgr.ts | 25 +++++++++++++++++++++---- packages/utils/test/procmgr.test.ts | 26 +++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index a05d90562..0c627bd16 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Support PowerShell (`powershell.exe` / `pwsh`) as a custom `shellPath`: spawn paths now pass `-NoLogo -Command` (plus `-NoProfile` under `PI_BASH_NO_LOGIN`) instead of the POSIX `-l -c` pair, which PowerShell rejected with `The term '-l' is not recognized`. + ## [17.2.9] - 2026-08-05 ### Added diff --git a/packages/utils/src/procmgr.ts b/packages/utils/src/procmgr.ts index 6d2a615d2..3df336760 100644 --- a/packages/utils/src/procmgr.ts +++ b/packages/utils/src/procmgr.ts @@ -50,12 +50,19 @@ function buildSpawnEnv(shell: string): Record { /** * Get shell args for the resolved shell. - * cmd.exe takes `/c`; POSIX shells take `-c`, with `-l` unless - * PI_BASH_NO_LOGIN / CLAUDE_BASH_NO_LOGIN is set. + * cmd.exe takes `/c`; PowerShell (powershell.exe / pwsh) takes + * `-NoLogo -Command`, with `-NoProfile` when PI_BASH_NO_LOGIN / + * CLAUDE_BASH_NO_LOGIN is set (profile scripts are PowerShell's login-shell + * analog); POSIX shells take `-c`, with `-l` unless the same env is set. + * + * Exported for tests; `env` overrides the process env gate. */ -function getShellArgs(shell: string): string[] { +export function getShellArgs(shell: string, env: Record = $env): string[] { if (isCmdShell(shell)) return ["/c"]; - const noLogin = $env.PI_BASH_NO_LOGIN || $env.CLAUDE_BASH_NO_LOGIN; + const noLogin = env.PI_BASH_NO_LOGIN || env.CLAUDE_BASH_NO_LOGIN; + if (isPowerShell(shell)) { + return noLogin ? ["-NoLogo", "-NoProfile", "-Command"] : ["-NoLogo", "-Command"]; + } return noLogin ? ["-c"] : ["-l", "-c"]; } @@ -65,6 +72,16 @@ export function isCmdShell(shell: string): boolean { return basename === "cmd.exe" || basename === "cmd"; } +/** + * Whether the shell is Windows PowerShell or PowerShell Core (pwsh). Spawn + * paths must use `-Command`: passing the POSIX `-l -c` pair makes PowerShell + * parse `-l` as the command and fail with `The term '-l' is not recognized`. + */ +export function isPowerShell(shell: string): boolean { + const basename = shell.replace(/\\/g, "/").split("/").pop()?.toLowerCase(); + return basename === "powershell.exe" || basename === "powershell" || basename === "pwsh.exe" || basename === "pwsh"; +} + /** * Get shell prefix for wrapping commands (profilers, strace, etc.). */ diff --git a/packages/utils/test/procmgr.test.ts b/packages/utils/test/procmgr.test.ts index 1e9e8ff61..67426fe10 100644 --- a/packages/utils/test/procmgr.test.ts +++ b/packages/utils/test/procmgr.test.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { getAgentDir, MAIN_CONFIG_FILENAMES } from "../src/dirs"; -import { getShellConfig, resolveWindowsShell } from "../src/procmgr"; +import { getShellArgs, getShellConfig, resolveWindowsShell } from "../src/procmgr"; describe("getShellConfig", () => { it("directs invalid custom shell paths to the canonical config file", () => { @@ -15,6 +15,30 @@ describe("getShellConfig", () => { }); }); +describe("getShellArgs", () => { + it("uses -Command for PowerShell shells instead of the POSIX -l -c pair", () => { + // `powershell -l -c ` parses `-l` as the command and fails with + // `The term '-l' is not recognized`, breaking every spawn path for a + // shellPath pointed at PowerShell. + expect(getShellArgs("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", {})).toEqual([ + "-NoLogo", + "-Command", + ]); + expect(getShellArgs("C:\\Program Files\\PowerShell\\7\\pwsh.exe", {})).toEqual(["-NoLogo", "-Command"]); + expect(getShellArgs("/usr/bin/pwsh", {})).toEqual(["-NoLogo", "-Command"]); + }); + + it("maps the no-login env gate to -NoProfile for PowerShell", () => { + expect(getShellArgs("pwsh.exe", { PI_BASH_NO_LOGIN: "1" })).toEqual(["-NoLogo", "-NoProfile", "-Command"]); + }); + + it("keeps cmd.exe and POSIX shell args unchanged", () => { + expect(getShellArgs("C:\\Windows\\System32\\cmd.exe", {})).toEqual(["/c"]); + expect(getShellArgs("/bin/bash", {})).toEqual(["-l", "-c"]); + expect(getShellArgs("/bin/bash", { PI_BASH_NO_LOGIN: "1" })).toEqual(["-c"]); + }); +}); + describe("resolveWindowsShell", () => { const tempDirs: string[] = [];