/**
 * GitWorktreeSandboxStrategy — sandbox using `git worktree` for repo-based tasks.
 *
 * Creates a git worktree at a specific ref, providing a deterministic
 * starting state for tasks that modify a git repository.
 *
 * All git CLI calls use `execFileSync` (array form, no shell) to prevent
 * injection from task-supplied values like git refs or repo paths.
 *
 * @see docs/archive/exec-plans/architecture-overhaul/phase-4-agent-harness.md
 */
import { randomUUID } from "crypto";
import { execFileSync } from "child_process";
import { existsSync, rmSync } from "fs";
import { tmpdir } from "os";
import { resolve } from "path";
/** Validate a git ref contains no shell metacharacters or path traversal */
function validateGitRef(ref) {
    // Git refs: alphanumeric, -, _, /, ., ~, ^ — no spaces, no shell metacharacters
    if (!/^[a-zA-Z0-9._\-/~^]+$/.test(ref)) {
        throw new Error(`Invalid git ref: "${ref}" — must contain only alphanumeric, -, _, /, ., ~, ^ characters`);
    }
    // Disallow path traversal via ".." segments
    if (ref.includes("..")) {
        throw new Error(`Invalid git ref: "${ref}" — must not contain ".." (path traversal)`);
    }
}
export class GitWorktreeSandboxStrategy {
    name = "Git Worktree";
    type = "git-worktree";
    async isAvailable() {
        try {
            execFileSync("git", ["--version"], { stdio: "ignore", timeout: 5000 });
            return true;
        }
        catch {
            return false;
        }
    }
    async provision(options) {
        const repoPath = options.repoPath;
        if (!repoPath) {
            throw new Error("GitWorktreeSandboxStrategy requires repoPath — " +
                "the path to the git repository to create a worktree from");
        }
        const ref = options.gitRef ?? "HEAD";
        validateGitRef(ref);
        const id = `ailf-worktree-${randomUUID().slice(0, 8)}`;
        const workingDir = resolve(tmpdir(), id);
        try {
            // Array form — no shell, prevents injection via repoPath/workingDir/ref
            execFileSync("git", ["-C", repoPath, "worktree", "add", workingDir, ref], {
                encoding: "utf-8",
                timeout: 30_000,
            });
            return {
                id,
                workingDir,
                strategy: "git-worktree",
                gitRef: ref,
                createdAt: new Date().toISOString(),
            };
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            throw new Error(`Failed to create git worktree at "${ref}": ${msg}`, {
                cause: err,
            });
        }
    }
    async collectArtifacts(sandbox) {
        const modifiedFiles = [];
        let diff;
        if (existsSync(sandbox.workingDir)) {
            try {
                diff = execFileSync("git", ["-C", sandbox.workingDir, "diff"], {
                    encoding: "utf-8",
                    timeout: 10_000,
                }).trim();
                const statusOutput = execFileSync("git", ["-C", sandbox.workingDir, "status", "--porcelain"], { encoding: "utf-8", timeout: 10_000 }).trim();
                if (statusOutput) {
                    for (const line of statusOutput.split("\n")) {
                        const file = line.slice(3).trim();
                        if (file)
                            modifiedFiles.push(file);
                    }
                }
            }
            catch {
                // Best-effort artifact collection
            }
        }
        return {
            modifiedFiles,
            diff: diff || undefined,
            durationMs: Date.now() - new Date(sandbox.createdAt).getTime(),
        };
    }
    async teardown(sandbox) {
        if (existsSync(sandbox.workingDir)) {
            try {
                execFileSync("git", ["worktree", "remove", sandbox.workingDir, "--force"], { stdio: "ignore", timeout: 10_000 });
            }
            catch {
                // If worktree remove fails, fall back to manual cleanup.
                // Guard: only delete under tmpdir to prevent accidental deletion.
                const tmp = resolve(tmpdir());
                if (resolve(sandbox.workingDir).startsWith(tmp)) {
                    rmSync(sandbox.workingDir, { recursive: true, force: true });
                }
            }
        }
    }
}
