import { execFile } from "child_process"; import { promisify } from "util"; import { existsSync } from "fs"; import { dirname, join } from "path"; import { execPath } from "process"; const execFileAsync = promisify(execFile); /** * Locate `npx-cli.js` shipped with the running Node.js installation. * * On Windows the `npx` on PATH is actually `npx.cmd`, which Node.js (since * 20.12 due to CVE-2024-27980) refuses to spawn from `execFile`/`spawn` * without `shell: true`. Going through a shell reintroduces quoting bugs for * user-supplied args. Instead we find the real `npx-cli.js` and invoke it * directly via the current `node` binary, which works identically on every * platform and needs no shell. */ function findNpxCli(): string | null { const nodeDir = dirname(execPath); const candidates = [ // Windows MSI installer layout: node.exe and node_modules share a dir join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js"), // Unix layout: .../bin/node + .../lib/node_modules/npm/bin/npx-cli.js join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js"), ]; for (const p of candidates) { try { if (existsSync(p)) return p; } catch { // ignore } } return null; } export interface RunNpxOptions { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv; } export interface RunNpxResult { stdout: string; stderr: string; } /** * Cross-platform wrapper for invoking `npx ` without ever using a * shell, so user-controlled arguments are never interpreted as shell syntax. */ export async function runNpx(args: string[], opts: RunNpxOptions = {}): Promise { const npxCli = findNpxCli(); const { command, commandArgs } = npxCli ? { command: execPath, commandArgs: [npxCli, ...args] } : { command: "npx", commandArgs: args }; return execFileAsync(command, commandArgs, { timeout: opts.timeout, cwd: opts.cwd, env: opts.env, }); }