import { closeSync, createReadStream, existsSync, openSync, readSync, statSync } from "node:fs";
import { appendFile, readFile, writeFile } from "node:fs/promises";
import { execFileSync, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { BestAgentError } from "@best-agent/foundation";
import { writeShellDebugLog } from "../core/debug-log.js";
const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
const EXIT_WITHOUT_CLOSE_GRACE_MS = 250;
const FORCE_KILL_GRACE_MS = 2_000;
const PROCESS_SESSION_BUFFER_BYTES = 1024 * 1024;
export class LocalSandbox {
    #processSessions = new Map();
    async close() {
        for (const id of Array.from(this.#processSessions.keys())) {
            await this.stopProcessSession(id);
        }
    }
    exists(path) {
        return existsSync(path);
    }
    isDirectory(path) {
        try {
            return statSync(path).isDirectory();
        }
        catch {
            return false;
        }
    }
    async readTextFile(path) {
        return readFile(path, "utf8");
    }
    /**
     * Sample the first bytes of a file to detect binary content (NUL bytes).
     * Returns true for likely-binary files that should not be read as text.
     */
    looksLikeBinary(path) {
        const fd = openSync(path, "r");
        try {
            const sample = Buffer.alloc(8192);
            const bytesRead = readSync(fd, sample, 0, 8192, 0);
            return sample.subarray(0, bytesRead).includes(0);
        }
        finally {
            closeSync(fd);
        }
    }
    statFile(path) {
        return {
            bytes: statSync(path).size,
        };
    }
    async inspectTextFile(path, options) {
        const stream = createReadStream(path, { encoding: "utf8" });
        const headLines = [];
        const tailLines = [];
        let totalLines = 0;
        const maxChars = options.maxCharsPerLine ?? Number.POSITIVE_INFINITY;
        // Use raw chunk streaming instead of readline to avoid readline buffering
        // entire lines in memory. For compressed/minified files with lines of 100K+
        // characters, readline would allocate a huge string per line even though we
        // only need the first maxChars characters.
        let currentContent = "";
        let currentCapped = false;
        const finishLine = () => {
            totalLines += 1;
            const trimmed = currentContent.endsWith("\r") ? currentContent.slice(0, -1) : currentContent;
            if (headLines.length < options.headLines) {
                headLines.push(trimmed);
            }
            if (options.tailLines > 0) {
                tailLines.push(trimmed);
                if (tailLines.length > options.tailLines) {
                    tailLines.shift();
                }
            }
            currentContent = "";
            currentCapped = false;
        };
        // When only head lines are requested (tailLines === 0), we can stop
        // streaming as soon as all head lines are collected. This avoids
        // iterating through the entire file just to count total lines.
        const canEarlyExit = options.tailLines === 0;
        let didEarlyExit = false;
        try {
            for await (const chunk of stream) {
                for (const character of chunk) {
                    if (character === "\n") {
                        finishLine();
                        if (canEarlyExit && headLines.length >= options.headLines) {
                            didEarlyExit = true;
                            stream.destroy();
                            break;
                        }
                        continue;
                    }
                    if (!currentCapped) {
                        if (currentContent.length < maxChars) {
                            currentContent += character;
                        }
                        else {
                            currentCapped = true;
                        }
                    }
                }
                if (didEarlyExit) {
                    break;
                }
            }
            // Handle final line without trailing newline
            if (!didEarlyExit && currentContent.length > 0) {
                finishLine();
            }
        }
        finally {
            stream.destroy();
        }
        return {
            bytes: statSync(path).size,
            headLines,
            tailLines,
            totalLines,
        };
    }
    async readTextFileRange(path, startLine, endLine, options = {}) {
        const stream = createReadStream(path, { encoding: "utf8" });
        const maxCharsPerLine = options.maxCharsPerLine ?? Number.POSITIVE_INFINITY;
        const selected = [];
        let currentLine = 1;
        let lastIncludedLine = startLine - 1;
        let currentContent = "";
        let currentTruncated = false;
        let sawAnyInput = false;
        let hasCharsSinceLastNewline = false;
        const appendCharacter = (value) => {
            if (currentLine < startLine || currentLine > endLine) {
                return;
            }
            if (currentContent.length < maxCharsPerLine) {
                currentContent += value;
                return;
            }
            currentTruncated = true;
        };
        const finishLine = () => {
            if (currentLine >= startLine && currentLine <= endLine) {
                const content = currentContent.endsWith("\r")
                    ? currentContent.slice(0, -1)
                    : currentContent;
                selected.push({
                    content,
                    truncated: currentTruncated,
                });
                lastIncludedLine = currentLine;
            }
            currentLine += 1;
            currentContent = "";
            currentTruncated = false;
        };
        try {
            for await (const chunk of stream) {
                sawAnyInput = true;
                for (const character of chunk) {
                    if (character === "\n") {
                        finishLine();
                        hasCharsSinceLastNewline = false;
                        if (currentLine > endLine) {
                            stream.destroy();
                            break;
                        }
                        continue;
                    }
                    hasCharsSinceLastNewline = true;
                    appendCharacter(character);
                }
                if (currentLine > endLine) {
                    break;
                }
            }
            if (sawAnyInput && hasCharsSinceLastNewline && currentLine <= endLine) {
                finishLine();
            }
        }
        finally {
            stream.destroy();
        }
        return {
            endLine: lastIncludedLine,
            lines: selected,
        };
    }
    async writeTextFile(path, content) {
        await writeFile(path, content, "utf8");
    }
    async appendTextFile(path, content) {
        await appendFile(path, content, "utf8");
    }
    async runCommand(argv, cwd, options = {}) {
        const result = await this.runCommandCapture(argv, cwd, options);
        if (result.timedOut) {
            throw new BestAgentError("EXEC_FAILED", `Command timed out after ${resolveTimeoutMs(options)}ms.`);
        }
        if (result.exitCode === 0) {
            return result.stdout.trimEnd();
        }
        const detail = result.stderr.trim() || result.stdout.trim() || `Command exited with code ${result.exitCode}`;
        throw new BestAgentError("EXEC_FAILED", detail);
    }
    async runMutableCommand(argv, cwd, options = {}) {
        const result = await this.runCapturedCommandResult(argv, cwd, {
            disableSandbox: true,
            denyNetwork: true,
            denyWrites: false,
            stdio: ["ignore", "pipe", "pipe"],
        }, options);
        if (result.timedOut) {
            throw new BestAgentError("EXEC_FAILED", `Command timed out after ${resolveTimeoutMs(options)}ms.`);
        }
        if (result.exitCode === 0) {
            return result.stdout.trimEnd() || result.stderr.trimEnd();
        }
        const detail = result.stderr.trim() || result.stdout.trim() || `Command exited with code ${result.exitCode}`;
        throw new BestAgentError("EXEC_FAILED", detail);
    }
    async runCommandCapture(argv, cwd, options = {}) {
        return this.runCapturedCommandResult(argv, cwd, {
            ...spawnSandboxOptions(options.sandboxProfile ?? "no-network-no-write"),
            stdio: ["ignore", "pipe", "pipe"],
        }, options);
    }
    runCapturedCommandResult(argv, cwd, spawnOptions, options) {
        if (argv.length === 0) {
            throw new BestAgentError("EXEC_INVALID", "Command execution requires at least one argv entry.");
        }
        if (options.signal?.aborted) {
            return Promise.reject(createAbortError("Command aborted before it started."));
        }
        return new Promise((resolve, reject) => {
            const commandId = randomUUID().slice(0, 8);
            const startedAt = Date.now();
            const timeoutMs = resolveTimeoutMs(options);
            writeShellDebugLog(cwd, "sandbox.command.start", {
                argv: previewArgv(argv),
                commandId,
                cwd,
                timeoutMs,
            });
            let child;
            try {
                child = this.spawnCommand(argv, cwd, { ...spawnOptions, debugId: commandId });
            }
            catch (error) {
                writeShellDebugLog(cwd, "sandbox.command.spawn_throw", {
                    commandId,
                    error: describeErrorForDebugLog(error),
                    elapsedMs: Date.now() - startedAt,
                });
                reject(error);
                return;
            }
            if (!child.stdout || !child.stderr) {
                writeShellDebugLog(cwd, "sandbox.command.missing_pipes", {
                    commandId,
                    elapsedMs: Date.now() - startedAt,
                    pid: child.pid,
                });
                reject(new BestAgentError("EXEC_FAILED", "Command pipes were not available."));
                return;
            }
            let stdout = "";
            let stderr = "";
            let settled = false;
            let timedOut = false;
            let aborted = false;
            let exitCode = null;
            let signalCode = null;
            let stdoutChunks = 0;
            let stderrChunks = 0;
            let lastStdoutLogAt = 0;
            let lastStderrLogAt = 0;
            let exitFallbackTimer;
            let forceKillTimer;
            let heartbeatTimer;
            let processGroupPollTimer;
            let timedOutDescendantPids = [];
            let abortedDescendantPids = [];
            const finish = (reason, callback) => {
                if (settled) {
                    return;
                }
                settled = true;
                clearTimeout(timeoutTimer);
                if (exitFallbackTimer) {
                    clearTimeout(exitFallbackTimer);
                }
                if (processGroupPollTimer) {
                    clearInterval(processGroupPollTimer);
                }
                if (heartbeatTimer) {
                    clearInterval(heartbeatTimer);
                }
                if (forceKillTimer && !timedOut) {
                    clearTimeout(forceKillTimer);
                }
                options.signal?.removeEventListener("abort", abortCommand);
                if (reason === "exit-fallback") {
                    terminateChildProcess(child, timedOut || aborted ? "SIGKILL" : "SIGTERM", [
                        ...timedOutDescendantPids,
                        ...abortedDescendantPids,
                    ]);
                }
                if (timedOut && reason === "close") {
                    terminateChildProcess(child, "SIGKILL", timedOutDescendantPids);
                }
                if (aborted && reason === "close") {
                    terminateChildProcess(child, "SIGKILL", abortedDescendantPids);
                }
                if (reason !== "close") {
                    child.stdout?.destroy();
                    child.stderr?.destroy();
                    child.unref();
                }
                writeShellDebugLog(cwd, "sandbox.command.finish", {
                    commandId,
                    elapsedMs: Date.now() - startedAt,
                    exitCode,
                    reason,
                    signalCode,
                    stderrBytes: Buffer.byteLength(stderr, "utf8"),
                    stdoutBytes: Buffer.byteLength(stdout, "utf8"),
                    aborted,
                    timedOut,
                });
                callback();
            };
            function abortCommand() {
                if (settled) {
                    return;
                }
                aborted = true;
                abortedDescendantPids =
                    process.platform === "win32" || typeof child.pid !== "number"
                        ? []
                        : collectDescendantPids(child.pid);
                writeShellDebugLog(cwd, "sandbox.command.abort", {
                    commandId,
                    descendantPids: abortedDescendantPids,
                    elapsedMs: Date.now() - startedAt,
                    pid: child.pid,
                });
                terminateChildProcess(child, "SIGTERM", abortedDescendantPids);
                forceKillTimer = setTimeout(() => {
                    if (!settled) {
                        writeShellDebugLog(cwd, "sandbox.command.abort_force_kill", {
                            commandId,
                            elapsedMs: Date.now() - startedAt,
                            pid: child.pid,
                        });
                        terminateChildProcess(child, "SIGKILL", abortedDescendantPids);
                    }
                }, FORCE_KILL_GRACE_MS);
                forceKillTimer.unref?.();
            }
            if (options.signal?.aborted) {
                abortCommand();
            }
            else {
                options.signal?.addEventListener("abort", abortCommand, { once: true });
            }
            const timeoutTimer = setTimeout(() => {
                if (settled) {
                    return;
                }
                timedOut = true;
                timedOutDescendantPids =
                    process.platform === "win32" || typeof child.pid !== "number"
                        ? []
                        : collectDescendantPids(child.pid);
                writeShellDebugLog(cwd, "sandbox.command.timeout", {
                    commandId,
                    descendantPids: timedOutDescendantPids,
                    elapsedMs: Date.now() - startedAt,
                    pid: child.pid,
                    timeoutMs,
                });
                terminateChildProcess(child, "SIGTERM", timedOutDescendantPids);
                forceKillTimer = setTimeout(() => {
                    if (!settled) {
                        writeShellDebugLog(cwd, "sandbox.command.force_kill", {
                            commandId,
                            elapsedMs: Date.now() - startedAt,
                            pid: child.pid,
                        });
                        terminateChildProcess(child, "SIGKILL", timedOutDescendantPids);
                    }
                }, FORCE_KILL_GRACE_MS);
                forceKillTimer.unref?.();
            }, timeoutMs);
            timeoutTimer.unref?.();
            child.stdout.on("data", (chunk) => {
                const text = chunk.toString();
                stdout += text;
                stdoutChunks += 1;
                options.onHeartbeat?.();
                options.onOutput?.({ stream: "stdout", text });
                const now = Date.now();
                if (stdoutChunks === 1 || now - lastStdoutLogAt >= 30_000) {
                    lastStdoutLogAt = now;
                    writeShellDebugLog(cwd, "sandbox.command.stdout", {
                        bytes: Buffer.byteLength(stdout, "utf8"),
                        chunks: stdoutChunks,
                        commandId,
                        elapsedMs: now - startedAt,
                    });
                }
            });
            child.stderr.on("data", (chunk) => {
                const text = chunk.toString();
                stderr += text;
                stderrChunks += 1;
                options.onHeartbeat?.();
                options.onOutput?.({ stream: "stderr", text });
                const now = Date.now();
                if (stderrChunks === 1 || now - lastStderrLogAt >= 30_000) {
                    lastStderrLogAt = now;
                    writeShellDebugLog(cwd, "sandbox.command.stderr", {
                        bytes: Buffer.byteLength(stderr, "utf8"),
                        chunks: stderrChunks,
                        commandId,
                        elapsedMs: now - startedAt,
                    });
                }
            });
            child.on("error", (error) => {
                writeShellDebugLog(cwd, "sandbox.command.error", {
                    commandId,
                    elapsedMs: Date.now() - startedAt,
                    error: describeErrorForDebugLog(error),
                });
                finish("error", () => {
                    reject(new BestAgentError("EXEC_FAILED", error.message));
                });
            });
            child.on("exit", (code, signal) => {
                exitCode = code;
                signalCode = signal;
                writeShellDebugLog(cwd, "sandbox.command.exit", {
                    code,
                    commandId,
                    elapsedMs: Date.now() - startedAt,
                    signal,
                });
                exitFallbackTimer = setTimeout(() => {
                    writeShellDebugLog(cwd, "sandbox.command.exit_fallback", {
                        commandId,
                        elapsedMs: Date.now() - startedAt,
                    });
                    finish("exit-fallback", () => {
                        resolve({
                            exitCode,
                            signalCode,
                            stderr,
                            stdout,
                            aborted,
                            timedOut,
                        });
                    });
                }, EXIT_WITHOUT_CLOSE_GRACE_MS);
                exitFallbackTimer.unref?.();
            });
            child.on("close", (code, signal) => {
                exitCode = code;
                signalCode = signal;
                writeShellDebugLog(cwd, "sandbox.command.close", {
                    code,
                    commandId,
                    elapsedMs: Date.now() - startedAt,
                    signal,
                });
                finish("close", () => {
                    resolve({
                        exitCode: code,
                        signalCode: signal,
                        stderr,
                        stdout,
                        aborted,
                        timedOut,
                    });
                });
            });
            if (process.platform !== "win32" && typeof child.pid === "number") {
                processGroupPollTimer = setInterval(() => {
                    if (settled) {
                        return;
                    }
                    try {
                        process.kill(-child.pid, 0);
                    }
                    catch {
                        writeShellDebugLog(cwd, "sandbox.command.process_group_gone", {
                            commandId,
                            elapsedMs: Date.now() - startedAt,
                            pid: child.pid,
                        });
                        finish("process-group-gone", () => {
                            resolve({
                                exitCode: exitCode ?? child.exitCode,
                                signalCode: signalCode ?? child.signalCode,
                                stderr,
                                stdout,
                                aborted,
                                timedOut,
                            });
                        });
                    }
                }, 1_000);
                processGroupPollTimer.unref?.();
            }
            if (options.onHeartbeat) {
                heartbeatTimer = setInterval(() => {
                    if (!settled) {
                        options.onHeartbeat?.();
                    }
                }, 1_000);
                heartbeatTimer.unref?.();
            }
        });
    }
    startProcessSession(argv, cwd, command, options = {}) {
        if (argv.length === 0) {
            throw new BestAgentError("EXEC_INVALID", "Process session requires at least one argv entry.");
        }
        const child = this.spawnCommand(argv, cwd, {
            ...spawnSandboxOptions(options.sandboxProfile ?? "no-network-no-write"),
            stdio: ["pipe", "pipe", "pipe"],
        });
        if (!child.stdin || !child.stdout || !child.stderr) {
            throw new BestAgentError("EXEC_FAILED", "Process session pipes were not available.");
        }
        const session = {
            child,
            command,
            cwd,
            exitCode: null,
            id: `proc_${randomUUID().slice(0, 8)}`,
            revision: 0,
            status: "running",
            stderr: "",
            stdout: "",
            waiters: new Set(),
        };
        this.#processSessions.set(session.id, session);
        child.stdout.on("data", (chunk) => {
            session.stdout = appendBoundedText(session.stdout, chunk.toString());
            notifyProcessSession(session);
        });
        child.stderr.on("data", (chunk) => {
            session.stderr = appendBoundedText(session.stderr, chunk.toString());
            notifyProcessSession(session);
        });
        child.on("error", (error) => {
            session.stderr = appendBoundedText(session.stderr, error.message);
            notifyProcessSession(session);
        });
        child.on("close", (code, signal) => {
            session.status = "completed";
            session.exitCode = code;
            session.signalCode = signal;
            notifyProcessSession(session);
        });
        return snapshotProcessSession(session);
    }
    async readProcessSession(id, options = {}) {
        const session = this.#getProcessSession(id);
        const startRevision = session.revision;
        const waitMs = options.waitMs ?? 0;
        const wakeOnOutput = options.wakeOnOutput ?? true;
        if (options.signal?.aborted) {
            throw createAbortError("Process session read aborted.");
        }
        if (waitMs > 0 && session.status === "running") {
            await new Promise((resolve, reject) => {
                let settled = false;
                let timer;
                const waiter = () => {
                    if (!wakeOnOutput && session.status === "running") {
                        session.waiters.add(waiter);
                        return;
                    }
                    finish();
                };
                const cleanup = () => {
                    session.waiters.delete(waiter);
                    if (timer)
                        clearTimeout(timer);
                    options.signal?.removeEventListener("abort", abortRead);
                };
                const finish = () => {
                    if (settled)
                        return;
                    settled = true;
                    cleanup();
                    resolve();
                };
                const abortRead = () => {
                    if (settled)
                        return;
                    settled = true;
                    cleanup();
                    reject(createAbortError("Process session read aborted."));
                };
                timer = setTimeout(finish, waitMs);
                timer.unref?.();
                if (options.signal?.aborted) {
                    abortRead();
                    return;
                }
                options.signal?.addEventListener("abort", abortRead, { once: true });
                session.waiters.add(waiter);
            });
            if (session.revision === startRevision && session.status === "running") {
                return snapshotProcessSession(session, options.maxBytes);
            }
        }
        return snapshotProcessSession(session, options.maxBytes);
    }
    writeProcessSession(id, input) {
        const session = this.#getProcessSession(id);
        if (session.status !== "running" || !session.child.stdin || session.child.stdin.destroyed) {
            throw new BestAgentError("EXEC_FAILED", `Process session ${id} is not accepting input.`);
        }
        session.child.stdin.write(input);
    }
    async stopProcessSession(id) {
        const session = this.#getProcessSession(id);
        const descendantPids = process.platform === "win32" || typeof session.child.pid !== "number"
            ? []
            : collectDescendantPids(session.child.pid);
        if (session.status === "running") {
            terminateChildProcess(session.child, "SIGTERM", descendantPids);
            await waitForProcessSessionExit(session, FORCE_KILL_GRACE_MS);
            if (session.status === "running") {
                terminateChildProcess(session.child, "SIGKILL", descendantPids);
                await waitForProcessSessionExit(session, FORCE_KILL_GRACE_MS);
            }
        }
        const snapshot = snapshotProcessSession(session);
        this.#processSessions.delete(id);
        return snapshot;
    }
    #getProcessSession(id) {
        const session = this.#processSessions.get(id);
        if (!session) {
            throw new BestAgentError("EXEC_INVALID", `Unknown process session: ${id}`);
        }
        return session;
    }
    spawnCommand(argv, cwd, options = {}) {
        if (argv.length === 0) {
            throw new BestAgentError("EXEC_INVALID", "Command execution requires at least one argv entry.");
        }
        const effectiveArgv = createSandboxWrappedArgv(argv, options);
        const [command, ...args] = effectiveArgv;
        writeShellDebugLog(cwd, "sandbox.spawn.start", {
            argv: previewArgv(argv),
            commandId: options.debugId,
            cwd,
            effectiveArgv: effectiveArgv.length === argv.length &&
                effectiveArgv.every((arg, index) => arg === argv[index])
                ? undefined
                : previewArgv(effectiveArgv),
            stdio: options.stdio ?? ["pipe", "pipe", "pipe"],
        });
        try {
            const child = spawn(command, args, {
                cwd,
                detached: process.platform !== "win32",
                stdio: options.stdio ?? ["pipe", "pipe", "pipe"],
            });
            writeShellDebugLog(cwd, "sandbox.spawn.done", {
                commandId: options.debugId,
                pid: child.pid,
            });
            return child;
        }
        catch (error) {
            writeShellDebugLog(cwd, "sandbox.spawn.error", {
                commandId: options.debugId,
                error: describeErrorForDebugLog(error),
            });
            throw error;
        }
    }
}
function previewArgv(argv) {
    return argv.slice(0, 12).map((arg) => previewDebugText(arg, 240));
}
function previewDebugText(value, maxLength) {
    return value.length <= maxLength ? value : `${value.slice(0, maxLength)}...[truncated]`;
}
function describeErrorForDebugLog(error) {
    if (error instanceof Error) {
        return {
            message: error.message,
            name: error.name,
        };
    }
    return { message: String(error) };
}
function appendBoundedText(current, chunk) {
    const next = current + chunk;
    if (Buffer.byteLength(next, "utf8") <= PROCESS_SESSION_BUFFER_BYTES) {
        return next;
    }
    return next.slice(Math.max(0, next.length - PROCESS_SESSION_BUFFER_BYTES));
}
function notifyProcessSession(session) {
    session.revision += 1;
    const waiters = Array.from(session.waiters);
    session.waiters.clear();
    for (const waiter of waiters) {
        waiter();
    }
}
function snapshotProcessSession(session, maxBytes = 12 * 1024) {
    return {
        command: session.command,
        cwd: session.cwd,
        exitCode: session.exitCode,
        id: session.id,
        signalCode: session.signalCode,
        status: session.status,
        stderr: tailTextByBytes(session.stderr, maxBytes),
        stdout: tailTextByBytes(session.stdout, maxBytes),
    };
}
async function waitForProcessSessionExit(session, timeoutMs) {
    const startedAt = Date.now();
    while (session.status === "running" && Date.now() - startedAt < timeoutMs) {
        await new Promise((resolve) => setTimeout(resolve, 50));
    }
}
function tailTextByBytes(value, maxBytes) {
    if (Buffer.byteLength(value, "utf8") <= maxBytes) {
        return value;
    }
    return value.slice(Math.max(0, value.length - maxBytes));
}
function resolveTimeoutMs(options) {
    return typeof options.timeoutMs === "number" &&
        Number.isFinite(options.timeoutMs) &&
        options.timeoutMs > 0
        ? options.timeoutMs
        : DEFAULT_COMMAND_TIMEOUT_MS;
}
function collectDescendantPids(rootPid) {
    try {
        const output = execFileSync("ps", ["-axo", "pid=,ppid="], {
            encoding: "utf8",
            timeout: 1_000,
        });
        const childrenByParent = new Map();
        for (const line of output.split("\n")) {
            const [pidText, ppidText] = line.trim().split(/\s+/);
            const pid = Number(pidText);
            const ppid = Number(ppidText);
            if (!Number.isInteger(pid) || !Number.isInteger(ppid)) {
                continue;
            }
            const children = childrenByParent.get(ppid) ?? [];
            children.push(pid);
            childrenByParent.set(ppid, children);
        }
        const descendants = [];
        const stack = [...(childrenByParent.get(rootPid) ?? [])];
        while (stack.length > 0) {
            const pid = stack.pop();
            descendants.push(pid);
            stack.push(...(childrenByParent.get(pid) ?? []));
        }
        return descendants;
    }
    catch {
        return [];
    }
}
function terminateChildProcess(child, signal, descendantPids = []) {
    if (process.platform !== "win32" && typeof child.pid === "number") {
        try {
            process.kill(-child.pid, signal);
        }
        catch {
            // Fall back to the direct child if process-group termination is unavailable.
        }
        for (const pid of descendantPids) {
            if (pid === child.pid) {
                continue;
            }
            try {
                process.kill(-pid, signal);
            }
            catch {
                try {
                    process.kill(pid, signal);
                }
                catch {
                    // Ignore kill failures for descendants that already exited.
                }
            }
        }
        return;
    }
    if (process.platform === "win32" && typeof child.pid === "number") {
        try {
            execFileSync("taskkill", ["/pid", String(child.pid), "/t", ...(signal === "SIGKILL" ? ["/f"] : [])], { stdio: "ignore", timeout: 1_000 });
            return;
        }
        catch {
            // Fall back to the direct child if Windows process-tree termination is unavailable.
        }
    }
    if (child.exitCode !== null || child.signalCode !== null) {
        return;
    }
    try {
        child.kill(signal);
    }
    catch {
        // Ignore kill failures for already-exiting children.
    }
}
function createAbortError(message) {
    const error = new Error(message);
    error.name = "AbortError";
    return error;
}
function createSandboxWrappedArgv(argv, options) {
    if (options.disableSandbox) {
        return argv;
    }
    if (!existsSync("/usr/bin/sandbox-exec")) {
        return argv;
    }
    const profile = buildSandboxProfile(options);
    if (!profile) {
        return argv;
    }
    return ["/usr/bin/sandbox-exec", "-p", profile, ...argv];
}
function buildSandboxProfile(options) {
    const denyRules = [];
    if (options.denyNetwork) {
        denyRules.push("(deny network*)");
    }
    if (options.denyWrites) {
        denyRules.push('(deny file-write* (require-not (literal "/dev/null")))');
    }
    if (denyRules.length === 0) {
        return undefined;
    }
    return ["(version 1)", "(allow default)", ...denyRules].join(" ");
}
function spawnSandboxOptions(profile) {
    return {
        denyNetwork: profile === "no-network" || profile === "no-network-no-write",
        denyWrites: profile === "no-write" || profile === "no-network-no-write",
    };
}
//# sourceMappingURL=local-sandbox.js.map