import { spawn } from "node:child_process";
import { isDestructiveCommand, validateCommand, } from "../safety/guardrails.js";
import { expectNonEmptyString, expectOptionalNumber } from "./helpers.js";
const COMMAND_KILL_GRACE_MS = 1_000;
function executeBashCommand(command, cwd, timeoutMs) {
    return new Promise((resolve, reject) => {
        const useProcessGroup = process.platform !== "win32";
        const child = spawn("bash", ["-lc", command], {
            cwd,
            detached: useProcessGroup,
            stdio: ["ignore", "pipe", "pipe"],
        });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        let settled = false;
        // Forward-declared because the `cleanup` closure below reads it
        // before the setTimeout call lower in the function assigns it.
        // eslint-disable-next-line prefer-const -- reassigned in setTimeout call below; declared early so cleanup() closure can reference it
        let timeout;
        let forceKillTimeout;
        let forceResolveTimeout;
        const cleanup = () => {
            if (timeout)
                clearTimeout(timeout);
            if (forceKillTimeout)
                clearTimeout(forceKillTimeout);
            if (forceResolveTimeout)
                clearTimeout(forceResolveTimeout);
        };
        const settle = (result) => {
            if (settled)
                return;
            settled = true;
            cleanup();
            resolve(result);
        };
        const terminate = (signal) => {
            if (child.pid && useProcessGroup) {
                try {
                    process.kill(-child.pid, signal);
                    return;
                }
                catch {
                    // Fall through to killing the shell process directly.
                }
            }
            try {
                child.kill(signal);
            }
            catch {
                // The process may have already exited.
            }
        };
        timeout = setTimeout(() => {
            timedOut = true;
            terminate("SIGTERM");
            forceKillTimeout = setTimeout(() => {
                terminate("SIGKILL");
                forceResolveTimeout = setTimeout(() => {
                    child.stdout.destroy();
                    child.stderr.destroy();
                    settle({
                        stdout: stdout.trimEnd(),
                        stderr: stderr.trimEnd(),
                        exitCode: null,
                        timedOut,
                    });
                }, COMMAND_KILL_GRACE_MS);
            }, COMMAND_KILL_GRACE_MS);
        }, timeoutMs);
        child.stdout.on("data", (chunk) => {
            stdout += chunk.toString();
        });
        child.stderr.on("data", (chunk) => {
            stderr += chunk.toString();
        });
        child.on("error", (error) => {
            cleanup();
            reject(error);
        });
        child.on("close", (code) => {
            settle({
                stdout: stdout.trimEnd(),
                stderr: stderr.trimEnd(),
                exitCode: code,
                timedOut,
            });
        });
    });
}
function parseTimeout(rawTimeout, defaultTimeoutMs) {
    if (rawTimeout === undefined) {
        return defaultTimeoutMs;
    }
    if (!Number.isInteger(rawTimeout) || rawTimeout <= 0) {
        throw new Error(`"timeout" must be a positive integer in milliseconds.`);
    }
    return Math.min(rawTimeout, 10 * 60 * 1000);
}
export function createRunCommandTool(options, hooks) {
    return {
        name: "run_command",
        description: "Run a shell command in the workspace and return stdout/stderr/exit code.",
        inputSchema: {
            type: "object",
            properties: {
                command: {
                    type: "string",
                    description: "Shell command to execute within workspace root.",
                },
                timeout: {
                    type: "number",
                    description: "Optional timeout in milliseconds. Defaults to configured command timeout.",
                },
            },
            required: ["command"],
            additionalProperties: false,
        },
        execute: async (input) => {
            const command = expectNonEmptyString(input, "command");
            const rawTimeout = expectOptionalNumber(input, "timeout");
            const timeoutMs = parseTimeout(rawTimeout, options.commandTimeoutMs);
            validateCommand(command, options.commandDenylist);
            if (options.confirmDestructive && isDestructiveCommand(command)) {
                throw new Error(`Command "${command}" appears destructive and requires explicit user confirmation.`);
            }
            const result = await executeBashCommand(command, options.workspaceRoot, timeoutMs);
            if (hooks?.afterCommand) {
                await hooks.afterCommand(command, result);
            }
            const maxStdoutChars = 8_000;
            const maxStderrChars = 4_000;
            let stdoutOut = result.stdout.length > 0 ? result.stdout : "(empty)";
            if (stdoutOut.length > maxStdoutChars) {
                stdoutOut = `${stdoutOut.slice(0, maxStdoutChars)}\n[... truncated, ${result.stdout.length - maxStdoutChars} more chars ...]`;
            }
            let stderrOut = result.stderr.length > 0 ? result.stderr : "(empty)";
            if (stderrOut.length > maxStderrChars) {
                stderrOut = `${stderrOut.slice(0, maxStderrChars)}\n[... truncated, ${result.stderr.length - maxStderrChars} more chars ...]`;
            }
            return [
                `exit_code: ${result.exitCode ?? "null"}`,
                `timed_out: ${result.timedOut ? "true" : "false"}`,
                "stdout:",
                stdoutOut,
                "stderr:",
                stderrOut,
            ].join("\n");
        },
    };
}
//# sourceMappingURL=run-command.js.map