"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pruneWorktrees = exports.cleanupOrphanedWorktrees = exports.registerFreshWorktreeRoot = exports.WorktreeManager = exports.INSTANCE_PREFIX = exports.FRESH_WORKTREES_DIRNAME = void 0;
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const fs_1 = __importDefault(require("fs"));
const net_1 = __importDefault(require("net"));
const simple_git_1 = require("simple-git");
const child_process_1 = require("child_process");
const logger_1 = require("../utils/logger");
const designCatalog_1 = require("./templates/designCatalog");
const viteReactTs_1 = require("./templates/viteReactTs");
const sourceContext_1 = require("../mcp/agent-variants/sourceContext");
const log = (0, logger_1.createLogger)('WorktreeManager');
// Abort any git child process that goes silent (no stdout/stderr) for this
// long. Provisioning worktrees for a large project (e.g. React Native, whose
// working tree carries native build dirs) used to hang indefinitely because
// none of these git calls had a timeout — a stuck `worktree add`/`commit`
// would stall the whole `start_variants` tool call. `block` resets on every
// data chunk, so a slow-but-progressing checkout keeps running; only a
// genuinely wedged process (lock contention, credential/hook prompt) trips it.
const GIT_BLOCK_TIMEOUT_MS = 120_000;
const GIT_OPTS = { timeout: { block: GIT_BLOCK_TIMEOUT_MS } };
// Existing-project worktrees still go under /tmp — `git worktree add` refuses
// to nest inside the main worktree, so the user's project tree is off-limits
// and /tmp is the right neutral location.
const EXISTING_WORKTREE_BASE = '/tmp';
// Fresh-project worktrees live under destinationParent so commit time is a
// same-volume rename instead of a cross-volume cp. Tucked under a
// dot-prefixed dir so the editor file tree collapses them cleanly.
exports.FRESH_WORKTREES_DIRNAME = '.rivet-variants';
const WORKTREE_PREFIX = 'rivet-variants';
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
// Include the PID so each server instance only cleans up its own worktrees.
exports.INSTANCE_PREFIX = `${WORKTREE_PREFIX}-pid${process.pid}`;
// Registry of every destinationParent that has ever housed a fresh-worktree
// root in this user account. Read at startup so orphan cleanup can sweep
// roots created by prior MCP processes that crashed before teardown ran.
const FRESH_ROOTS_REGISTRY_PATH = path_1.default.join(os_1.default.homedir(), '.rivet', 'fresh-worktree-roots.json');
const FRESH_SESSION_ERROR = 'Fresh-project sessions have no source project — this operation requires an existing project path.';
class WorktreeManager {
    resolveProjectPath;
    /**
     * Accepts either a static path, null, or a lazy getter. The getter form
     * lets long-lived callers (e.g. the agent-variants orchestrator) defer the
     * project path until each operation, since `bridge.getProjectPath()`
     * isn't known until `open_visual_editor` runs. A null projectPath is
     * allowed for fresh-project sessions that scaffold from scratch and
     * therefore have no source repo; methods that need a real project path
     * throw a clear error in that case.
     */
    constructor(projectPath) {
        if (typeof projectPath === 'function') {
            this.resolveProjectPath = projectPath;
        }
        else {
            const value = projectPath;
            this.resolveProjectPath = () => value;
        }
    }
    get projectPath() {
        const resolved = this.resolveProjectPath();
        if (!resolved) {
            throw new Error(FRESH_SESSION_ERROR);
        }
        return resolved;
    }
    /**
     * Return the absolute git root for the repo containing this.projectPath.
     * When projectPath is a subdirectory (e.g. examples/microsoft-paint inside
     * rivet-core), this returns the repo root — not the subdirectory.
     */
    async getGitRoot() {
        const root = await (0, simple_git_1.simpleGit)(this.projectPath, GIT_OPTS).revparse([
            '--show-toplevel',
        ]);
        return root.trim();
    }
    /**
     * Ensure `this.projectPath` is inside a git repo, bootstrapping one if not.
     *
     * Fresh / blank projects (an empty folder, or a plain HTML preview) that the
     * user opens outside any repo would otherwise fail at `git worktree add`
     * (and `getGitRoot`'s `rev-parse --show-toplevel`) with "expected a git
     * workspace". We `git init` the folder and create an EMPTY initial commit so
     * `HEAD` exists — `git worktree add HEAD` requires a commit to point at, so a
     * bare `git init` on a blank folder isn't enough. `add -A` first captures
     * whatever is already there (e.g. an `index.html`) as the starting point.
     *
     * No-op when an enclosing repo already exists — we never nest a repo inside
     * another. Best-effort: a failure here surfaces at the subsequent worktree
     * step rather than masking the original operation.
     */
    async ensureGitRepo() {
        const projectPath = this.projectPath;
        const git = (0, simple_git_1.simpleGit)(projectPath, GIT_OPTS);
        const insideRepo = await git
            .raw(['rev-parse', '--is-inside-work-tree'])
            .then((out) => out.trim() === 'true')
            .catch(() => false);
        if (insideRepo)
            return;
        log.info(`No git repo at ${projectPath} — initializing one for Rivet`);
        await git.init();
        // Stage anything already present so a non-empty start point (e.g. an
        // HTML preview) lands in the initial commit. Empty dir → nothing staged,
        // and `--allow-empty` still produces the HEAD commit we need.
        await git.add(['-A']).catch(() => { });
        await git.raw([
            '-c',
            'user.name=Rivet',
            '-c',
            'user.email=hello@tryrivet.design',
            'commit',
            '--allow-empty',
            '--no-gpg-sign',
            '-m',
            'rivet: initial',
        ]);
    }
    /**
     * Return the path to the project within a worktree.
     * When projectPath == gitRoot this is just worktreePath.
     * When projectPath is a subdirectory (e.g. rivet-core/examples/microsoft-paint),
     * this returns worktreePath/examples/microsoft-paint so the dev server starts
     * from the right directory.
     */
    async getProjectCwdInWorktree(worktreePath) {
        const gitRoot = await this.getGitRoot();
        const relPath = path_1.default.relative(gitRoot, path_1.default.resolve(this.projectPath));
        return relPath ? path_1.default.join(worktreePath, relPath) : worktreePath;
    }
    /**
     * Translate an absolute path that lives inside the main project tree to its
     * equivalent path inside a worktree.  Paths outside the git root are returned
     * unchanged so the method is safe to call on arbitrary strings.
     */
    async translateToWorktreePath(absPath, worktreePath) {
        const gitRoot = await this.getGitRoot();
        const rel = path_1.default.relative(gitRoot, absPath);
        if (rel.startsWith('..') || path_1.default.isAbsolute(rel))
            return absPath;
        return path_1.default.join(worktreePath, rel);
    }
    /** Get diff of all uncommitted changes (staged + unstaged) vs HEAD */
    async getUncommittedPatch() {
        const git = (0, simple_git_1.simpleGit)(this.projectPath, GIT_OPTS);
        const diff = await git.diff(['HEAD']);
        return diff;
    }
    /**
     * Return untracked, non-ignored file paths relative to the git root.
     * These are files present in the working tree but not yet in any commit,
     * so `git diff HEAD` won't include them.
     */
    async getUntrackedFiles() {
        const gitRoot = await this.getGitRoot();
        const git = (0, simple_git_1.simpleGit)(gitRoot, GIT_OPTS);
        const result = await git
            .raw(['ls-files', '--others', '--exclude-standard'])
            .catch(() => '');
        return result.split('\n').filter(Boolean);
    }
    /** Copy untracked files from the source repo into a worktree */
    copyUntrackedFiles(untrackedFiles, gitRoot, worktreePath) {
        for (const relPath of untrackedFiles) {
            try {
                const src = path_1.default.join(gitRoot, relPath);
                const dest = path_1.default.join(worktreePath, relPath);
                fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
                fs_1.default.copyFileSync(src, dest);
            }
            catch (err) {
                log.warn(`Could not copy untracked file ${relPath} to worktree:`, err);
            }
        }
    }
    /**
     * @effect Links an existing dependency install into a worktree location when the destination is absent.
     */
    symlinkNodeModulesIfAvailable(sourceRoot, destRoot) {
        const nodeModulesSource = path_1.default.join(sourceRoot, 'node_modules');
        const nodeModulesDest = path_1.default.join(destRoot, 'node_modules');
        if (fs_1.default.existsSync(nodeModulesSource) &&
            !fs_1.default.existsSync(nodeModulesDest)) {
            fs_1.default.mkdirSync(destRoot, { recursive: true });
            fs_1.default.symlinkSync(nodeModulesSource, nodeModulesDest);
        }
    }
    /** Apply a patch string via stdin to the given cwd using git apply */
    applyPatchViaStdin(patch, cwd) {
        return new Promise((resolve, reject) => {
            const cp = (0, child_process_1.spawn)('git', ['apply', '--whitespace=nowarn', '-'], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
            let stderr = '';
            cp.stderr.on('data', (d) => { stderr += d.toString(); });
            cp.on('close', (code) => {
                if (code !== 0)
                    reject(new Error(`git apply failed: ${stderr}`));
                else
                    resolve();
            });
            cp.stdin.write(patch);
            cp.stdin.end();
        });
    }
    /** Create N worktrees for a session, apply uncommitted patch to each, return paths */
    async createWorktrees(sessionId, count) {
        this.assertSafeSessionId(sessionId);
        // Blank / non-repo workspaces (empty folder or a bare HTML preview) have
        // no git history for `git worktree add` to branch from — bootstrap one
        // first so variants work outside an existing repo.
        await this.ensureGitRepo();
        const git = (0, simple_git_1.simpleGit)(this.projectPath, GIT_OPTS);
        const patch = await this.getUncommittedPatch();
        const gitRoot = await this.getGitRoot();
        const untrackedFiles = await this.getUntrackedFiles();
        const paths = [];
        const projectCwdRelative = path_1.default.relative(gitRoot, path_1.default.resolve(this.projectPath));
        for (let i = 0; i < count; i++) {
            const worktreePath = path_1.default.join(EXISTING_WORKTREE_BASE, `${exports.INSTANCE_PREFIX}-${sessionId}-${i}`);
            try {
                await git.raw(['worktree', 'add', '--detach', worktreePath, 'HEAD']);
                // Symlink dependency installs into both roots the dev tooling may
                // resolve from: the repo root for shared config files and the project
                // subdirectory for app-local imports.
                const projectCwdInWorktree = projectCwdRelative
                    ? path_1.default.join(worktreePath, projectCwdRelative)
                    : worktreePath;
                this.symlinkNodeModulesIfAvailable(gitRoot, worktreePath);
                if (projectCwdInWorktree !== worktreePath) {
                    this.symlinkNodeModulesIfAvailable(this.projectPath, projectCwdInWorktree);
                }
                // Apply uncommitted changes so agent sees current state, then commit
                // them as a "baseline" so getDiff returns only the agent's delta.
                // Without this, getDiff (git diff HEAD) would include the user's
                // pre-existing changes, and reverting the combined diff would wipe them.
                if (patch.trim()) {
                    await this.applyPatchViaStdin(patch, worktreePath).catch(() => {
                        log.warn(`Could not apply uncommitted patch to worktree ${i} — working from HEAD`);
                    });
                }
                // Copy untracked files — git diff HEAD only covers tracked files, so
                // untracked-but-not-ignored files (e.g. new components not yet committed)
                // must be copied manually or the worktree's dev server will fail to
                // resolve their imports.
                if (untrackedFiles.length > 0) {
                    this.copyUntrackedFiles(untrackedFiles, gitRoot, worktreePath);
                }
                const wtGit = (0, simple_git_1.simpleGit)(worktreePath, GIT_OPTS);
                await wtGit.raw(['add', '-A']).catch(() => { });
                await wtGit
                    .raw([
                    'commit',
                    '--allow-empty',
                    '--no-gpg-sign',
                    '-m',
                    'rivet: variant baseline',
                ])
                    .catch(() => { });
                paths.push(worktreePath);
            }
            catch (e) {
                log.error(`Failed to create worktree ${i}:`, e);
                const msg = e instanceof Error ? e.message : String(e);
                if (/block timeout reached/i.test(msg)) {
                    throw new Error(`git stalled while provisioning variant worktree ${i} (no output for ${GIT_BLOCK_TIMEOUT_MS / 1000}s). ` +
                        `This usually means a very large working tree (e.g. a React Native project with native build dirs ` +
                        `under ios/android that aren't git-ignored) or a wedged git process. ` +
                        `Consider .gitignoring build artifacts before generating variants.`);
                }
                throw e;
            }
        }
        return paths;
    }
    /** Get git diff of all changes made in a worktree vs HEAD.
     *
     * `git diff HEAD` only covers tracked files. New files created by the agent
     * are untracked and would be silently omitted, breaking imports/styles when
     * the diff is applied to the main tree. `git add --intent-to-add .` marks
     * every untracked file in the index as a zero-length blob so `git diff HEAD`
     * includes them as pure additions without actually staging their content.
     */
    async getDiff(worktreePath) {
        const git = (0, simple_git_1.simpleGit)(worktreePath);
        // Stage new files with intent-to-add so they appear in the diff
        await git.raw(['add', '--intent-to-add', '.']).catch(() => {
            // Non-fatal — if this fails, fall back to tracked-only diff
        });
        return git.diff(['HEAD']);
    }
    /**
     * Apply or revert a diff to the main project working tree.
     *
     * Because getDiff now diffs against the worktree baseline commit (HEAD +
     * user's uncommitted changes), the codeDiff contains only the agent's delta.
     * This means git apply --reverse reliably restores to the baseline state
     * (user's uncommitted changes intact) without touching git checkout HEAD,
     * which would wipe the user's pre-existing work.
     */
    async applyDiff(diff, reverse) {
        if (!diff.trim())
            return;
        const args = ['apply', '--whitespace=nowarn'];
        if (reverse)
            args.push('--reverse');
        args.push('-');
        return new Promise((resolve, reject) => {
            const cp = (0, child_process_1.spawn)('git', args, {
                cwd: this.projectPath,
                stdio: ['pipe', 'pipe', 'pipe'],
            });
            let stderr = '';
            cp.stderr.on('data', (d) => { stderr += d.toString(); });
            cp.on('close', (code) => {
                if (code !== 0)
                    reject(new Error(`git apply failed: ${stderr}`));
                else
                    resolve();
            });
            cp.stdin.write(diff);
            cp.stdin.end();
        });
    }
    /**
     * Apply a unified diff to the repository that contains the target project path.
     */
    async applyDiffToProject(diff, projectPath) {
        if (!diff.trim())
            return;
        const git = (0, simple_git_1.simpleGit)(projectPath);
        const gitRoot = (await git.revparse(['--show-toplevel'])).trim();
        return this.applyPatchViaStdin(diff, gitRoot);
    }
    /** Bind port 0 to get an OS-assigned free port */
    getFreePort() {
        return new Promise((resolve, reject) => {
            const server = net_1.default.createServer();
            server.listen(0, () => {
                const addr = server.address();
                const port = addr && typeof addr === 'object' ? addr.port : 0;
                server.close(() => resolve(port));
            });
            server.on('error', reject);
        });
    }
    /** Validate a diff via `git apply --check` without modifying files */
    validateDiff(diff, reverse) {
        if (!diff.trim())
            return Promise.resolve();
        return new Promise((resolve, reject) => {
            const args = ['apply', '--check', '--whitespace=nowarn'];
            if (reverse)
                args.push('--reverse');
            args.push('-');
            const cp = (0, child_process_1.spawn)('git', args, {
                cwd: this.projectPath,
                stdio: ['pipe', 'pipe', 'pipe'],
            });
            let stderr = '';
            cp.stderr.on('data', (d) => { stderr += d.toString(); });
            cp.on('close', (code) => {
                if (code !== 0)
                    reject(new Error(`Diff validation failed: ${stderr.trim()}`));
                else
                    resolve();
            });
            cp.stdin.write(diff);
            cp.stdin.end();
        });
    }
    /**
     * Spawn a dev server and poll until it responds with a non-502 HTTP status
     * (max 30s). Throws on timeout.
     */
    async startDevServer(worktreePath, port, cmd, args, env) {
        const proc = (0, child_process_1.spawn)(cmd, args, {
            cwd: worktreePath,
            env: { ...process.env, ...env },
            stdio: 'pipe',
            detached: false,
        });
        proc.on('error', (err) => log.warn(`Dev server error (port ${port}):`, err));
        // Drain stdout/stderr. With `stdio: 'pipe'` and no consumer, the OS pipe
        // buffer (~64KB) fills within seconds of Vite's HMR/request logging and
        // the child blocks on write, freezing the dev server even though it
        // passed the readiness probe.
        proc.stdout?.on('data', (data) => {
            log.debug(`[variant-dev:${port}] ${data.toString().trim()}`);
        });
        proc.stderr?.on('data', (data) => {
            log.debug(`[variant-dev:${port}] ${data.toString().trim()}`);
        });
        const deadline = Date.now() + 30_000;
        while (Date.now() < deadline) {
            try {
                const res = await fetch(`http://localhost:${port}`, {
                    signal: AbortSignal.timeout(1_000),
                });
                if (res.status !== 502)
                    return proc;
            }
            catch {
                // not up yet
            }
            await new Promise((r) => setTimeout(r, 500));
        }
        try {
            proc.kill('SIGTERM');
        }
        catch { /* ignore */ }
        throw new Error(`Dev server on port ${port} did not become ready within 30s`);
    }
    /** Stop a dev server with SIGTERM + SIGKILL fallback */
    async stopDevServer(proc, timeoutMs = 5_000) {
        if (proc.killed || proc.exitCode !== null || proc.pid == null)
            return;
        const exited = new Promise((resolve) => {
            const done = () => { proc.off('exit', done); proc.off('error', done); resolve(); };
            proc.once('exit', done);
            proc.once('error', done);
        });
        try {
            proc.kill('SIGTERM');
        }
        catch { /* ignore */ }
        const timedOut = await Promise.race([
            exited.then(() => false),
            new Promise((r) => setTimeout(() => r(true), timeoutMs)),
        ]);
        if (!timedOut)
            return;
        try {
            proc.kill('SIGKILL');
        }
        catch { /* ignore */ }
        await exited;
    }
    /** Remove all worktrees for a session.
     *
     *  For existing-project sessions, asks git to unregister the worktree
     *  (handles `.git/worktrees/*` cleanup) and falls back to plain rm. For
     *  fresh-project sessions (no projectPath) the worktrees are detached
     *  directories with their own `.git` under `<destinationParent>/.rivet-variants/`
     *  — git registry cleanup is a no-op and we just rm the matching dirs.
     *
     *  `freshDestinationParent` is required when the session was fresh; pass
     *  `path.dirname(projectContext.workspacePath)` so we know which
     *  `.rivet-variants/` root to sweep.
     */
    async cleanupSession(sessionId, freshDestinationParent) {
        this.assertSafeSessionId(sessionId);
        if (!this.resolveProjectPath()) {
            // Fresh sessions: no source repo, no git registry to clean. Just rm.
            if (freshDestinationParent) {
                this.cleanupFreshWorktrees(sessionId, freshDestinationParent);
            }
            return;
        }
        // GIT_OPTS so cleanup can't re-hang: this path runs from teardown after a
        // provision timeout/stall, and an unbounded `git worktree list`/`remove`
        // would leave the tool hanging on cleanup instead of returning the error.
        const git = (0, simple_git_1.simpleGit)(this.projectPath, GIT_OPTS);
        const pattern = path_1.default.join(EXISTING_WORKTREE_BASE, `${exports.INSTANCE_PREFIX}-${sessionId}-`);
        try {
            const worktreeList = await git.raw(['worktree', 'list', '--porcelain']);
            const worktreePaths = worktreeList
                .split('\n')
                .filter((line) => line.startsWith('worktree '))
                .map((line) => line.replace('worktree ', '').trim())
                .filter((p) => p.startsWith(pattern));
            for (const wPath of worktreePaths) {
                try {
                    await git.raw(['worktree', 'remove', '--force', wPath]);
                }
                catch {
                    // Force remove directory if git command fails
                    fs_1.default.rmSync(wPath, { recursive: true, force: true });
                }
            }
        }
        catch (e) {
            log.warn('Worktree cleanup error:', e);
        }
        // Always also remove any leftover fresh-style directories for this
        // session (e.g. mixed-mode sessions, or git registry mismatch).
        if (freshDestinationParent) {
            this.cleanupFreshWorktrees(sessionId, freshDestinationParent);
        }
    }
    /**
     * Create N worktrees for a fresh-project session by writing a template
     * bundle into each `<destinationParent>/.rivet-variants/rivet-variants-pid<PID>-<sessionId>-<i>`
     * directory, optionally writing a DESIGN.md per slot, and committing a
     * baseline.
     *
     * Worktrees live under the user's destinationParent (not /tmp) so commit
     * time can be a same-volume rename instead of a cross-volume copy. The
     * destinationParent is registered to a small JSON file so a future MCP
     * startup can sweep orphans even when the original process crashed.
     *
     * Does NOT run `npm install` — the orchestrator dispatches that as a
     * background task so failures surface via work-item state without blocking
     * the MCP tool response.
     *
     * @effect Creates directories under destinationParent, writes template
     *         files, runs git init/add/commit, appends destinationParent to
     *         the fresh-worktree-roots registry.
     */
    async createFreshWorktrees(sessionId, count, template, designContext, sourceContext, destinationParent) {
        this.assertSafeSessionId(sessionId);
        if (!destinationParent || !path_1.default.isAbsolute(destinationParent)) {
            throw new Error('createFreshWorktrees requires an absolute destinationParent path');
        }
        const freshRoot = path_1.default.join(destinationParent, exports.FRESH_WORKTREES_DIRNAME);
        fs_1.default.mkdirSync(freshRoot, { recursive: true });
        (0, exports.registerFreshWorktreeRoot)(freshRoot);
        const paths = [];
        for (let i = 0; i < count; i++) {
            const worktreePath = path_1.default.join(freshRoot, `${exports.INSTANCE_PREFIX}-${sessionId}-${i}`);
            fs_1.default.mkdirSync(worktreePath, { recursive: true });
            (0, viteReactTs_1.writeTemplateBundle)(template, worktreePath);
            this.writeDesignContext(worktreePath, i, designContext?.[i]);
            this.writeSourceContext(worktreePath, sourceContext);
            const wtGit = (0, simple_git_1.simpleGit)(worktreePath, GIT_OPTS);
            try {
                await wtGit.raw(['init']);
                await wtGit.raw(['add', '-A']);
                await wtGit
                    .raw([
                    '-c',
                    'user.name=Rivet',
                    '-c',
                    'user.email=hello@tryrivet.design',
                    'commit',
                    '--no-gpg-sign',
                    '-m',
                    'rivet: variant baseline',
                ])
                    .catch((err) => {
                    log.warn(`Fresh worktree ${i}: baseline commit failed`, err);
                });
            }
            catch (err) {
                // Best-effort: even without a baseline commit the worktree is usable
                // for the agent — log and continue.
                log.warn(`Fresh worktree ${i}: git init failed`, err);
            }
            paths.push(worktreePath);
        }
        return paths;
    }
    /**
     * Write SOURCE_CONTEXT.md when source context exists for the session.
     *
     * @effect Writes SOURCE_CONTEXT.md for source-grounded fresh worktrees.
     */
    writeSourceContext(worktreePath, sourceContext) {
        if (!sourceContext)
            return;
        if (sourceContext.artifact) {
            fs_1.default.writeFileSync(path_1.default.join(worktreePath, 'SOURCE_CONTEXT.md'), (0, sourceContext_1.buildSourceContextMarkdown)(sourceContext.artifact), 'utf8');
            return;
        }
        if ((sourceContext.sourceUrls?.length ?? 0) === 0)
            return;
        const lines = [
            '# SOURCE_CONTEXT',
            '',
            '## Source URLs',
            ...sourceContext.sourceUrls.map((url) => `- ${url}`),
            '',
            '## Notes',
            '- Source research was provided as URL-only context.',
        ];
        fs_1.default.writeFileSync(path_1.default.join(worktreePath, 'SOURCE_CONTEXT.md'), `${lines.join('\n')}\n`, 'utf8');
    }
    /**
     * Write the per-slot DESIGN.md for a fresh worktree.
     *
     * @effect Writes DESIGN.md when a design context is available.
     */
    writeDesignContext(worktreePath, slot, entry) {
        if (!entry)
            return;
        if (entry.kind === 'markdown') {
            const label = entry.label.replace(/-->/g, '-- >');
            fs_1.default.writeFileSync(path_1.default.join(worktreePath, 'DESIGN.md'), `<!-- Custom design context: ${label} -->\n\n${entry.content}`, 'utf8');
            return;
        }
        const markdown = (0, designCatalog_1.loadDesignSystemMarkdown)(entry.slug);
        if (markdown) {
            fs_1.default.writeFileSync(path_1.default.join(worktreePath, 'DESIGN.md'), markdown, 'utf8');
            return;
        }
        log.warn(`Fresh worktree ${slot}: unknown design system slug '${entry.slug}' — DESIGN.md skipped`);
    }
    /**
     * Move a worktree to its history location preserving contents. Used by the
     * orchestrator at commit time so unchosen variants survive on disk as
     * runnable references (siblings of the chosen project).
     *
     * Returns the destination path (`<historyDir>/<folderName>`).
     *
     * @effect renames `srcWorktreePath` → `<historyDir>/<folderName>`, creating
     *         the historyDir if needed.
     */
    moveWorktreeToHistory(srcWorktreePath, historyDir, folderName) {
        fs_1.default.mkdirSync(historyDir, { recursive: true });
        const dest = path_1.default.join(historyDir, folderName);
        fs_1.default.renameSync(srcWorktreePath, dest);
        return dest;
    }
    /**
     * Repoint a worktree's `node_modules` at a relative symlink so it tracks
     * the chosen variant's real install after both have moved into their new
     * permanent homes. Idempotent — removes any existing entry first.
     */
    repointNodeModulesSymlink(worktreePath, sharedNodeModulesAbsPath) {
        const link = path_1.default.join(worktreePath, 'node_modules');
        try {
            fs_1.default.rmSync(link, { force: true, recursive: true });
        }
        catch {
            /* may not exist */
        }
        const target = path_1.default.relative(path_1.default.dirname(link), sharedNodeModulesAbsPath);
        fs_1.default.symlinkSync(target, link, 'dir');
    }
    /** Remove all fresh worktrees for a session.
     *
     *  @effect Removes directories under <destinationParent>/.rivet-variants/
     *          matching ${INSTANCE_PREFIX}-<sessionId>-*. Also removes the
     *          .rivet-variants/ parent if it becomes empty so the editor file
     *          tree doesn't accumulate noise.
     *
     *  When `preserve` is provided, entries whose basenames are in the set are
     *  skipped — used by the orchestrator after a vite_app commit, where the
     *  unchosen variant worktrees have already been moved to a sibling history
     *  dir and must not be re-deleted.
     */
    cleanupFreshWorktrees(sessionId, destinationParent, preserve) {
        this.assertSafeSessionId(sessionId);
        if (!destinationParent || !path_1.default.isAbsolute(destinationParent)) {
            log.warn('cleanupFreshWorktrees: destinationParent missing or not absolute — skipping');
            return;
        }
        const freshRoot = path_1.default.join(destinationParent, exports.FRESH_WORKTREES_DIRNAME);
        try {
            const prefix = `${exports.INSTANCE_PREFIX}-${sessionId}-`;
            const entries = fs_1.default
                .readdirSync(freshRoot)
                .filter((e) => e.startsWith(prefix))
                .filter((e) => !preserve?.has(e));
            for (const entry of entries) {
                fs_1.default.rmSync(path_1.default.join(freshRoot, entry), {
                    recursive: true,
                    force: true,
                });
            }
            // If the freshRoot is now empty, remove it too.
            try {
                const remaining = fs_1.default.readdirSync(freshRoot);
                if (remaining.length === 0) {
                    fs_1.default.rmdirSync(freshRoot);
                }
            }
            catch {
                // Non-fatal — leaving the dir is harmless.
            }
        }
        catch (e) {
            // ENOENT is expected when the dir was already cleaned up — only log
            // when it's a different failure mode.
            if (e.code !== 'ENOENT') {
                log.warn('Fresh worktree cleanup error:', e);
            }
        }
    }
    assertSafeSessionId(sessionId) {
        if (!SESSION_ID_PATTERN.test(sessionId)) {
            throw new Error('Invalid sessionId: expected a safe path segment');
        }
    }
}
exports.WorktreeManager = WorktreeManager;
/**
 * Read the fresh-worktree-root registry, dropping entries whose dir no
 * longer exists so the file stays small.
 */
const loadFreshWorktreeRoots = () => {
    try {
        const raw = fs_1.default.readFileSync(FRESH_ROOTS_REGISTRY_PATH, 'utf8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return [];
        return parsed.filter((p) => typeof p === 'string');
    }
    catch {
        return [];
    }
};
const saveFreshWorktreeRoots = (roots) => {
    try {
        fs_1.default.mkdirSync(path_1.default.dirname(FRESH_ROOTS_REGISTRY_PATH), { recursive: true });
        const deduped = Array.from(new Set(roots));
        fs_1.default.writeFileSync(FRESH_ROOTS_REGISTRY_PATH, JSON.stringify(deduped, null, 2), 'utf8');
    }
    catch (err) {
        log.warn('Could not persist fresh-worktree-roots registry:', err);
    }
};
/**
 * Append a freshRoot path to the registry (de-duped). Called by
 * createFreshWorktrees so future MCP startups can sweep orphans even if
 * the original process crashed before teardown ran.
 */
const registerFreshWorktreeRoot = (freshRoot) => {
    const existing = loadFreshWorktreeRoots();
    if (existing.includes(freshRoot))
        return;
    existing.push(freshRoot);
    saveFreshWorktreeRoots(existing);
};
exports.registerFreshWorktreeRoot = registerFreshWorktreeRoot;
/**
 * Sweep orphan worktrees from /tmp (existing-project flow) and from every
 * registered fresh-worktree root (fresh-project flow). Filters by
 * INSTANCE_PREFIX (includes PID) so each server instance only removes its
 * own worktrees and never touches those of sibling instances. Prunes
 * registry entries whose directory no longer exists so the file stays small.
 */
const cleanupOrphanedWorktrees = () => {
    try {
        const entries = fs_1.default
            .readdirSync(EXISTING_WORKTREE_BASE)
            .filter((e) => e.startsWith(exports.INSTANCE_PREFIX));
        for (const entry of entries) {
            fs_1.default.rmSync(path_1.default.join(EXISTING_WORKTREE_BASE, entry), {
                recursive: true,
                force: true,
            });
        }
    }
    catch {
        /* ignore */
    }
    const roots = loadFreshWorktreeRoots();
    const survivingRoots = [];
    for (const root of roots) {
        let rootStillExists = true;
        try {
            const entries = fs_1.default
                .readdirSync(root)
                .filter((e) => e.startsWith(exports.INSTANCE_PREFIX));
            for (const entry of entries) {
                fs_1.default.rmSync(path_1.default.join(root, entry), { recursive: true, force: true });
            }
        }
        catch (err) {
            if (err.code === 'ENOENT') {
                rootStillExists = false;
            }
        }
        if (rootStillExists)
            survivingRoots.push(root);
    }
    if (survivingRoots.length !== roots.length) {
        saveFreshWorktreeRoots(survivingRoots);
    }
};
exports.cleanupOrphanedWorktrees = cleanupOrphanedWorktrees;
// Clean up on natural exit (covers process.exit() calls from server shutdown)
process.on('exit', exports.cleanupOrphanedWorktrees);
/**
 * Prune stale worktree registry entries for a project (`.git/worktrees/<name>`
 * entries where the referenced directory no longer exists). Safe to call at
 * startup — only removes entries for directories that are already gone.
 */
const pruneWorktrees = async (projectPath) => {
    try {
        await (0, simple_git_1.simpleGit)(projectPath, GIT_OPTS).raw(['worktree', 'prune']);
    }
    catch {
        // Non-fatal — prune is best-effort housekeeping
    }
};
exports.pruneWorktrees = pruneWorktrees;
//# sourceMappingURL=WorktreeManager.js.map