Skip to content

fix: run the bundle build's npm spawn through npm's CLI entry point - #159

Open
AuroraAeon wants to merge 1 commit into
stepfun-ai:mainfrom
AuroraAeon:fix/windows-bundle-npm-spawn
Open

AuroraAeon wants to merge 1 commit into
stepfun-ai:mainfrom
AuroraAeon:fix/windows-bundle-npm-spawn

Conversation

@AuroraAeon

Copy link
Copy Markdown

Problem

From a fresh Windows checkout, ./test.sh cannot get past its build step: scripts/build-coding-agent-bundle.mjs spawns the app build with

execFileSync("npm", ["--prefix", appEntryDir, "run", "build"], { stdio: "inherit", cwd: repoRoot });

On Windows this fails in every naive form, which I reproduced in order on Node v25.2.1:

  1. As written upstreamENOENT: execFileSync does not resolve the npm.cmd shim through PATHEXT.

    Error: spawnSync npm ENOENT
        at execFileSync (node:child_process:954:15)
        at file:///.../scripts/build-coding-agent-bundle.mjs:181:1
    
  2. "npm.cmd"EINVAL: Node rejects spawning .cmd files without a shell (cmd-spawn hardening in recent Node releases).

  3. shell: true (the pattern profile-coding-agent-node.mjs uses) — cmd.exe receives the arguments unquoted, so --prefix <path> splits on the first space in the repo path:

    Unknown command: "Project\Step-Code\apps\cli"
    Error: Command failed: npm --prefix C:\Users\Lenovo\Documents\Default Project\Step-Code\apps\cli run build
    

    (That sibling script gets away with shell: true only because its arguments are fixed tokens and workspace-relative paths without spaces; --prefix here is an absolute path.)

So the documented contribution gate (npm run check + ./test.sh) is unreachable on Windows, including for repo paths as ordinary as ...\Default Project\Step-Code.

Fix

Run npm's own CLI entry point through the current Node binary — no shell, no .cmd, no quoting:

function resolveNpmEntryPoint() {
	const fromEnv = process.env.npm_execpath;
	if (fromEnv && fromEnv.endsWith(".js") && existsSync(fromEnv)) return fromEnv;
	const besideNode = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
	return existsSync(besideNode) ? besideNode : undefined;
}
const npmBuildArgs = ["--prefix", appEntryDir, "run", "build"];
const npmEntryPoint = resolveNpmEntryPoint();
if (npmEntryPoint) {
	execFileSync(process.execPath, [npmEntryPoint, ...npmBuildArgs], { stdio: "inherit", cwd: repoRoot });
} else {
	execFileSync("npm", npmBuildArgs, { stdio: "inherit", cwd: repoRoot, shell: process.platform === "win32" });
}

Resolution order:

  1. npm_execpath — set by npm (and pnpm) lifecycle scripts, i.e. every documented build entry point (build, build:offline, test.sh);
  2. npm-cli.js beside node.exe — covers manual node scripts/build-coding-agent-bundle.mjs on the standard Windows installer;
  3. PATH spawn with a win32 shell — preserves today's POSIX behavior verbatim as the last resort.

grep over *.{mjs,js,ts,mts} finds only this script and profile-coding-agent-node.mjs spawning npm/pnpm/npx; the latter's arguments are space-free relative paths and are intentionally left alone.

Verification (Windows 11, Node v25.2.1, npm 11, pnpm 9.15.9, repo path contains a space)

  • Fresh git clone into ...\Documents\Default Project\Step-Codepnpm install --frozen-lockfile ✅ exit 0.
  • Manual invocation (tier 2, no npm_execpath): node scripts/build-coding-agent-bundle.mjs ✅ exit 0 — produces apps/cli/dist/main.js and packages/coding-agent/dist/bundle/step.js. This is the exact command and path that failed with all three error modes above.
  • npm run build:offline (tier 1, lifecycle npm_execpath) ✅.
  • npm run check ✅ exit 0 (also run by the pre-commit hook at commit time).
  • ./test.sh — the build step, which previously could not complete on Windows at all, now finishes; its test:scripts phase passes 34/34.
  • Full workspace suite, same-checkout baseline comparison (pnpm -r --if-present test --no-bail, this branch vs clean adcf37b):
    • with the fix: 30 failing files; baseline: 30 failing files;
    • the only delta is one file in each direction (test/bash-close-hang-windows.test.ts only in the fixed run, test/session-file-invalid.test.ts only in the baseline) and both pass when re-run individually → flaky, not a regression;
    • all failures are the pre-existing Windows-only set (path separators, EACCES vs EPERM, ESM c: URL scheme, missing SIGCONT, repo path containing a space, …) that exist on clean upstream/main regardless of this change — same methodology as the baseline comparison in the discussion of fix: route search_web to the Step Plan MCP endpoint #158.

Notes

  • Linux/macOS behavior is unchanged: tier 1/2 exec the same npm CLI that the npm shim would run, and the tier-3 fallback spawns npm directly without a shell exactly as today.
  • No dependencies, no new packages; existsSync, dirname, and join are already imported.

build-coding-agent-bundle.mjs spawns `execFileSync("npm", ...)`, which
fails on Windows: ENOENT because the npm.cmd shim is not resolved,
EINVAL when npm.cmd is passed without a shell, and with shell:true the
unquoted `--prefix <path>` argument splits on spaces in the repo path,
so ./test.sh cannot complete its build step from a fresh checkout.

Resolve npm's JS entry via npm_execpath (set by npm/pnpm lifecycle
scripts), falling back to the npm-cli.js beside node.exe and then to the
previous PATH spawn (shell-resolved on win32 only). POSIX behavior is
unchanged.
Copilot AI lite review requested due to automatic review settings September 23, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants