"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const agent_session_runner_1 = require("./agent-session-runner");
const agents_1 = require("./agents");
const app = (0, express_1.default)();
app.use((0, cors_1.default)());
const startTime = new Date();
// Raw body capture for webhook HMAC — must come before express.json()
app.use("/webhook/gitea", express_1.default.raw({ type: "*/*" }));
app.use(express_1.default.json());
const PORT = process.env.PORT || 3333;
// ---------------------------------------------------------------------------
// Build ToolContext from environment variables
// ---------------------------------------------------------------------------
function ensureWorkspace(repo, sessionId) {
    const base = process.env.WORKSPACE_BASE || "/workspaces";
    if (!repo) {
        const dir = path.join(base, "default");
        fs.mkdirSync(dir, { recursive: true });
        return dir;
    }
    const mainRepoDir = path.join(base, repo.replace("/", "_"));
    const gitea = {
        apiUrl: process.env.GITEA_API_URL || "",
        apiToken: process.env.GITEA_API_TOKEN || "",
        username: process.env.GITEA_USERNAME || "",
    };
    // 1. Ensure main repo clone exists
    if (!fs.existsSync(path.join(mainRepoDir, ".git"))) {
        fs.mkdirSync(mainRepoDir, { recursive: true });
        const authedUrl = `${gitea.apiUrl}/${repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`);
        try {
            (0, child_process_1.execSync)(`git clone "${authedUrl}" "${mainRepoDir}"`, { stdio: "pipe" });
        }
        catch {
            // Repo may not exist yet — just init
            (0, child_process_1.execSync)(`git init`, { cwd: mainRepoDir, stdio: "pipe" });
            (0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, {
                cwd: mainRepoDir,
                stdio: "pipe",
            });
        }
    }
    // 2. If no sessionId, fall back to main repo clone directly
    if (!sessionId) {
        return mainRepoDir;
    }
    // 3. Isolated Worktree Directory per task session
    const taskWorktreePath = path.join(base, "tasks", sessionId);
    fs.mkdirSync(path.join(base, "tasks"), { recursive: true });
    // 4. Create isolated worktree if not yet active
    if (!fs.existsSync(path.join(taskWorktreePath, ".git"))) {
        // Clean up any stale directory from previous failed runs before adding worktree
        if (fs.existsSync(taskWorktreePath)) {
            try {
                fs.rmSync(taskWorktreePath, { recursive: true, force: true });
            }
            catch { }
        }
        try {
            console.log(`[worktree] Adding isolated git worktree for session ${sessionId} at ${taskWorktreePath}...`);
            // Check if the branch task-sessionId already exists in the main repository
            let branchExists = false;
            try {
                const branches = (0, child_process_1.execSync)(`git branch --list "task-${sessionId}"`, {
                    cwd: mainRepoDir,
                }).toString();
                branchExists = branches.trim().length > 0;
            }
            catch {
                branchExists = false;
            }
            if (branchExists) {
                // Checkout the existing branch into the new worktree path
                (0, child_process_1.execSync)(`git worktree add -f "${taskWorktreePath}" "task-${sessionId}"`, { cwd: mainRepoDir, stdio: "pipe" });
            }
            else {
                // Create and checkout a new isolated branch
                (0, child_process_1.execSync)(`git worktree add -f -b "task-${sessionId}" "${taskWorktreePath}"`, { cwd: mainRepoDir, stdio: "pipe" });
            }
        }
        catch (e) {
            console.error("[worktree] Failed to add git worktree, falling back to main clone:", e.message || String(e));
            return mainRepoDir;
        }
    }
    // 5. Sync active workspace edits from mainRepoDir (containing Monaco edits) to taskWorktreePath
    if (taskWorktreePath !== mainRepoDir) {
        try {
            console.log(`[worktree] Syncing active workspace edits from ${mainRepoDir} to ${taskWorktreePath}...`);
            // Use rsync to copy active files while preserving structure and deleting files deleted in mainRepoDir
            // Exclude node_modules, .git, .next, .vibncode/settings.json, etc.
            (0, child_process_1.execSync)(`rsync -ar --delete --exclude="node_modules" --exclude=".git" --exclude=".next" --exclude=".vibncode/settings.json" "${mainRepoDir}/" "${taskWorktreePath}/"`, { stdio: "pipe" });
        }
        catch (syncErr) {
            console.warn("[worktree] rsync failed, falling back to cp:", syncErr.message || syncErr);
        }
    }
    return taskWorktreePath;
}
function buildContext(repo, sessionId) {
    const workspaceRoot = ensureWorkspace(repo, sessionId);
    return {
        workspaceRoot,
        gitea: {
            apiUrl: process.env.GITEA_API_URL || "",
            apiToken: process.env.GITEA_API_TOKEN || "",
            username: process.env.GITEA_USERNAME || "",
        },
        coolify: {
            apiUrl: process.env.COOLIFY_API_URL || "",
            apiToken: process.env.COOLIFY_API_TOKEN || "",
        },
        mcpToken: "",
        vibnApiUrl: process.env.VIBN_API_URL ?? "https://vibnai.com",
        memoryUpdates: [],
    };
}
function cleanupWorkspace(repo, sessionId) {
    const base = process.env.WORKSPACE_BASE || "/workspaces";
    const mainRepoDir = path.join(base, repo.replace("/", "_"));
    const taskWorktreePath = path.join(base, "tasks", sessionId);
    if (fs.existsSync(taskWorktreePath)) {
        try {
            console.log(`[worktree] Pruning and removing git worktree for session ${sessionId}...`);
            // 1. Tell git to remove the worktree references
            (0, child_process_1.execSync)(`git worktree remove --force "${taskWorktreePath}"`, {
                cwd: mainRepoDir,
                stdio: "pipe",
            });
            // 2. Delete the temporary branch from the main repository index
            (0, child_process_1.execSync)(`git branch -D "task-${sessionId}"`, {
                cwd: mainRepoDir,
                stdio: "pipe",
            });
            // 3. Force clean directory
            if (fs.existsSync(taskWorktreePath)) {
                fs.rmSync(taskWorktreePath, { recursive: true, force: true });
            }
        }
        catch (e) {
            console.warn(`[worktree] Non-fatal cleanup error for session ${sessionId}:`, e.message || String(e));
        }
    }
}
// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
// Health check
app.get("/health", (_req, res) => {
    res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// ---------------------------------------------------------------------------
// GitHub mirror — clone a public GitHub repo and push to Gitea as-is
// ---------------------------------------------------------------------------
app.post("/api/mirror", async (req, res) => {
    const { github_url, gitea_repo, project_name, github_token } = req.body;
    if (!github_url || !gitea_repo) {
        res
            .status(400)
            .json({ error: '"github_url" and "gitea_repo" are required' });
        return;
    }
    const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
    const fs = await Promise.resolve().then(() => __importStar(require("fs")));
    const path = await Promise.resolve().then(() => __importStar(require("path")));
    const os = await Promise.resolve().then(() => __importStar(require("os")));
    const mirrorId = `mirror_${Date.now()}`;
    const tmpDir = path.join(os.tmpdir(), mirrorId);
    const gitea = {
        apiUrl: process.env.GITEA_API_URL || "",
        apiToken: process.env.GITEA_API_TOKEN || "",
        username: process.env.GITEA_USERNAME || "",
    };
    try {
        // Build authenticated Gitea push URL
        // GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present
        const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, "");
        const authedPushUrl = `${giteaBase}/${gitea_repo}.git`.replace("https://", `https://${gitea.username}:${gitea.apiToken}@`);
        console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`);
        fs.mkdirSync(tmpDir, { recursive: true });
        // Build authenticated clone URL for private repos
        let cloneUrl = github_url;
        if (github_token) {
            cloneUrl = github_url.replace("https://", `https://${github_token}@`);
        }
        // Mirror-clone the GitHub repo (preserves all branches + tags)
        execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
            stdio: "pipe",
            timeout: 120000,
        });
        execSync(`git config --bool core.bare false`, {
            cwd: tmpDir,
            stdio: "pipe",
        });
        execSync(`git checkout`, { cwd: tmpDir, stdio: "pipe" });
        // Point origin at Gitea and push all refs
        execSync(`git remote set-url origin "${authedPushUrl}"`, {
            cwd: tmpDir,
            stdio: "pipe",
        });
        execSync(`git push --mirror origin`, {
            cwd: tmpDir,
            stdio: "pipe",
            timeout: 120000,
        });
        console.log(`[mirror] Pushed ${gitea_repo} successfully`);
        res.json({ success: true, gitea_repo, github_url });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`[mirror] Failed:`, msg);
        res.status(500).json({ error: "Mirror failed", details: msg });
    }
    finally {
        // Clean up temp dir
        try {
            const { execSync: rm } = await Promise.resolve().then(() => __importStar(require("child_process")));
            rm(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
        }
        catch {
            /* best effort */
        }
    }
});
// List available agents
app.get("/api/agents", (_req, res) => {
    const agents = Object.values(agents_1.AGENTS).map((a) => ({
        name: a.name,
        description: a.description,
        tools: a.tools.map((t) => t.name),
    }));
    res.json(agents);
});
const activeSessions = new Map();
app.post("/agent/execute", async (req, res) => {
    const { sessionId, projectId, appName, appPath: rawAppPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, mcpToken, } = req.body;
    const appPath = rawAppPath === undefined || rawAppPath === null || rawAppPath === ""
        ? "."
        : rawAppPath;
    if (!sessionId || !projectId || !appPath || !task) {
        res
            .status(400)
            .json({ error: "sessionId, projectId, appPath and task are required" });
        return;
    }
    const vibnApiUrl = process.env.VIBN_API_URL ?? "https://vibnai.com";
    const patchHeaders = {
        "Content-Type": "application/json",
        ...(process.env.AGENT_RUNNER_SECRET
            ? { "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET }
            : {}),
    };
    // Register session as active
    const sessionState = { stopped: false };
    activeSessions.set(sessionId, sessionState);
    // Respond immediately — execution is async
    res.status(202).json({ sessionId, status: "running" });
    // Build workspace context — clone/update the Gitea repo if provided
    let ctx;
    try {
        ctx = buildContext(giteaRepo, sessionId);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error("[agent/execute] buildContext failed:", msg);
        // Notify VIBN DB of failure
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: "PATCH",
            headers: patchHeaders,
            body: JSON.stringify({ status: "failed", error: msg }),
        }).catch(() => { });
        activeSessions.delete(sessionId);
        return;
    }
    // Capture repo root before scoping to appPath — needed for git commit in auto-approve
    const repoRoot = ctx.workspaceRoot;
    // Wire the ToolContext so its tools can call back into the VIBN frontend MCP
    // with the right URL and auth. buildContext() defaults these to safe values,
    // but the authoritative ones come from env (VIBN_API_URL) and the frontend
    // (mcpToken passed in the /agent/execute body). Without this, tools fetch
    // http://localhost:3000 with no token and fail with "fetch failed".
    ctx.vibnApiUrl = vibnApiUrl;
    ctx.mcpToken = mcpToken ?? ctx.mcpToken;
    ctx.projectId = projectId;
    // Scope workspace to the app subdirectory so the agent works there naturally
    if (appPath) {
        const path = require("path");
        ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
        const fs = require("fs");
        fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
    }
    const agentConfig = agents_1.AGENTS["Coder"];
    if (!agentConfig) {
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: "PATCH",
            headers: patchHeaders,
            body: JSON.stringify({
                status: "failed",
                error: "Coder agent not registered",
            }),
        }).catch(() => { });
        activeSessions.delete(sessionId);
        return;
    }
    // If continuing a previous task, combine into a single prompt so the agent
    // understands what was already attempted.
    const effectiveTask = continueTask
        ? `Original task: ${task}\n\nFollow-up instruction: ${continueTask}`
        : task;
    // Run the streaming agent loop (fire and forget)
    (0, agent_session_runner_1.runSessionAgent)(agentConfig, effectiveTask, ctx, {
        sessionId,
        projectId,
        vibnApiUrl,
        appPath,
        repoRoot,
        isStopped: () => sessionState.stopped,
        autoApprove: autoApprove ?? true,
        giteaRepo,
        coolifyAppUuid,
        coolifyApiUrl: process.env.COOLIFY_API_URL,
        coolifyApiToken: process.env.COOLIFY_API_TOKEN,
    })
        .catch((err) => {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
        fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
            method: "PATCH",
            headers: patchHeaders,
            body: JSON.stringify({ status: "failed", error: msg }),
        }).catch(() => { });
    })
        .finally(() => {
        activeSessions.delete(sessionId);
        if (giteaRepo && sessionId) {
            cleanupWorkspace(giteaRepo, sessionId);
        }
    });
});
app.post("/agent/stop", (req, res) => {
    const { sessionId } = req.body;
    if (!sessionId) {
        res.status(400).json({ error: "sessionId required" });
        return;
    }
    const session = activeSessions.get(sessionId);
    if (session) {
        session.stopped = true;
        res.json({ status: "stopped" });
    }
    else {
        res.status(404).json({ error: "session not found or not running" });
    }
});
app.listen(PORT, () => {
    console.log(`AgentRunner listening on port ${PORT}`);
    console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(", ")}`);
    if (!process.env.GOOGLE_API_KEY) {
        console.warn("WARNING: GOOGLE_API_KEY is not set — agents will fail");
    }
});
