What happened?
On Windows, pi v0.75.1 fails to spawn npm-related commands with spawn C:\Program Files\nodejs\npm ENOENT. All npm-dependent functionality is broken — pi, pi update, or any operation that invokes the npm family.
This is a regression introduced by commit 6b872be2 which replaced shouldUseWindowsShell() with resolveSpawnCommand() in packages/coding-agent/src/utils/child-process.ts.
Steps to reproduce
- Windows, Node.js installed at
C:\Program Files\nodejs\ (shipped by Node.js installer since 2024-11-07, includes both npm bash script and npm.cmd)
- Install
@earendil-works/pi-coding-agent@0.75.1
- Run
pi or pi update
Error: spawn C:\Program Files\nodejs\npm ENOENT
Root Cause
Two bugs in resolveSpawnCommand():
Bug 1: Extension lookup order picks the wrong file
const WINDOWS_COMMAND_EXTENSIONS = ["", ".exe", ".cmd", ".bat"];
The empty string "" comes first in PATH traversal. The Node.js install directory contains both npm (a Unix bash script with #!/usr/bin/env bash shebang) and npm.cmd (a Windows batch file). findWindowsCommand() finds the extensionless npm bash script before npm.cmd.
resolveSpawnCommand() then checks WINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i — since npm has no .cmd/.bat extension, it is treated as a regular executable and spawned directly. Node.js on Windows cannot execute a bash script → ENOENT.
Bug 2: Script entrypoint detection is structurally fragile
Even if Bug 1 were fixed and npm.cmd was found, findNodeShimScript() has two sub-issues:
2a. First-match-only regex
const match = readFileSync(shimPath, "utf-8").match(NODE_SHIM_SCRIPT_RE);
String.match() with a non-global regex returns only the first match. A typical npm.cmd has two %~dp0...js references:
SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js" ← first match (NOT the entrypoint)
SET "NPM_CLI_JS=%~dp0\node_modules\npm\bin\npm-cli.js" ← second match (IS the entrypoint)
The first match returns npm-prefix.js instead of the actual execution script npm-cli.js.
2b. Execution line never matches the regex
The actual execution line in npm.cmd:
"%NODE_EXE%" "%NPM_CLI_JS%" %*
This uses %NPM_CLI_JS% (a variable reference), not a literal %~dp0...js path. NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/i only matches SET declaration lines, never the execution line itself. This means any heuristic based solely on regex match order (first/last) is implicitly relying on the internal declaration order of npm.cmd, which is fragile.
Proposed Fix
Fix 1: Split extension lists by context
// PATH search — exclude "" (matches Windows PATHEXT behavior)
const PATH_EXTENSIONS = [".exe", ".cmd", ".bat"];
// Explicit-path commands — allow "" (user intentionally specified a path)
const EXPLICIT_PATH_EXTENSIONS = ["", ".exe", ".cmd", ".bat"];
findWindowsCommand() selects the appropriate list based on whether the command contains a path separator. On PATH traversal, npm.cmd is now found before the extensionless npm bash script.
Fix 2: Structural batch variable resolution for entrypoint detection
Replace the naive first-match approach with structured batch parsing in findNodeShimScript():
- Build a variable table — collect all
SET "VAR=value" declarations into a Map<string, string>
- Resolve variable references — recursively expand
%VAR% tokens (depth-limited to 10)
- Locate the execution line — find the line containing
%* (argument passthrough)
- Match the resolved execution line — apply
NODE_SHIM_SCRIPT_RE after variable expansion
- Fallback — if execution-line parsing fails, try all regex matches in reverse order
For npm.cmd's execution line "%NODE_EXE%" "%NPM_CLI_JS%" %*, resolving %NPM_CLI_JS% yields %~dp0\node_modules\npm\bin\npm-cli.js, which the regex now correctly matches.
Additional fixes
- UNC path detection:
hasPath regex updated from /^[a-zA-Z]:/ to /^[a-zA-Z]:|^\\\\/ to handle \\server\share\tool paths
- Environment variable reading: Removed
env.path fallback (only env.PATH and env.Path are standard Windows variables)
Verification
resolveSpawnCommand('npm', ['--version'])
// → { command: "node.exe", args: ["npm-cli.js", "--version"] } ✅
resolveSpawnCommand('npx', ['--version'])
// → { command: "node.exe", args: ["npx-cli.js", "--version"] } ✅
Both correctly resolve to the actual entrypoint scripts via batch variable resolution, not the fragile first/last-match heuristic.
Expected behavior
pi, pi update, and any npm-dependent operations should succeed without errors. The npm command should be resolved to the correct Windows batch shim (npm.cmd) or its underlying Node.js entrypoint script (npm-cli.js), and execute normally.
Version
0.75.1
What happened?
On Windows, pi v0.75.1 fails to spawn npm-related commands with
spawn C:\Program Files\nodejs\npm ENOENT. All npm-dependent functionality is broken —pi,pi update, or any operation that invokes the npm family.This is a regression introduced by commit
6b872be2which replacedshouldUseWindowsShell()withresolveSpawnCommand()inpackages/coding-agent/src/utils/child-process.ts.Steps to reproduce
C:\Program Files\nodejs\(shipped by Node.js installer since 2024-11-07, includes bothnpmbash script andnpm.cmd)@earendil-works/pi-coding-agent@0.75.1piorpi updateRoot Cause
Two bugs in
resolveSpawnCommand():Bug 1: Extension lookup order picks the wrong file
The empty string
""comes first in PATH traversal. The Node.js install directory contains bothnpm(a Unix bash script with#!/usr/bin/env bashshebang) andnpm.cmd(a Windows batch file).findWindowsCommand()finds the extensionlessnpmbash script beforenpm.cmd.resolveSpawnCommand()then checksWINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i— sincenpmhas no.cmd/.batextension, it is treated as a regular executable and spawned directly. Node.js on Windows cannot execute a bash script → ENOENT.Bug 2: Script entrypoint detection is structurally fragile
Even if Bug 1 were fixed and
npm.cmdwas found,findNodeShimScript()has two sub-issues:2a. First-match-only regex
String.match()with a non-global regex returns only the first match. A typicalnpm.cmdhas two%~dp0...jsreferences:The first match returns
npm-prefix.jsinstead of the actual execution scriptnpm-cli.js.2b. Execution line never matches the regex
The actual execution line in
npm.cmd:This uses
%NPM_CLI_JS%(a variable reference), not a literal%~dp0...jspath.NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/ionly matchesSETdeclaration lines, never the execution line itself. This means any heuristic based solely on regex match order (first/last) is implicitly relying on the internal declaration order ofnpm.cmd, which is fragile.Proposed Fix
Fix 1: Split extension lists by context
findWindowsCommand()selects the appropriate list based on whether the command contains a path separator. On PATH traversal,npm.cmdis now found before the extensionlessnpmbash script.Fix 2: Structural batch variable resolution for entrypoint detection
Replace the naive first-match approach with structured batch parsing in
findNodeShimScript():SET "VAR=value"declarations into aMap<string, string>%VAR%tokens (depth-limited to 10)%*(argument passthrough)NODE_SHIM_SCRIPT_REafter variable expansionFor
npm.cmd's execution line"%NODE_EXE%" "%NPM_CLI_JS%" %*, resolving%NPM_CLI_JS%yields%~dp0\node_modules\npm\bin\npm-cli.js, which the regex now correctly matches.Additional fixes
hasPathregex updated from/^[a-zA-Z]:/to/^[a-zA-Z]:|^\\\\/to handle\\server\share\toolpathsenv.pathfallback (onlyenv.PATHandenv.Pathare standard Windows variables)Verification
Both correctly resolve to the actual entrypoint scripts via batch variable resolution, not the fragile first/last-match heuristic.
Expected behavior
pi,pi update, and any npm-dependent operations should succeed without errors. The npm command should be resolved to the correct Windows batch shim (npm.cmd) or its underlying Node.js entrypoint script (npm-cli.js), and execute normally.Version
0.75.1