import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process"; import { extname } from "node:path"; import { promisify } from "node:util"; import { createExternalCommandProcessEnv, type ProcessEnvRecord } from "../server/paseo-env.js"; import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand, } from "./windows-command.js"; const execFileAsync = promisify(execFile); interface ExternalEnvOptions { baseEnv?: ProcessEnvRecord; envMode?: "external" | "internal"; env?: ProcessEnvRecord; envOverlay?: ProcessEnvRecord; } export type SpawnProcessOptions = Omit & ExternalEnvOptions; interface ExecCommandOptions extends ExternalEnvOptions { cwd?: string; encoding?: BufferEncoding; killSignal?: NodeJS.Signals; timeout?: number; maxBuffer?: number; shell?: boolean | string; } interface ExecCommandResult { stdout: string; stderr: string; } function hasPathSeparator(value: string): boolean { return value.includes("/") || value.includes("\\"); } function shouldUseWindowsShell( command: string, requestedShell?: boolean | string, ): boolean | string { if (isWindowsCommandScript(command)) { return true; } if (requestedShell !== undefined) { return requestedShell; } return process.platform === "win32" && !hasPathSeparator(command) && !extname(command); } export function spawnProcess( command: string, args: string[], options?: SpawnProcessOptions, ): ChildProcess { const { baseEnv, env, envOverlay, ...spawnOptions } = options ?? {}; const resolvedBaseEnv = env ?? baseEnv ?? process.env; const isWindows = process.platform === "win32"; const shell = shouldUseWindowsShell(command, spawnOptions.shell); const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; const childEnv = options?.envMode === "internal" ? ({ ...resolvedBaseEnv, ...envOverlay } as NodeJS.ProcessEnv) : createExternalCommandProcessEnv( command, resolvedBaseEnv, ...(envOverlay ? [envOverlay] : []), ); return spawn(resolvedCommand, resolvedArgs, { ...spawnOptions, env: childEnv, shell, windowsHide: true, }); } export async function execCommand( command: string, args: string[], options?: ExecCommandOptions, ): Promise { const { baseEnv, env, envOverlay } = options ?? {}; const resolvedBaseEnv = env ?? baseEnv ?? process.env; const isWindows = process.platform === "win32"; const shell = shouldUseWindowsShell(command, options?.shell); const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; const childEnv = options?.envMode === "internal" ? ({ ...resolvedBaseEnv, ...envOverlay } as NodeJS.ProcessEnv) : createExternalCommandProcessEnv( command, resolvedBaseEnv, ...(envOverlay ? [envOverlay] : []), ); return execFileAsync(resolvedCommand, resolvedArgs, { cwd: options?.cwd, env: childEnv, encoding: options?.encoding ?? "utf8", killSignal: options?.killSignal, timeout: options?.timeout, maxBuffer: options?.maxBuffer, shell, windowsHide: true, }) as Promise; }