Skip to content

pi-windows-npm-ENOENT #4665

@qzsecfr

Description

@qzsecfr

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

  1. 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)
  2. Install @earendil-works/pi-coding-agent@0.75.1
  3. 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():

  1. Build a variable table — collect all SET "VAR=value" declarations into a Map<string, string>
  2. Resolve variable references — recursively expand %VAR% tokens (depth-limited to 10)
  3. Locate the execution line — find the line containing %* (argument passthrough)
  4. Match the resolved execution line — apply NODE_SHIM_SCRIPT_RE after variable expansion
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions