Spaces:
Configuration error
Configuration error
| 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); | |
| // Simple promise-wrapped execFile with optional verbosity logging. | |
| 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, | |
| }); | |
| // Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed. | |
| 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 }); | |
| }); | |
| }); | |
| } | |