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,