| import { execFile, spawn } from "node:child_process"; |
| import path from "node:path"; |
| import { promisify } from "node:util"; |
|
|
| import { danger, shouldLogVerbose } from "../globals.js"; |
| import { logDebug, logError } from "../logger.js"; |
| import { resolveCommandStdio } from "./spawn-utils.js"; |
|
|
| const execFileAsync = promisify(execFile); |
|
|
| |
| export async function runExec( |
| command: string, |
| args: string[], |
| opts: number | { timeoutMs?: number; maxBuffer?: number } = 10_000, |
| ): Promise<{ stdout: string; stderr: string }> { |
| const options = |
| typeof opts === "number" |
| ? { timeout: opts, encoding: "utf8" as const } |
| : { |
| timeout: opts.timeoutMs, |
| maxBuffer: opts.maxBuffer, |
| encoding: "utf8" as const, |
| }; |
| try { |
| const { stdout, stderr } = await execFileAsync(command, args, options); |
| if (shouldLogVerbose()) { |
| if (stdout.trim()) { |
| logDebug(stdout.trim()); |
| } |
| if (stderr.trim()) { |
| logError(stderr.trim()); |
| } |
| } |
| return { stdout, stderr }; |
| } catch (err) { |
| if (shouldLogVerbose()) { |
| logError(danger(`Command failed: ${command} ${args.join(" ")}`)); |
| } |
| throw err; |
| } |
| } |
|
|
| export type SpawnResult = { |
| stdout: string; |
| stderr: string; |
| code: number | null; |
| signal: NodeJS.Signals | null; |
| killed: boolean; |
| }; |
|
|
| export type CommandOptions = { |
| timeoutMs: number; |
| cwd?: string; |
| input?: string; |
| env?: NodeJS.ProcessEnv; |
| windowsVerbatimArguments?: boolean; |
| }; |
|
|
| export async function runCommandWithTimeout( |
| argv: string[], |
| optionsOrTimeout: number | CommandOptions, |
| ): Promise<SpawnResult> { |
| const options: CommandOptions = |
| typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; |
| const { timeoutMs, cwd, input, env } = options; |
| const { windowsVerbatimArguments } = options; |
| const hasInput = input !== undefined; |
|
|
| const shouldSuppressNpmFund = (() => { |
| const cmd = path.basename(argv[0] ?? ""); |
| if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") { |
| return true; |
| } |
| if (cmd === "node" || cmd === "node.exe") { |
| const script = argv[1] ?? ""; |
| return script.includes("npm-cli.js"); |
| } |
| return false; |
| })(); |
|
|
| const resolvedEnv = env ? { ...process.env, ...env } : { ...process.env }; |
| if (shouldSuppressNpmFund) { |
| if (resolvedEnv.NPM_CONFIG_FUND == null) { |
| resolvedEnv.NPM_CONFIG_FUND = "false"; |
| } |
| if (resolvedEnv.npm_config_fund == null) { |
| resolvedEnv.npm_config_fund = "false"; |
| } |
| } |
|
|
| const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); |
| const child = spawn(argv[0], argv.slice(1), { |
| stdio, |
| cwd, |
| env: resolvedEnv, |
| windowsVerbatimArguments, |
| }); |
| |
| return await new Promise((resolve, reject) => { |
| let stdout = ""; |
| let stderr = ""; |
| let settled = false; |
| const timer = setTimeout(() => { |
| if (typeof child.kill === "function") { |
| child.kill("SIGKILL"); |
| } |
| }, timeoutMs); |
|
|
| if (hasInput && child.stdin) { |
| child.stdin.write(input ?? ""); |
| child.stdin.end(); |
| } |
|
|
| child.stdout?.on("data", (d) => { |
| stdout += d.toString(); |
| }); |
| child.stderr?.on("data", (d) => { |
| stderr += d.toString(); |
| }); |
| child.on("error", (err) => { |
| if (settled) { |
| return; |
| } |
| settled = true; |
| clearTimeout(timer); |
| reject(err); |
| }); |
| child.on("close", (code, signal) => { |
| if (settled) { |
| return; |
| } |
| settled = true; |
| clearTimeout(timer); |
| resolve({ stdout, stderr, code, signal, killed: child.killed }); |
| }); |
| }); |
| } |
|
|