fix(utils): support PowerShell as custom shellPath

This commit is contained in:
metaphorics
2026-08-05 09:33:04 +00:00
parent 06477855d1
commit b86f788ae7
3 changed files with 50 additions and 5 deletions
+4
View File
@@ -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
+21 -4
View File
@@ -50,12 +50,19 @@ function buildSpawnEnv(shell: string): Record<string, string> {
/**
* 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<string, string | undefined> = $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.).
*/
+25 -1
View File
@@ -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 <cmd>` 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[] = [];