From 5c8880ed3f43e0971037bf5fdda8a3e64127d1d2 Mon Sep 17 00:00:00 2001 From: Jhin Date: Sun, 1 Feb 2026 01:03:35 +0000 Subject: [PATCH] fix(process): resolve npm/pnpm spawn ENOENT on Windows On Windows, non-.exe commands like npm, pnpm, yarn, npx require their .cmd extension when using spawn(). This adds a resolveCommand() helper that automatically appends .cmd on Windows for these commands. Fixes #5773 --- src/process/exec.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/process/exec.ts b/src/process/exec.ts index 43b92ed53b..2af4ee99cd 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -7,6 +7,23 @@ import { resolveCommandStdio } from "./spawn-utils.js"; const execFileAsync = promisify(execFile); +/** + * Resolves a command for Windows compatibility. + * On Windows, non-.exe commands (like npm, pnpm) require their .cmd extension. + */ +function resolveCommand(command: string): string { + if (process.platform !== "win32") { + return command; + } + const basename = path.basename(command).toLowerCase(); + // Common npm-related commands that need .cmd extension on Windows + const cmdCommands = ["npm", "pnpm", "yarn", "npx"]; + if (cmdCommands.includes(basename)) { + return `${command}.cmd`; + } + return command; +} + // Simple promise-wrapped execFile with optional verbosity logging. export async function runExec( command: string, @@ -22,7 +39,7 @@ export async function runExec( encoding: "utf8" as const, }; try { - const { stdout, stderr } = await execFileAsync(command, args, options); + const { stdout, stderr } = await execFileAsync(resolveCommand(command), args, options); if (shouldLogVerbose()) { if (stdout.trim()) { logDebug(stdout.trim()); @@ -89,7 +106,7 @@ export async function runCommandWithTimeout( } const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); - const child = spawn(argv[0], argv.slice(1), { + const child = spawn(resolveCommand(argv[0]), argv.slice(1), { stdio, cwd, env: resolvedEnv,