|
1 | 1 | import { execFile, spawn } from "node:child_process"; |
| 2 | +import fs from "node:fs"; |
2 | 3 | import path from "node:path"; |
| 4 | +import process from "node:process"; |
3 | 5 | import { promisify } from "node:util"; |
4 | 6 | import { danger, shouldLogVerbose } from "../globals.js"; |
5 | 7 | import { logDebug, logError } from "../logger.js"; |
6 | 8 | import { resolveCommandStdio } from "./spawn-utils.js"; |
7 | 9 |
|
8 | 10 | const execFileAsync = promisify(execFile); |
9 | 11 |
|
| 12 | +/** |
| 13 | + * On Windows, Node 18.20.2+ (CVE-2024-27980) rejects spawning .cmd/.bat directly |
| 14 | + * without shell, causing EINVAL. Resolve npm/npx to node + cli script so we |
| 15 | + * spawn node.exe instead of npm.cmd. |
| 16 | + */ |
| 17 | +function resolveNpmArgvForWindows(argv: string[]): string[] | null { |
| 18 | + if (process.platform !== "win32" || argv.length === 0) { |
| 19 | + return null; |
| 20 | + } |
| 21 | + const basename = path |
| 22 | + .basename(argv[0]) |
| 23 | + .toLowerCase() |
| 24 | + .replace(/\.(cmd|exe|bat)$/, ""); |
| 25 | + const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null; |
| 26 | + if (!cliName) { |
| 27 | + return null; |
| 28 | + } |
| 29 | + const nodeDir = path.dirname(process.execPath); |
| 30 | + const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName); |
| 31 | + if (!fs.existsSync(cliPath)) { |
| 32 | + return null; |
| 33 | + } |
| 34 | + return [process.execPath, cliPath, ...argv.slice(1)]; |
| 35 | +} |
| 36 | + |
10 | 37 | /** |
11 | 38 | * Resolves a command for Windows compatibility. |
12 | | - * On Windows, non-.exe commands (like npm, pnpm) require their .cmd extension. |
| 39 | + * On Windows, non-.exe commands (like pnpm, yarn) are resolved to .cmd; npm/npx |
| 40 | + * are handled by resolveNpmArgvForWindows to avoid spawn EINVAL (no direct .cmd). |
13 | 41 | */ |
14 | 42 | function resolveCommand(command: string): string { |
15 | 43 | if (process.platform !== "win32") { |
16 | 44 | return command; |
17 | 45 | } |
18 | 46 | const basename = path.basename(command).toLowerCase(); |
19 | | - // Skip if already has an extension (.cmd, .exe, .bat, etc.) |
20 | 47 | const ext = path.extname(basename); |
21 | 48 | if (ext) { |
22 | 49 | return command; |
23 | 50 | } |
24 | | - // Common npm-related commands that need .cmd extension on Windows |
25 | | - const cmdCommands = ["npm", "pnpm", "yarn", "npx"]; |
| 51 | + const cmdCommands = ["pnpm", "yarn"]; |
26 | 52 | if (cmdCommands.includes(basename)) { |
27 | 53 | return `${command}.cmd`; |
28 | 54 | } |
@@ -58,7 +84,23 @@ export async function runExec( |
58 | 84 | encoding: "utf8" as const, |
59 | 85 | }; |
60 | 86 | try { |
61 | | - const { stdout, stderr } = await execFileAsync(resolveCommand(command), args, options); |
| 87 | + const argv = [command, ...args]; |
| 88 | + let execCommand: string; |
| 89 | + let execArgs: string[]; |
| 90 | + if (process.platform === "win32") { |
| 91 | + const resolved = resolveNpmArgvForWindows(argv); |
| 92 | + if (resolved) { |
| 93 | + execCommand = resolved[0] ?? ""; |
| 94 | + execArgs = resolved.slice(1); |
| 95 | + } else { |
| 96 | + execCommand = resolveCommand(command); |
| 97 | + execArgs = args; |
| 98 | + } |
| 99 | + } else { |
| 100 | + execCommand = resolveCommand(command); |
| 101 | + execArgs = args; |
| 102 | + } |
| 103 | + const { stdout, stderr } = await execFileAsync(execCommand, execArgs, options); |
62 | 104 | if (shouldLogVerbose()) { |
63 | 105 | if (stdout.trim()) { |
64 | 106 | logDebug(stdout.trim()); |
@@ -134,8 +176,9 @@ export async function runCommandWithTimeout( |
134 | 176 | } |
135 | 177 |
|
136 | 178 | const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); |
137 | | - const resolvedCommand = resolveCommand(argv[0] ?? ""); |
138 | | - const child = spawn(resolvedCommand, argv.slice(1), { |
| 179 | + const finalArgv = process.platform === "win32" ? (resolveNpmArgvForWindows(argv) ?? argv) : argv; |
| 180 | + const resolvedCommand = finalArgv !== argv ? (finalArgv[0] ?? "") : resolveCommand(argv[0] ?? ""); |
| 181 | + const child = spawn(resolvedCommand, finalArgv.slice(1), { |
139 | 182 | stdio, |
140 | 183 | cwd, |
141 | 184 | env: resolvedEnv, |
|
0 commit comments