import { z } from "zod";
import { killProcessTree } from "../utils/process.js";
import { truncateTail } from "./truncate.js";
import { compressToolOutput } from "./compress.js";
import { writeOverflow } from "./overflow.js";
import { localOperations } from "./operations.js";
import { getSafeToolEnv } from "./safe-env.js";
import { resolveShell } from "../core/shell.js";
import { isReadOnlyCommand } from "./read-only-bash.js";
import { getActiveGoalMode, goalModeBashRestriction, isPlanModeActive, planModeRestriction, } from "../core/runtime-mode.js";
const DEFAULT_TIMEOUT = 120_000; // 120 seconds
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB — cap buffered output to prevent OOM
function isPlanModeRef(value) {
    return (typeof value === "object" &&
        value !== null &&
        typeof value.current === "boolean");
}
function isGoalModeRef(value) {
    return (typeof value === "object" &&
        value !== null &&
        typeof value.current === "string");
}
const BashParams = z.object({
    command: z.string().describe("The bash command to execute"),
    timeout: z
        .number()
        .int()
        .min(1000)
        .optional()
        .describe("Timeout in milliseconds (default: 120000)"),
    run_in_background: z
        .boolean()
        .optional()
        .describe("Run the command in the background. Returns a process ID immediately. " +
        "Use task_output to read output and task_stop to stop it."),
});
export function createBashTool(cwd, processManager, ops = localOperations, planModeRefArg, goalModeRefArg) {
    const planModeRef = isPlanModeRef(planModeRefArg) ? planModeRefArg : undefined;
    const goalModeRef = isGoalModeRef(planModeRefArg) ? planModeRefArg : goalModeRefArg;
    return {
        name: "bash",
        description: "Execute a bash command. The shell's working directory is already set to the project root — " +
            "don't cd into it redundantly. Use cd only when you need a different directory. " +
            "Returns exit code and combined stdout/stderr. " +
            "Commands run in a non-interactive bash shell with TERM=dumb. " +
            "Long output is truncated (tail kept). " +
            "Set run_in_background=true for long-running OR interactive processes " +
            "(dev servers, watchers, REPLs, scaffolders, programs that prompt for input). " +
            "Use task_output to read output, task_send to type input/answer prompts, and " +
            "task_stop to stop background processes.",
        parameters: BashParams,
        executionMode: "sequential",
        async execute({ command, timeout: timeoutMs, run_in_background }, context) {
            const goalModeRestriction = goalModeBashRestriction(getActiveGoalMode(goalModeRef), run_in_background === true);
            if (goalModeRestriction) {
                return goalModeRestriction;
            }
            if (isPlanModeActive(planModeRef) && !isReadOnlyCommand(command)) {
                return planModeRestriction("bash");
            }
            if (run_in_background) {
                const result = await processManager.start(command, cwd);
                return (`Background process started.\n` +
                    `ID: ${result.id}\n` +
                    `PID: ${result.pid}\n` +
                    `Log: ${result.logFile}\n` +
                    `Use task_output with id="${result.id}" to read output, ` +
                    `task_send to type input/answer prompts, task_stop to stop it.`);
            }
            const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT;
            return new Promise((resolve) => {
                // Cross-platform shell: bash on macOS/Linux, Git Bash on Windows (or
                // cmd.exe fallback). Hardcoding "bash" broke on Windows with `spawn
                // bash ENOENT`, and accidentally hitting WSL's bash ran commands in a
                // separate Linux filesystem (the "files not mounted" symptom).
                const shell = resolveShell(command);
                const child = ops.spawn(shell.file, shell.args, {
                    cwd,
                    detached: true,
                    stdio: ["ignore", "pipe", "pipe"],
                    env: getSafeToolEnv(),
                });
                const chunks = [];
                let totalBytes = 0;
                let outputCapped = false;
                const onData = (data) => {
                    if (outputCapped)
                        return;
                    totalBytes += data.length;
                    if (totalBytes > MAX_OUTPUT_BYTES) {
                        outputCapped = true;
                        return;
                    }
                    chunks.push(data);
                    // Stream progress to UI for live output display
                    if (context.onUpdate) {
                        context.onUpdate({
                            type: "bash_progress",
                            output: data.toString("utf-8"),
                            totalBytes,
                        });
                    }
                };
                child.stdout?.on("data", onData);
                child.stderr?.on("data", onData);
                let killed = false;
                let timedOut = false;
                // Timeout handling
                const timer = setTimeout(() => {
                    timedOut = true;
                    killed = true;
                    if (child.pid)
                        killProcessTree(child.pid);
                }, effectiveTimeout);
                // Abort signal handling
                const onAbort = () => {
                    killed = true;
                    if (child.pid)
                        killProcessTree(child.pid);
                };
                context.signal.addEventListener("abort", onAbort, { once: true });
                child.on("close", async (code) => {
                    clearTimeout(timer);
                    context.signal.removeEventListener("abort", onAbort);
                    const rawOutput = Buffer.concat(chunks).toString("utf-8");
                    const result = truncateTail(rawOutput);
                    let output = result.content;
                    const capNotice = outputCapped
                        ? `[Output capped at ${MAX_OUTPUT_BYTES / 1024 / 1024} MB to prevent memory exhaustion]\n`
                        : "";
                    if (outputCapped) {
                        output = `${capNotice}${output}`;
                    }
                    if (result.truncated) {
                        // Over-limit: a blind tail slice would drop the head and any
                        // mid-stream error. Compress instead (keeps errors + head/tail,
                        // collapses repeats); the overflow file preserves the original.
                        const overflowPath = await writeOverflow(rawOutput, "bash").catch(() => null);
                        const overflowNotice = overflowPath ? ` Full output: ${overflowPath}` : "";
                        const c = compressToolOutput(rawOutput);
                        output = `${capNotice}[${c.notice}${overflowNotice}]\n${c.content}`;
                    }
                    // Windows without Git Bash: commands ran under cmd.exe, NOT bash. Tell
                    // the model so it uses cmd syntax (no `ls`/`grep`/pipes/single-quotes)
                    // and doesn't misread failures as a wrong directory / environment.
                    if (shell.isCmdFallback) {
                        output =
                            "[Ran under Windows cmd.exe — bash is unavailable. Use cmd syntax " +
                                "(dir, findstr, type); POSIX commands and quoting will fail. " +
                                "Install Git for Windows to get bash.]\n" +
                                output;
                    }
                    const exitCode = timedOut
                        ? `TIMEOUT (${effectiveTimeout}ms)`
                        : killed
                            ? "KILLED"
                            : String(code ?? 1);
                    resolve(`Exit code: ${exitCode}\n${output}`);
                });
                child.on("error", (err) => {
                    clearTimeout(timer);
                    context.signal.removeEventListener("abort", onAbort);
                    resolve(`Exit code: 1\nFailed to spawn: ${err.message}`);
                });
            });
        },
    };
}
//# sourceMappingURL=bash.js.map