{"version":3,"file":"nodejs.d.ts","sourceRoot":"","sources":["../../../src/harness/env/nodejs.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { constants, createReadStream, createWriteStream, type WriteStream } from \"node:fs\";\nimport {\n\taccess,\n\tappendFile,\n\tlstat,\n\tmkdir,\n\tmkdtemp,\n\treaddir,\n\treadFile,\n\trealpath,\n\trename,\n\trm,\n\twriteFile,\n} from \"node:fs/promises\";\nimport { homedir, constants as osConstants, tmpdir } from \"node:os\";\nimport { basename, isAbsolute, join, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Context } from \"../context.ts\";\nimport {\n\ttype ExecutionEnv,\n\tExecutionError,\n\terr,\n\tFileError,\n\ttype FileInfo,\n\ttype FileKind,\n\tok,\n\ttype Result,\n\ttype ShellExecOptions,\n\ttype ShellExecResult,\n\ttoError,\n} from \"../types.ts\";\nimport { OutputCapture } from \"../utils/output-capture.ts\";\n\nconst MAX_TIMEOUT_MS = 2_147_483_647;\nconst MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;\nconst EXIT_STDIO_GRACE_MS = 100;\nconst SPILL_HIGH_WATER_MARK = 8 * 1024 * 1024;\n\ntype SpillChunk = string | Uint8Array;\n\nfunction resolveTimeoutMs(timeout: number | undefined): Result<number | undefined, ExecutionError> {\n\tif (timeout === undefined) return ok(undefined);\n\tif (!Number.isFinite(timeout) || timeout <= 0) {\n\t\treturn err(new ExecutionError(\"timeout\", \"Invalid timeout: must be a finite number of seconds\"));\n\t}\n\n\tconst timeoutMs = timeout * 1000;\n\tif (timeoutMs > MAX_TIMEOUT_MS) {\n\t\treturn err(new ExecutionError(\"timeout\", `Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`));\n\t}\n\treturn ok(timeoutMs);\n}\n\nfunction resolvePath(cwd: string, path: string): string {\n\tlet normalized = path;\n\tif (normalized === \"~\") {\n\t\tnormalized = homedir();\n\t} else if (normalized.startsWith(\"~/\") || (process.platform === \"win32\" && normalized.startsWith(\"~\\\\\"))) {\n\t\tnormalized = join(homedir(), normalized.slice(2));\n\t} else if (normalized.startsWith(\"file://\")) {\n\t\ttry {\n\t\t\tnormalized = fileURLToPath(normalized);\n\t\t} catch {\n\t\t\t// Keep malformed URLs as ordinary paths so filesystem methods preserve their non-throwing contract.\n\t\t}\n\t}\n\treturn isAbsolute(normalized) ? resolve(normalized) : resolve(cwd, normalized);\n}\n\nfunction fileKindFromStats(stats: {\n\tisFile(): boolean;\n\tisDirectory(): boolean;\n\tisSymbolicLink(): boolean;\n}): FileKind | undefined {\n\tif (stats.isFile()) return \"file\";\n\tif (stats.isDirectory()) return \"directory\";\n\tif (stats.isSymbolicLink()) return \"symlink\";\n\treturn undefined;\n}\n\nfunction fileInfoFromStats(\n\tpath: string,\n\tstats: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean; size: number; mtimeMs: number },\n): Result<FileInfo, FileError> {\n\tconst kind = fileKindFromStats(stats);\n\tif (!kind) return err(new FileError(\"invalid\", \"Unsupported file type\", path));\n\treturn ok({\n\t\tname: basename(path),\n\t\tpath,\n\t\tkind,\n\t\tsize: stats.size,\n\t\tmtimeMs: stats.mtimeMs,\n\t});\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n\treturn error instanceof Error && \"code\" in error;\n}\n\nfunction toFileError(error: unknown, fallbackPath?: string): FileError {\n\tif (error instanceof FileError) return error;\n\tconst cause = toError(error);\n\tconst nodeError = isNodeError(error) ? error : undefined;\n\tconst path = typeof nodeError?.path === \"string\" ? nodeError.path : fallbackPath;\n\tif (nodeError) {\n\t\tconst message = nodeError.message;\n\t\tswitch (nodeError.code) {\n\t\t\tcase \"ABORT_ERR\":\n\t\t\t\treturn new FileError(\"aborted\", message, path, cause);\n\t\t\tcase \"ENOENT\":\n\t\t\t\treturn new FileError(\"not_found\", message, path, cause);\n\t\t\tcase \"EACCES\":\n\t\t\tcase \"EPERM\":\n\t\t\t\treturn new FileError(\"permission_denied\", message, path, cause);\n\t\t\tcase \"ENOTDIR\":\n\t\t\t\treturn new FileError(\"not_directory\", message, path, cause);\n\t\t\tcase \"EISDIR\":\n\t\t\t\treturn new FileError(\"is_directory\", message, path, cause);\n\t\t\tcase \"EINVAL\":\n\t\t\t\treturn new FileError(\"invalid\", message, path, cause);\n\t\t}\n\t}\n\treturn new FileError(\"unknown\", cause.message, path, cause);\n}\n\nfunction abortResult<TValue>(signal: AbortSignal | undefined, path?: string): Result<TValue, FileError> | undefined {\n\treturn signal?.aborted ? err(new FileError(\"aborted\", \"aborted\", path)) : undefined;\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait access(path, constants.F_OK);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function runCommand(\n\tcommand: string,\n\targs: string[],\n\ttimeoutMs: number,\n): Promise<{ stdout: string; status: number | null }> {\n\treturn await new Promise((resolve) => {\n\t\tlet stdout = \"\";\n\t\tlet child: ReturnType<typeof spawn>;\n\t\ttry {\n\t\t\tchild = spawn(command, args, {\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t} catch {\n\t\t\tresolve({ stdout: \"\", status: null });\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(() => {\n\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t}, timeoutMs);\n\t\tchild.stdout?.setEncoding(\"utf8\");\n\t\tchild.stdout?.on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.on(\"error\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\tresolve({ stdout: \"\", status: null });\n\t\t});\n\t\tchild.on(\"close\", (status) => {\n\t\t\tclearTimeout(timeout);\n\t\t\tresolve({ stdout, status });\n\t\t});\n\t});\n}\n\nasync function findBashOnPath(): Promise<string | null> {\n\tconst result =\n\t\tprocess.platform === \"win32\"\n\t\t\t? await runCommand(\"where\", [\"bash.exe\"], 5000)\n\t\t\t: await runCommand(\"which\", [\"bash\"], 5000);\n\tif (result.status !== 0 || !result.stdout) return null;\n\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\treturn firstMatch && (await pathExists(firstMatch)) ? firstMatch : null;\n}\n\ninterface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\n\nasync function getShellConfig(customShellPath?: string): Promise<Result<ShellConfig, ExecutionError>> {\n\tif (customShellPath) {\n\t\tif (await pathExists(customShellPath)) {\n\t\t\treturn ok(getBashShellConfig(customShellPath));\n\t\t}\n\t\treturn err(new ExecutionError(\"shell_unavailable\", `Custom shell path not found: ${customShellPath}`));\n\t}\n\tif (process.platform === \"win32\") {\n\t\tconst candidates: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) candidates.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) candidates.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\tfor (const candidate of candidates) {\n\t\t\tif (await pathExists(candidate)) {\n\t\t\t\treturn ok(getBashShellConfig(candidate));\n\t\t\t}\n\t\t}\n\t\tconst bashOnPath = await findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\treturn ok(getBashShellConfig(bashOnPath));\n\t\t}\n\t\treturn err(\n\t\t\tnew ExecutionError(\n\t\t\t\t\"shell_unavailable\",\n\t\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t\t`  1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t\t`  2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t\t\"  3. Configure an explicit shellPath\\n\\n\" +\n\t\t\t\t\t`Searched Git Bash in:\\n${candidates.map((path) => `  ${path}`).join(\"\\n\")}`,\n\t\t\t),\n\t\t);\n\t}\n\n\tif (await pathExists(\"/bin/bash\")) {\n\t\treturn ok(getBashShellConfig(\"/bin/bash\"));\n\t}\n\tconst bashOnPath = await findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn ok(getBashShellConfig(bashOnPath));\n\t}\n\treturn ok({ shell: \"sh\", args: [\"-c\"] });\n}\n\nfunction getShellEnv(\n\tbaseEnv?: NodeJS.ProcessEnv,\n\textraEnv?: Record<string, string>,\n\tinheritEnv = true,\n): NodeJS.ProcessEnv {\n\tif (!inheritEnv) return { ...extraEnv };\n\treturn {\n\t\t...process.env,\n\t\t...baseEnv,\n\t\t...extraEnv,\n\t};\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tconst child = spawn(\n\t\t\t\tjoin(process.env.SystemRoot ?? \"C:\\\\Windows\", \"System32\", \"taskkill.exe\"),\n\t\t\t\t[\"/F\", \"/T\", \"/PID\", String(pid)],\n\t\t\t\t{\n\t\t\t\t\tstdio: \"ignore\",\n\t\t\t\t\tdetached: true,\n\t\t\t\t\twindowsHide: true,\n\t\t\t\t},\n\t\t\t);\n\t\t\t// A failed spawn emits \"error\" asynchronously; consume it to avoid crashing Node.\n\t\t\tchild.once(\"error\", () => {});\n\t\t} catch {\n\t\t\t// Ignore errors.\n\t\t}\n\t\treturn;\n\t}\n\n\ttry {\n\t\tprocess.kill(-pid, \"SIGKILL\");\n\t} catch {\n\t\ttry {\n\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Process already dead.\n\t\t}\n\t}\n}\n\nfunction waitForChildProcess(\n\tchild: ChildProcess,\n\tspillIsDraining: () => boolean,\n): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {\n\treturn new Promise((resolvePromise, reject) => {\n\t\tlet settled = false;\n\t\tlet exited = false;\n\t\tlet exitCode: number | null = null;\n\t\tlet exitSignal: NodeJS.Signals | null = null;\n\t\tlet postExitTimer: ReturnType<typeof setTimeout> | undefined;\n\t\tlet stdoutEnded = child.stdout === null;\n\t\tlet stderrEnded = child.stderr === null;\n\n\t\tconst cleanup = (): void => {\n\t\t\tif (postExitTimer) clearTimeout(postExitTimer);\n\t\t\tchild.removeListener(\"error\", onError);\n\t\t\tchild.removeListener(\"exit\", onExit);\n\t\t\tchild.removeListener(\"close\", onClose);\n\t\t\tchild.stdout?.removeListener(\"end\", onStdoutEnd);\n\t\t\tchild.stderr?.removeListener(\"end\", onStderrEnd);\n\t\t\tchild.stdout?.removeListener(\"data\", onData);\n\t\t\tchild.stderr?.removeListener(\"data\", onData);\n\t\t};\n\t\tconst finalize = (): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tchild.stdout?.destroy();\n\t\t\tchild.stderr?.destroy();\n\t\t\tresolvePromise({ code: exitCode, signal: exitSignal });\n\t\t};\n\t\tconst maybeFinalizeAfterExit = (): void => {\n\t\t\tif (exited && stdoutEnded && stderrEnded) finalize();\n\t\t};\n\t\tconst armIdleTimer = (): void => {\n\t\t\tif (postExitTimer) clearTimeout(postExitTimer);\n\t\t\tpostExitTimer = setTimeout(() => {\n\t\t\t\tif (spillIsDraining()) armIdleTimer();\n\t\t\t\telse finalize();\n\t\t\t}, EXIT_STDIO_GRACE_MS);\n\t\t};\n\t\tconst onData = (): void => {\n\t\t\tif (exited && !settled) armIdleTimer();\n\t\t};\n\t\tconst onStdoutEnd = (): void => {\n\t\t\tstdoutEnded = true;\n\t\t\tmaybeFinalizeAfterExit();\n\t\t};\n\t\tconst onStderrEnd = (): void => {\n\t\t\tstderrEnded = true;\n\t\t\tmaybeFinalizeAfterExit();\n\t\t};\n\t\tconst onError = (error: Error): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\treject(error);\n\t\t};\n\t\tconst onExit = (code: number | null, signal: NodeJS.Signals | null): void => {\n\t\t\texited = true;\n\t\t\texitCode = code;\n\t\t\texitSignal = signal;\n\t\t\tmaybeFinalizeAfterExit();\n\t\t\tif (!settled) armIdleTimer();\n\t\t};\n\t\tconst onClose = (code: number | null, signal: NodeJS.Signals | null): void => {\n\t\t\texitCode = code;\n\t\t\texitSignal = signal;\n\t\t\tfinalize();\n\t\t};\n\n\t\tchild.stdout?.once(\"end\", onStdoutEnd);\n\t\tchild.stderr?.once(\"end\", onStderrEnd);\n\t\tchild.stdout?.on(\"data\", onData);\n\t\tchild.stderr?.on(\"data\", onData);\n\t\tchild.once(\"error\", onError);\n\t\tchild.once(\"exit\", onExit);\n\t\tchild.once(\"close\", onClose);\n\t});\n}\n\nexport class NodeExecutionEnv implements ExecutionEnv {\n\tcwd: string;\n\tprivate shellPath?: string;\n\tprivate shellEnv?: NodeJS.ProcessEnv;\n\tprivate activeChildPids = new Set<number>();\n\n\tconstructor(options: { cwd: string; shellPath?: string; shellEnv?: NodeJS.ProcessEnv }) {\n\t\tthis.cwd = options.cwd;\n\t\tthis.shellPath = options.shellPath;\n\t\tthis.shellEnv = options.shellEnv;\n\t}\n\n\tasync absolutePath(path: string, _context: Context): Promise<Result<string, FileError>> {\n\t\treturn ok(resolvePath(this.cwd, path));\n\t}\n\n\tasync joinPath(parts: string[], _context: Context): Promise<Result<string, FileError>> {\n\t\treturn ok(join(...parts));\n\t}\n\n\tasync exec(\n\t\tcommand: string,\n\t\toptions: ShellExecOptions | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<ShellExecResult, ExecutionError>> {\n\t\tconst signal = context.abortSignal;\n\t\tif (signal?.aborted) return err(new ExecutionError(\"aborted\", \"aborted\"));\n\t\tconst timeoutMsResult = resolveTimeoutMs(options?.timeout);\n\t\tif (!timeoutMsResult.ok) return err(timeoutMsResult.error);\n\t\tconst timeoutMs = timeoutMsResult.value;\n\n\t\tconst cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;\n\t\tconst shellConfig = await getShellConfig(this.shellPath);\n\t\tif (!shellConfig.ok) return shellConfig;\n\t\ttry {\n\t\t\tawait access(cwd, constants.F_OK);\n\t\t} catch (error) {\n\t\t\tconst cause = toError(error);\n\t\t\treturn err(\n\t\t\t\tnew ExecutionError(\n\t\t\t\t\t\"spawn_error\",\n\t\t\t\t\t`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`,\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\treturn await new Promise((resolvePromise) => {\n\t\t\tlet settled = false;\n\t\t\tlet timedOut = false;\n\t\t\tlet callbackError: ExecutionError | undefined;\n\t\t\tlet spillError: ExecutionError | undefined;\n\t\t\tlet child: ReturnType<typeof spawn> | undefined;\n\t\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\n\t\t\tconst spillPrefix: SpillChunk[] = [];\n\t\t\tlet spillPath: string | undefined;\n\t\t\tconst spillQueue: SpillChunk[] = [];\n\t\t\tlet spillStart: Promise<void> | undefined;\n\t\t\tlet spillStream: WriteStream | undefined;\n\t\t\tlet spillBackpressured = false;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child?.pid) killProcessTree(child.pid);\n\t\t\t};\n\t\t\tconst failCallback = (error: unknown) => {\n\t\t\t\tif (callbackError !== undefined) return;\n\t\t\t\tconst cause = toError(error);\n\t\t\t\tcallbackError = new ExecutionError(\"callback_error\", cause.message, cause);\n\t\t\t\tonAbort();\n\t\t\t};\n\t\t\tlet capture: OutputCapture;\n\t\t\ttry {\n\t\t\t\tcapture = new OutputCapture(options?.capture, context, {\n\t\t\t\t\tonUpdate: options?.onUpdate,\n\t\t\t\t\tonError: failCallback,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst cause = toError(error);\n\t\t\t\tresolvePromise(err(new ExecutionError(\"unknown\", cause.message, cause)));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst settle = (result: Result<ShellExecResult, ExecutionError>) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\t\tif (signal) signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tif (child?.pid) this.activeChildPids.delete(child.pid);\n\t\t\t\tcapture.dispose();\n\t\t\t\tresolvePromise(result);\n\t\t\t};\n\t\t\tconst pauseOutput = () => {\n\t\t\t\tchild?.stdout?.pause();\n\t\t\t\tchild?.stderr?.pause();\n\t\t\t};\n\t\t\tconst resumeOutput = () => {\n\t\t\t\tif (callbackError || spillError || timedOut || signal?.aborted || spillBackpressured) return;\n\t\t\t\tchild?.stdout?.resume();\n\t\t\t\tchild?.stderr?.resume();\n\t\t\t};\n\t\t\tconst failSpill = (error: unknown) => {\n\t\t\t\tif (spillError !== undefined) return;\n\t\t\t\tconst cause = toError(error);\n\t\t\t\tspillError = new ExecutionError(\n\t\t\t\t\t\"unknown\",\n\t\t\t\t\t`Failed to preserve complete shell output: ${cause.message}`,\n\t\t\t\t\tcause,\n\t\t\t\t);\n\t\t\t\tspillBackpressured = false;\n\t\t\t\tonAbort();\n\t\t\t};\n\t\t\tconst writeSpill = (chunk: SpillChunk): void => {\n\t\t\t\tif (spillStream === undefined || chunk.length === 0) return;\n\t\t\t\tif (spillStream.write(chunk) || spillBackpressured) return;\n\t\t\t\tspillBackpressured = true;\n\t\t\t\tpauseOutput();\n\t\t\t\tspillStream.once(\"drain\", () => {\n\t\t\t\t\tspillBackpressured = false;\n\t\t\t\t\tresumeOutput();\n\t\t\t\t});\n\t\t\t};\n\t\t\tconst startSpill = (chunk: SpillChunk): void => {\n\t\t\t\tif (spillStream !== undefined) {\n\t\t\t\t\twriteSpill(chunk);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tspillQueue.push(chunk);\n\t\t\t\tif (spillStart !== undefined) return;\n\t\t\t\tpauseOutput();\n\t\t\t\tspillStart = (async () => {\n\t\t\t\t\tconst created = await this.createTempFile({ prefix: \"pi-output-\", suffix: \".log\" }, context);\n\t\t\t\t\tif (!created.ok) throw created.error;\n\t\t\t\t\tspillPath = created.value;\n\t\t\t\t\tcapture.setSpillPath(spillPath);\n\t\t\t\t\tspillStream = createWriteStream(spillPath, { flags: \"a\", highWaterMark: SPILL_HIGH_WATER_MARK });\n\t\t\t\t\tspillStream.on(\"error\", failSpill);\n\t\t\t\t\tfor (const queued of spillQueue) writeSpill(queued);\n\t\t\t\t\tspillQueue.length = 0;\n\t\t\t\t})()\n\t\t\t\t\t.catch(failSpill)\n\t\t\t\t\t.finally(resumeOutput);\n\t\t\t};\n\t\t\tconst finishSpill = async (): Promise<void> => {\n\t\t\t\tawait spillStart;\n\t\t\t\tconst stream = spillStream;\n\t\t\t\tif (stream === undefined || spillError !== undefined || stream.destroyed) return;\n\t\t\t\tawait new Promise<void>((resolveFinish) => {\n\t\t\t\t\tstream.once(\"error\", () => resolveFinish());\n\t\t\t\t\tstream.once(\"finish\", resolveFinish);\n\t\t\t\t\tstream.end();\n\t\t\t\t});\n\t\t\t};\n\n\t\t\ttry {\n\t\t\t\tconst commandFromStdin = shellConfig.value.commandTransport === \"stdin\";\n\t\t\t\tchild = spawn(\n\t\t\t\t\tshellConfig.value.shell,\n\t\t\t\t\tcommandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command],\n\t\t\t\t\t{\n\t\t\t\t\t\tcwd,\n\t\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\t\tenv: getShellEnv(this.shellEnv, options?.env, options?.inheritEnv),\n\t\t\t\t\t\tstdio: [commandFromStdin ? \"pipe\" : \"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\t\t\twindowsHide: true,\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tif (child.pid) this.activeChildPids.add(child.pid);\n\t\t\t\tif (commandFromStdin) {\n\t\t\t\t\tchild.stdin?.on(\"error\", () => {});\n\t\t\t\t\tchild.stdin?.end(command);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst cause = toError(error);\n\t\t\t\tsettle(err(new ExecutionError(\"spawn_error\", cause.message, cause)));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttimeoutId =\n\t\t\t\ttimeoutMs === undefined\n\t\t\t\t\t? undefined\n\t\t\t\t\t: setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tonAbort();\n\t\t\t\t\t\t}, timeoutMs);\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) onAbort();\n\t\t\t\telse signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\tconst feed = (chunk: Uint8Array) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst wasTruncated = capture.truncated;\n\t\t\t\t\tcapture.push(chunk);\n\t\t\t\t\tif (!options?.capture?.spill || chunk.length === 0) return;\n\t\t\t\t\tif (spillPath !== undefined || wasTruncated) {\n\t\t\t\t\t\tstartSpill(chunk);\n\t\t\t\t\t} else if (capture.truncated) {\n\t\t\t\t\t\tfor (const prefix of spillPrefix) startSpill(prefix);\n\t\t\t\t\t\tspillPrefix.length = 0;\n\t\t\t\t\t\tstartSpill(chunk);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tspillPrefix.push(chunk);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailCallback(error);\n\t\t\t\t}\n\t\t\t};\n\t\t\tchild.stdout?.on(\"data\", feed);\n\t\t\tchild.stderr?.on(\"data\", feed);\n\n\t\t\tvoid waitForChildProcess(\n\t\t\t\tchild,\n\t\t\t\t() =>\n\t\t\t\t\tspillError === undefined &&\n\t\t\t\t\tspillStart !== undefined &&\n\t\t\t\t\t(spillStream === undefined || spillBackpressured),\n\t\t\t).then(\n\t\t\t\tasync ({ code, signal: exitSignal }) => {\n\t\t\t\t\tawait finishSpill();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tcapture.finish();\n\t\t\t\t\t\tcapture.flush();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tfailCallback(error);\n\t\t\t\t\t}\n\t\t\t\t\tif (callbackError) {\n\t\t\t\t\t\tsettle(err(callbackError));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\tsettle(err(new ExecutionError(\"timeout\", `timeout:${options?.timeout}`)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\tsettle(err(new ExecutionError(\"aborted\", \"aborted\")));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (spillError) {\n\t\t\t\t\t\tsettle(err(spillError));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst output = capture.snapshot();\n\t\t\t\t\t// A process killed by a signal (e.g. OOM killer) has no exit code; map it\n\t\t\t\t\t// to the conventional 128 + signal number so callers do not mistake it\n\t\t\t\t\t// for a successful exit.\n\t\t\t\t\tconst exitCode = code ?? (exitSignal ? 128 + (osConstants.signals[exitSignal] ?? 0) : 1);\n\t\t\t\t\tsettle(\n\t\t\t\t\t\tok({\n\t\t\t\t\t\t\texitCode,\n\t\t\t\t\t\t\ttruncation: output.truncation,\n\t\t\t\t\t\t\t...(output.spillPath === undefined ? {} : { spillPath: output.spillPath }),\n\t\t\t\t\t\t\t...(output.lastLineBytes === undefined ? {} : { lastLineBytes: output.lastLineBytes }),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\t(error: Error) => settle(err(new ExecutionError(\"spawn_error\", error.message, error))),\n\t\t\t);\n\t\t});\n\t}\n\n\tasync readTextFile(path: string, context: Context): Promise<Result<string, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<string>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\treturn ok(await readFile(resolved, { encoding: \"utf8\", signal }));\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync readTextLines(\n\t\tpath: string,\n\t\toptions: { maxLines?: number } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string[], FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<string[]>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\tif (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n\t\tlet stream: ReturnType<typeof createReadStream> | undefined;\n\t\tlet lineReader: ReturnType<typeof createInterface> | undefined;\n\t\ttry {\n\t\t\tstream = createReadStream(resolved, { encoding: \"utf8\", signal });\n\t\t\tlineReader = createInterface({ input: stream, crlfDelay: Infinity });\n\t\t\tconst lines: string[] = [];\n\t\t\tfor await (const line of lineReader) {\n\t\t\t\tconst loopAbort = abortResult<string[]>(signal, resolved);\n\t\t\t\tif (loopAbort) return loopAbort;\n\t\t\t\tlines.push(line);\n\t\t\t\tif (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n\t\t\t}\n\t\t\tconst afterReadAbort = abortResult<string[]>(signal, resolved);\n\t\t\tif (afterReadAbort) return afterReadAbort;\n\t\t\treturn ok(lines);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t} finally {\n\t\t\tlineReader?.close();\n\t\t\tstream?.destroy();\n\t\t}\n\t}\n\n\tasync readBinaryFile(path: string, context: Context): Promise<Result<Uint8Array, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<Uint8Array>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\treturn ok(await readFile(resolved, { signal }));\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync writeFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<void>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tawait mkdir(resolve(resolved, \"..\"), { recursive: true });\n\t\t\tconst afterMkdirAbort = abortResult<void>(signal, resolved);\n\t\t\tif (afterMkdirAbort) return afterMkdirAbort;\n\t\t\tawait writeFile(resolved, content, { signal });\n\t\t\treturn ok(undefined);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync appendFile(path: string, content: string | Uint8Array, context: Context): Promise<Result<void, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<void>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tawait mkdir(resolve(resolved, \"..\"), { recursive: true });\n\t\t\tconst afterMkdirAbort = abortResult<void>(signal, resolved);\n\t\t\tif (afterMkdirAbort) return afterMkdirAbort;\n\t\t\tawait appendFile(resolved, content);\n\t\t\tconst afterAppendAbort = abortResult<void>(signal, resolved);\n\t\t\treturn afterAppendAbort ?? ok(undefined);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync renameFile(sourcePath: string, destinationPath: string, context: Context): Promise<Result<void, FileError>> {\n\t\tconst source = resolvePath(this.cwd, sourcePath);\n\t\tconst destination = resolvePath(this.cwd, destinationPath);\n\t\tconst aborted = abortResult<void>(context.abortSignal, destination);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tawait rename(source, destination);\n\t\t\treturn ok(undefined);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, source));\n\t\t}\n\t}\n\n\tasync fileInfo(path: string, context: Context): Promise<Result<FileInfo, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst aborted = abortResult<FileInfo>(context.abortSignal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\treturn fileInfoFromStats(resolved, await lstat(resolved));\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync listDir(path: string, context: Context): Promise<Result<FileInfo[], FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst signal = context.abortSignal;\n\t\tconst aborted = abortResult<FileInfo[]>(signal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tconst entries = await readdir(resolved, { withFileTypes: true });\n\t\t\tconst infos: FileInfo[] = [];\n\t\t\tfor (const entry of entries) {\n\t\t\t\tconst loopAbort = abortResult<FileInfo[]>(signal, resolved);\n\t\t\t\tif (loopAbort) return loopAbort;\n\t\t\t\tconst entryPath = resolve(resolved, entry.name);\n\t\t\t\ttry {\n\t\t\t\t\tconst info = fileInfoFromStats(entryPath, await lstat(entryPath));\n\t\t\t\t\tif (info.ok) infos.push(info.value);\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn err(toFileError(error, entryPath));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ok(infos);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync canonicalPath(path: string, context: Context): Promise<Result<string, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst aborted = abortResult<string>(context.abortSignal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\treturn ok(await realpath(resolved));\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync exists(path: string, context: Context): Promise<Result<boolean, FileError>> {\n\t\tconst result = await this.fileInfo(path, context);\n\t\tif (result.ok) return ok(true);\n\t\tif (result.error.code === \"not_found\") return ok(false);\n\t\treturn err(result.error);\n\t}\n\n\tasync createDir(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst aborted = abortResult<void>(context.abortSignal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tawait mkdir(resolved, { recursive: options?.recursive ?? true });\n\t\t\treturn ok(undefined);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync remove(\n\t\tpath: string,\n\t\toptions: { recursive?: boolean; force?: boolean } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<void, FileError>> {\n\t\tconst resolved = resolvePath(this.cwd, path);\n\t\tconst aborted = abortResult<void>(context.abortSignal, resolved);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tawait rm(resolved, { recursive: options?.recursive ?? false, force: options?.force ?? false });\n\t\t\treturn ok(undefined);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, resolved));\n\t\t}\n\t}\n\n\tasync createTempDir(prefix: string | undefined, context: Context): Promise<Result<string, FileError>> {\n\t\tconst aborted = abortResult<string>(context.abortSignal);\n\t\tif (aborted) return aborted;\n\t\ttry {\n\t\t\tprefix ??= \"tmp-\";\n\t\t\treturn ok(await mkdtemp(join(tmpdir(), prefix)));\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error));\n\t\t}\n\t}\n\n\tasync createTempFile(\n\t\toptions: { prefix?: string; suffix?: string } | undefined,\n\t\tcontext: Context,\n\t): Promise<Result<string, FileError>> {\n\t\tconst dir = await this.createTempDir(\"tmp-\", context);\n\t\tif (!dir.ok) return dir;\n\t\tconst filePath = join(dir.value, `${options?.prefix ?? \"\"}${randomUUID()}${options?.suffix ?? \"\"}`);\n\t\ttry {\n\t\t\tawait writeFile(filePath, \"\");\n\t\t\treturn ok(filePath);\n\t\t} catch (error) {\n\t\t\treturn err(toFileError(error, filePath));\n\t\t}\n\t}\n\n\tasync cleanup(_context: Context): Promise<void> {\n\t\tfor (const pid of this.activeChildPids) killProcessTree(pid);\n\t\tthis.activeChildPids.clear();\n\t}\n}\n"]}