{"version":3,"file":"command-runner.d.ts","sourceRoot":"","sources":["../../src/code/command-runner.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC7B,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAClG;AASD,qBAAa,kBAAmB,YAAW,aAAa;IACjD,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAoHtG;CACD","sourcesContent":["import { spawn } from \"node:child_process\";\n\nconst DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;\nconst KILL_GRACE_MS = 1_000;\n\nexport interface CommandRunOptions {\n\tcwd: string;\n\tenv?: NodeJS.ProcessEnv;\n\tsignal?: AbortSignal;\n\ttimeoutMs?: number;\n\tmaxOutputBytes?: number;\n}\n\nexport interface CommandResult {\n\tcommand: string;\n\targs: string[];\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\ttimedOut: boolean;\n\taborted: boolean;\n\toutputLimitExceeded: boolean;\n}\n\nexport interface CommandRunner {\n\trun(command: string, args: readonly string[], options: CommandRunOptions): Promise<CommandResult>;\n}\n\nfunction appendBounded(chunks: Buffer[], chunk: Buffer, remaining: number): number {\n\tif (remaining <= 0) return 0;\n\tconst accepted = chunk.subarray(0, remaining);\n\tif (accepted.length > 0) chunks.push(accepted);\n\treturn accepted.length;\n}\n\nexport class SpawnCommandRunner implements CommandRunner {\n\tasync run(command: string, args: readonly string[], options: CommandRunOptions): Promise<CommandResult> {\n\t\tif (!command) throw new Error(\"Command must not be empty\");\n\t\tif (!Number.isSafeInteger(options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES)) {\n\t\t\tthrow new Error(\"maxOutputBytes must be a safe integer\");\n\t\t}\n\t\tconst maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;\n\t\tif (maxOutputBytes <= 0) throw new Error(\"maxOutputBytes must be positive\");\n\t\tif (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) {\n\t\t\tthrow new Error(\"timeoutMs must be a positive safe integer\");\n\t\t}\n\n\t\treturn new Promise<CommandResult>((resolvePromise, rejectPromise) => {\n\t\t\tconst copiedArgs = [...args];\n\t\t\tconst child = spawn(command, copiedArgs, {\n\t\t\t\tcwd: options.cwd,\n\t\t\t\tenv: options.env,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tconst stdoutChunks: Buffer[] = [];\n\t\t\tconst stderrChunks: Buffer[] = [];\n\t\t\tlet capturedBytes = 0;\n\t\t\tlet killed = false;\n\t\t\tlet timedOut = false;\n\t\t\tlet aborted = false;\n\t\t\tlet outputLimitExceeded = false;\n\t\t\tlet settled = false;\n\t\t\tlet timeout: NodeJS.Timeout | undefined;\n\t\t\tlet forceKillTimeout: NodeJS.Timeout | undefined;\n\n\t\t\tconst signalProcessTree = (signal: NodeJS.Signals): void => {\n\t\t\t\tif (process.platform !== \"win32\" && child.pid !== undefined) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tprocess.kill(-child.pid, signal);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (typeof error !== \"object\" || error === null || !(\"code\" in error) || error.code !== \"ESRCH\")\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tchild.kill(signal);\n\t\t\t};\n\n\t\t\tconst terminateProcessTree = (signal: NodeJS.Signals): void => {\n\t\t\t\ttry {\n\t\t\t\t\tsignalProcessTree(signal);\n\t\t\t\t} catch {\n\t\t\t\t\tchild.kill(signal);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst kill = (): void => {\n\t\t\t\tif (killed) return;\n\t\t\t\tkilled = true;\n\t\t\t\tterminateProcessTree(\"SIGTERM\");\n\t\t\t\tforceKillTimeout = setTimeout(() => terminateProcessTree(\"SIGKILL\"), KILL_GRACE_MS);\n\t\t\t\tforceKillTimeout.unref();\n\t\t\t};\n\t\t\tconst onAbort = (): void => {\n\t\t\t\taborted = true;\n\t\t\t\tkill();\n\t\t\t};\n\t\t\tconst onData =\n\t\t\t\t(target: Buffer[]) =>\n\t\t\t\t(value: Buffer | string): void => {\n\t\t\t\t\tconst chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);\n\t\t\t\t\tconst accepted = appendBounded(target, chunk, maxOutputBytes - capturedBytes);\n\t\t\t\t\tcapturedBytes += accepted;\n\t\t\t\t\tif (accepted < chunk.length) {\n\t\t\t\t\t\toutputLimitExceeded = true;\n\t\t\t\t\t\tkill();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\tconst cleanup = (): void => {\n\t\t\t\tif (timeout) clearTimeout(timeout);\n\t\t\t\tif (forceKillTimeout) clearTimeout(forceKillTimeout);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t};\n\n\t\t\tchild.stdout.on(\"data\", onData(stdoutChunks));\n\t\t\tchild.stderr.on(\"data\", onData(stderrChunks));\n\t\t\tchild.once(\"error\", (error) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tterminateProcessTree(\"SIGKILL\");\n\t\t\t\tcleanup();\n\t\t\t\trejectPromise(error);\n\t\t\t});\n\t\t\tchild.once(\"close\", (code) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tterminateProcessTree(\"SIGKILL\");\n\t\t\t\tcleanup();\n\t\t\t\tresolvePromise({\n\t\t\t\t\tcommand,\n\t\t\t\t\targs: copiedArgs,\n\t\t\t\t\tstdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n\t\t\t\t\tstderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n\t\t\t\t\tcode: code ?? 1,\n\t\t\t\t\tkilled,\n\t\t\t\t\ttimedOut,\n\t\t\t\t\taborted,\n\t\t\t\t\toutputLimitExceeded,\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tif (options.signal?.aborted) onAbort();\n\t\t\telse options.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tif (options.timeoutMs !== undefined) {\n\t\t\t\ttimeout = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tkill();\n\t\t\t\t}, options.timeoutMs);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}