import { execFile } from "child_process";
import { promisify } from "util";
import { existsSync, mkdirSync, realpathSync, rmSync, statSync } from "fs";
import { copyFile, rm, stat } from "fs/promises";
import { join, basename, dirname, isAbsolute, resolve, sep } from "path";
import net from "node:net";
import { createHash } from "node:crypto";
import stripAnsi from "strip-ansi";
import { buildStringCommandShellInvocation } from "./string-command-shell.js";
import { readPaseoConfigJson, resolvePaseoConfigPath } from "./paseo-config-file.js";
export { PaseoConfigRawSchema, PaseoLifecycleCommandRawSchema, PaseoScriptEntryRawSchema, PaseoWorktreeConfigRawSchema, PaseoConfigSchema, } from "@kong-link/protocol/paseo-config-schema";
import { PaseoConfigSchema } from "@kong-link/protocol/paseo-config-schema";
import { normalizeBaseRefName, readPaseoWorktreeMetadata, readPaseoWorktreeRuntimePort, writePaseoWorktreeMetadata, writePaseoWorktreeRuntimeMetadata, } from "./worktree-metadata.js";
import { runGitCommand } from "./run-git-command.js";
import { spawnProcess } from "./spawn.js";
import { resolvePaseoHome } from "../server/paseo-home.js";
import { createExternalProcessEnv } from "../server/paseo-env.js";
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
import { validateBranchSlug } from "@kong-link/protocol/branch-slug";
import { expandTilde } from "./path.js";
export { slugify, validateBranchSlug } from "@kong-link/protocol/branch-slug";
const execFileAsync = promisify(execFile);
const READ_ONLY_GIT_ENV = {
    GIT_OPTIONAL_LOCKS: "0",
};
export function isServiceScript(config) {
    return "type" in config && config.type === "service";
}
export class WorktreeSetupError extends Error {
    constructor(message, results) {
        super(message);
        this.name = "WorktreeSetupError";
        this.results = results;
    }
}
export class WorktreeTeardownError extends Error {
    constructor(message, results) {
        super(message);
        this.name = "WorktreeTeardownError";
        this.results = results;
    }
}
export class BranchAlreadyCheckedOutError extends Error {
    constructor(branchName) {
        super(`Branch already checked out: ${branchName}`);
        this.name = "BranchAlreadyCheckedOutError";
        this.branchName = branchName;
    }
}
export class UnknownBranchError extends Error {
    constructor(params) {
        super(`Unknown branch: ${params.branchName}`);
        this.name = "UnknownBranchError";
        this.branchName = params.branchName;
        this.cwd = params.cwd;
    }
}
export function readPaseoConfig(repoRoot) {
    try {
        const json = readPaseoConfigJson(repoRoot);
        if (json === null) {
            return { ok: true, config: null };
        }
        return { ok: true, config: PaseoConfigSchema.parse(json) };
    }
    catch (error) {
        return { ok: false, configPath: resolvePaseoConfigPath(repoRoot), error };
    }
}
export function paseoConfigParseError(failure) {
    const detail = failure.error instanceof Error ? failure.error.message : String(failure.error);
    return new Error(`Failed to parse paseo.json at ${failure.configPath}: ${detail}`, {
        cause: failure.error,
    });
}
function readPaseoConfigOrThrow(repoRoot) {
    const result = readPaseoConfig(repoRoot);
    if (!result.ok) {
        throw paseoConfigParseError(result);
    }
    return result.config;
}
export function getWorktreeSetupCommands(repoRoot) {
    return readPaseoConfigOrThrow(repoRoot)?.worktree?.setup ?? [];
}
export function getWorktreeTeardownCommands(repoRoot) {
    return readPaseoConfigOrThrow(repoRoot)?.worktree?.teardown ?? [];
}
export function getWorktreeTerminalSpecs(repoRoot) {
    const terminals = readPaseoConfigOrThrow(repoRoot)?.worktree?.terminals;
    if (!Array.isArray(terminals) || terminals.length === 0) {
        return [];
    }
    const specs = [];
    for (const terminal of terminals) {
        if (!terminal || typeof terminal !== "object") {
            continue;
        }
        const rawCommand = terminal.command;
        if (typeof rawCommand !== "string") {
            continue;
        }
        const command = rawCommand.trim();
        if (!command) {
            continue;
        }
        const rawName = terminal.name;
        const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName.trim() : undefined;
        specs.push({
            ...(name ? { name } : {}),
            command,
        });
    }
    return specs;
}
export function getScriptConfigs(config) {
    const scripts = config?.scripts;
    if (!scripts || typeof scripts !== "object") {
        return new Map();
    }
    const result = new Map();
    for (const [name, entry] of Object.entries(scripts)) {
        if (!entry || typeof entry !== "object") {
            continue;
        }
        const rawCommand = entry.command;
        if (typeof rawCommand !== "string") {
            continue;
        }
        const command = rawCommand.trim();
        if (!command) {
            continue;
        }
        const scriptConfig = entry.type === "service"
            ? {
                type: "service",
                command,
            }
            : { command };
        if (isServiceScript(scriptConfig) &&
            typeof entry.port === "number" &&
            Number.isFinite(entry.port)) {
            scriptConfig.port = entry.port;
        }
        result.set(name, scriptConfig);
    }
    return result;
}
export function processCarriageReturns(text) {
    if (!text.includes("\r")) {
        return text;
    }
    const output = [];
    let line = [];
    let cursor = 0;
    const flushLine = () => {
        output.push(line.join(""));
        line = [];
        cursor = 0;
    };
    for (let index = 0; index < text.length; index += 1) {
        const char = text[index];
        if (char === "\r") {
            if (text[index + 1] === "\n") {
                flushLine();
                output.push("\n");
                index += 1;
                continue;
            }
            cursor = 0;
            continue;
        }
        if (char === "\n") {
            flushLine();
            output.push("\n");
            continue;
        }
        if (cursor < line.length) {
            line[cursor] = char;
        }
        else {
            line.push(char);
        }
        cursor += 1;
    }
    if (line.length > 0) {
        output.push(line.join(""));
    }
    return output.join("");
}
async function execSetupCommand(command, options) {
    const startedAt = Date.now();
    const shellInvocation = buildStringCommandShellInvocation({ command });
    try {
        const { stdout, stderr } = await execFileAsync(shellInvocation.shell, shellInvocation.args, {
            cwd: options.cwd,
            env: options.env,
        });
        return {
            command,
            cwd: options.cwd,
            stdout: stdout ?? "",
            stderr: stderr ?? "",
            exitCode: 0,
            durationMs: Date.now() - startedAt,
        };
    }
    catch (error) {
        const execErr = error;
        return {
            command,
            cwd: options.cwd,
            stdout: execErr?.stdout ?? "",
            stderr: execErr?.stderr ?? (error instanceof Error ? error.message : String(error)),
            exitCode: typeof execErr?.code === "number" ? execErr.code : null,
            durationMs: Date.now() - startedAt,
        };
    }
}
async function execSetupCommandStreamed(options) {
    return new Promise((resolvePromise) => {
        const startedAt = Date.now();
        const stdoutChunks = [];
        const stderrChunks = [];
        let settled = false;
        const emitOutput = (stream, chunk) => {
            const text = stripAnsi(chunk);
            if (!text) {
                return;
            }
            if (stream === "stdout") {
                stdoutChunks.push(text);
            }
            else {
                stderrChunks.push(text);
            }
            options.onEvent?.({
                type: "output",
                index: options.index,
                total: options.total,
                command: options.command,
                cwd: options.cwd,
                stream,
                chunk: text,
            });
        };
        const finish = (exitCode) => {
            if (settled) {
                return;
            }
            settled = true;
            const result = {
                command: options.command,
                cwd: options.cwd,
                stdout: stdoutChunks.join(""),
                stderr: stderrChunks.join(""),
                exitCode,
                durationMs: Date.now() - startedAt,
            };
            options.onEvent?.({
                type: "command_completed",
                index: options.index,
                total: options.total,
                command: options.command,
                cwd: options.cwd,
                exitCode: result.exitCode,
                durationMs: result.durationMs,
                stdout: result.stdout,
                stderr: result.stderr,
            });
            resolvePromise(result);
        };
        options.onEvent?.({
            type: "command_started",
            index: options.index,
            total: options.total,
            command: options.command,
            cwd: options.cwd,
        });
        const shellInvocation = buildStringCommandShellInvocation({ command: options.command });
        const child = spawnProcess(shellInvocation.shell, shellInvocation.args, {
            cwd: options.cwd,
            env: options.env,
            stdio: ["ignore", "pipe", "pipe"],
        });
        child.stdout?.on("data", (chunk) => {
            emitOutput("stdout", chunk.toString());
        });
        child.stderr?.on("data", (chunk) => {
            emitOutput("stderr", chunk.toString());
        });
        child.on("error", (error) => {
            emitOutput("stderr", error instanceof Error ? error.message : String(error));
            finish(null);
        });
        child.on("close", (code) => {
            finish(typeof code === "number" ? code : null);
        });
    });
}
async function getAvailablePort() {
    return new Promise((resolvePromise, reject) => {
        const server = net.createServer();
        server.once("error", reject);
        server.listen(0, () => {
            const address = server.address();
            if (!address || typeof address === "string") {
                server.close(() => reject(new Error("Failed to acquire available port")));
                return;
            }
            server.close((error) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolvePromise(address.port);
            });
        });
    });
}
async function assertPortAvailable(port) {
    await new Promise((resolvePromise, reject) => {
        const server = net.createServer();
        server.once("error", (error) => {
            let message;
            if (error?.code === "EADDRINUSE") {
                message = `Persisted worktree port ${port} is already in use`;
            }
            else if (error instanceof Error) {
                message = error.message;
            }
            else {
                message = String(error);
            }
            reject(new Error(message));
        });
        server.listen(port, () => {
            server.close((error) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolvePromise();
            });
        });
    });
}
async function inferRepoRootPathFromWorktreePath(worktreePath) {
    try {
        const commonDir = await getGitCommonDir(worktreePath);
        const normalizedCommonDir = normalizePathForOwnership(commonDir);
        // Normal repo/worktree: common dir is <repoRoot>/.git
        if (basename(normalizedCommonDir) === ".git") {
            return dirname(normalizedCommonDir);
        }
        // Bare repo: common dir is the repo dir itself
        return normalizedCommonDir;
    }
    catch {
        // Fallback: best-effort resolve toplevel (will be the worktree root in typical cases)
        try {
            const { stdout } = await runGitCommand(["rev-parse", "--show-toplevel"], {
                cwd: worktreePath,
                envOverlay: READ_ONLY_GIT_ENV,
            });
            const topLevel = parseGitRevParsePath(stdout);
            if (topLevel) {
                return normalizePathForOwnership(topLevel);
            }
        }
        catch {
            // ignore
        }
        return normalizePathForOwnership(worktreePath);
    }
}
export async function runWorktreeSetupCommands(options) {
    // Read paseo.json from the worktree (it will have the same content as the source repo)
    const setupCommands = getWorktreeSetupCommands(options.worktreePath);
    if (setupCommands.length === 0) {
        return [];
    }
    const runtimeEnv = options.runtimeEnv ??
        (await resolveWorktreeRuntimeEnv({
            worktreePath: options.worktreePath,
            branchName: options.branchName,
            ...(options.repoRootPath ? { repoRootPath: options.repoRootPath } : {}),
        }));
    const setupEnv = createExternalProcessEnv(process.env, runtimeEnv);
    const results = [];
    for (const [index, cmd] of setupCommands.entries()) {
        const result = options.onEvent
            ? await execSetupCommandStreamed({
                command: cmd,
                cwd: options.worktreePath,
                env: setupEnv,
                index: index + 1,
                total: setupCommands.length,
                onEvent: options.onEvent,
            })
            : await execSetupCommand(cmd, {
                cwd: options.worktreePath,
                env: setupEnv,
            });
        results.push(result);
        if (result.exitCode !== 0) {
            if (options.cleanupOnFailure) {
                try {
                    await runGitCommand(["worktree", "remove", options.worktreePath, "--force"], {
                        cwd: options.worktreePath,
                        timeout: 120000,
                    });
                }
                catch {
                    rmSync(options.worktreePath, { recursive: true, force: true });
                }
            }
            throw new WorktreeSetupError(`Worktree setup command failed: ${cmd}\n${result.stderr}`.trim(), results);
        }
    }
    return results;
}
async function resolveBranchNameForWorktreePath(worktreePath) {
    try {
        const { stdout } = await runGitCommand(["branch", "--show-current"], {
            cwd: worktreePath,
            envOverlay: READ_ONLY_GIT_ENV,
        });
        const branchName = stdout.trim();
        if (branchName.length > 0) {
            return branchName;
        }
    }
    catch {
        // ignore
    }
    return basename(worktreePath);
}
export async function resolveWorktreeRuntimeEnv(options) {
    const repoRootPath = options.repoRootPath ?? (await inferRepoRootPathFromWorktreePath(options.worktreePath));
    const branchName = options.branchName ?? (await resolveBranchNameForWorktreePath(options.worktreePath));
    let worktreePort = readPaseoWorktreeRuntimePort(options.worktreePath);
    if (worktreePort === null) {
        worktreePort = await getAvailablePort();
        const metadata = readPaseoWorktreeMetadata(options.worktreePath);
        if (metadata) {
            writePaseoWorktreeRuntimeMetadata(options.worktreePath, { worktreePort });
        }
    }
    else {
        await assertPortAvailable(worktreePort);
    }
    return {
        // Source checkout path is the original git repo root (shared across worktrees), not the
        // worktree itself. This allows setup scripts to copy local files (e.g. .env) from the
        // source checkout.
        PASEO_SOURCE_CHECKOUT_PATH: repoRootPath,
        // Backward-compatible alias.
        PASEO_ROOT_PATH: repoRootPath,
        PASEO_WORKTREE_PATH: options.worktreePath,
        PASEO_BRANCH_NAME: branchName,
        PASEO_WORKTREE_PORT: String(worktreePort),
    };
}
export async function runWorktreeTeardownCommands(options) {
    // Read paseo.json from the worktree (it will have the same content as the source repo)
    const teardownCommands = getWorktreeTeardownCommands(options.worktreePath);
    if (teardownCommands.length === 0) {
        return [];
    }
    const repoRootPath = options.repoRootPath ?? (await inferRepoRootPathFromWorktreePath(options.worktreePath));
    const branchName = options.branchName ?? (await resolveBranchNameForWorktreePath(options.worktreePath));
    const worktreePort = readPaseoWorktreeRuntimePort(options.worktreePath);
    const teardownEnv = createExternalProcessEnv(process.env, {
        // Source checkout path is the original git repo root (shared across worktrees), not the
        // worktree itself. This allows lifecycle scripts to copy or clean resources using paths
        // from the source checkout.
        PASEO_SOURCE_CHECKOUT_PATH: repoRootPath,
        // Backward-compatible alias.
        PASEO_ROOT_PATH: repoRootPath,
        PASEO_WORKTREE_PATH: options.worktreePath,
        PASEO_BRANCH_NAME: branchName,
        ...(worktreePort !== null ? { PASEO_WORKTREE_PORT: String(worktreePort) } : {}),
    });
    const results = [];
    for (const cmd of teardownCommands) {
        const result = await execSetupCommand(cmd, {
            cwd: options.worktreePath,
            env: teardownEnv,
        });
        results.push(result);
        if (result.exitCode !== 0) {
            throw new WorktreeTeardownError(`Worktree teardown command failed: ${cmd}\n${result.stderr}`.trim(), results);
        }
    }
    return results;
}
/**
 * Get the git common directory (shared across worktrees) for a given cwd.
 * This is where refs, objects, etc. are stored.
 */
export async function getGitCommonDir(cwd) {
    const { stdout } = await runGitCommand(["rev-parse", "--git-common-dir"], {
        cwd,
        envOverlay: READ_ONLY_GIT_ENV,
    });
    const commonDir = resolveGitRevParsePath(cwd, stdout);
    if (!commonDir) {
        throw new Error("Not in a git repository");
    }
    return commonDir;
}
const WORKTREE_PROJECT_HASH_LENGTH = 8;
function deriveShortAlphanumericHash(value) {
    const digest = createHash("sha256").update(value).digest();
    let hashValue = 0n;
    for (let index = 0; index < 8; index += 1) {
        hashValue = (hashValue << 8n) | BigInt(digest[index] ?? 0);
    }
    return hashValue.toString(36).padStart(13, "0").slice(0, WORKTREE_PROJECT_HASH_LENGTH);
}
export async function deriveWorktreeProjectHash(cwd) {
    try {
        const commonDir = await getGitCommonDir(cwd);
        const normalizedCommonDir = normalizePathForOwnership(commonDir);
        const repoRoot = basename(normalizedCommonDir) === ".git" ? dirname(normalizedCommonDir) : normalizedCommonDir;
        return deriveShortAlphanumericHash(repoRoot);
    }
    catch {
        return deriveShortAlphanumericHash(normalizePathForOwnership(cwd));
    }
}
export function resolvePaseoWorktreesBaseRoot(options) {
    if (options?.worktreesRoot) {
        const expandedRoot = expandTilde(options.worktreesRoot);
        if (isAbsolute(expandedRoot)) {
            return resolve(expandedRoot);
        }
        const home = options.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
        return resolve(home, expandedRoot);
    }
    const home = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
    return join(home, "worktrees");
}
export async function getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot) {
    const baseRoot = resolvePaseoWorktreesBaseRoot({ paseoHome, worktreesRoot });
    const projectHash = await deriveWorktreeProjectHash(cwd);
    return join(baseRoot, projectHash);
}
export async function computeWorktreePath(cwd, slug, paseoHome, worktreesRoot) {
    const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
    return join(projectWorktreesRoot, slug);
}
function normalizePathForOwnership(input) {
    try {
        return realpathSync(input);
    }
    catch {
        return resolve(input);
    }
}
function resolveRepoRootFromGitCommonDir(commonDir) {
    const normalizedCommonDir = normalizePathForOwnership(commonDir);
    return basename(normalizedCommonDir) === ".git"
        ? dirname(normalizedCommonDir)
        : normalizedCommonDir;
}
export async function isPaseoOwnedWorktreeCwd(cwd, options) {
    const resolvedCwd = normalizePathForOwnership(cwd);
    // repoRoot is best-effort: git may be unreachable from the worktree (e.g. a
    // previous archive attempt removed the admin dir before the working tree
    // could be fully cleaned up). We still want to allow archiving in that case.
    let repoRoot;
    try {
        const gitCommonDir = await getGitCommonDir(cwd);
        repoRoot = resolveRepoRootFromGitCommonDir(gitCommonDir);
    }
    catch {
        // ignore
    }
    const worktreesBaseRoot = resolvePaseoWorktreesBaseRoot(options);
    const paseoWorktreesPrefix = normalizePathForOwnership(worktreesBaseRoot) + sep;
    // Ownership is defined by the path living under <worktrees-root>/<hash>/<slug>[/...].
    // The <hash>/<slug> prefix is Paseo-private — nothing else writes there — so the
    // path shape alone is sufficient proof of ownership, even when git has already
    // forgotten about the worktree.
    if (!resolvedCwd.startsWith(paseoWorktreesPrefix)) {
        return {
            allowed: false,
            ...(repoRoot !== undefined ? { repoRoot } : {}),
            worktreePath: resolvedCwd,
        };
    }
    const relative = resolvedCwd.slice(paseoWorktreesPrefix.length);
    const parts = relative.split(sep).filter((part) => part.length > 0);
    if (parts.length < 2) {
        return {
            allowed: false,
            ...(repoRoot !== undefined ? { repoRoot } : {}),
            worktreePath: resolvedCwd,
        };
    }
    const worktreesRoot = join(worktreesBaseRoot, parts[0]);
    return {
        allowed: true,
        ...(repoRoot !== undefined ? { repoRoot } : {}),
        worktreeRoot: worktreesRoot,
        worktreePath: resolvedCwd,
    };
}
function parseWorktreeList(output) {
    const entries = [];
    let current = null;
    for (const line of output.split("\n")) {
        if (line.startsWith("worktree ")) {
            if (current?.path) {
                entries.push(current);
            }
            current = { path: line.slice("worktree ".length).trim() };
            continue;
        }
        if (!current) {
            continue;
        }
        if (line.startsWith("branch ")) {
            const ref = line.slice("branch ".length).trim();
            current.branchName = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
        }
        else if (line.startsWith("HEAD ")) {
            current.head = line.slice("HEAD ".length).trim();
        }
        else if (line.trim().length === 0) {
            if (current.path) {
                entries.push(current);
            }
            current = null;
        }
    }
    if (current?.path) {
        entries.push(current);
    }
    return entries;
}
function resolveWorktreeCreatedAtIso(worktreePath) {
    try {
        const stats = statSync(worktreePath);
        const birthtimeMs = stats.birthtimeMs;
        const createdAtMs = Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : stats.ctimeMs;
        return new Date(createdAtMs).toISOString();
    }
    catch {
        return new Date(0).toISOString();
    }
}
export async function listPaseoWorktrees({ cwd, paseoHome, worktreesRoot, }) {
    const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
    const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
        cwd,
        envOverlay: READ_ONLY_GIT_ENV,
    });
    const rootPrefix = normalizePathForOwnership(projectWorktreesRoot) + sep;
    return parseWorktreeList(stdout)
        .map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) }))
        .filter((entry) => entry.path.startsWith(rootPrefix))
        .map((entry) => Object.assign({}, entry, { createdAt: resolveWorktreeCreatedAtIso(entry.path) }));
}
export async function resolveExistingWorktreeForSlug({ slug, repoRoot, paseoHome, worktreesRoot, }) {
    const worktrees = await listPaseoWorktrees({
        cwd: repoRoot,
        paseoHome,
        worktreesRoot,
    });
    const slugSuffix = `${sep}${slug}`;
    const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
    if (!existingWorktree) {
        return null;
    }
    const { stdout } = await runGitCommand(["branch", "--show-current"], {
        cwd: existingWorktree.path,
        envOverlay: READ_ONLY_GIT_ENV,
    });
    const branchName = stdout.trim();
    if (!branchName) {
        throw new Error(`Unable to resolve branch for existing worktree: ${existingWorktree.path}`);
    }
    return {
        branchName,
        worktreePath: existingWorktree.path,
    };
}
export async function resolvePaseoWorktreeRootForCwd(cwd, options) {
    let gitCommonDir;
    try {
        gitCommonDir = await getGitCommonDir(cwd);
    }
    catch {
        return null;
    }
    const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome, options?.worktreesRoot);
    const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
    let worktreeRoot = null;
    try {
        const { stdout } = await runGitCommand(["rev-parse", "--show-toplevel"], {
            cwd,
            envOverlay: READ_ONLY_GIT_ENV,
        });
        worktreeRoot = parseGitRevParsePath(stdout);
    }
    catch {
        worktreeRoot = null;
    }
    if (!worktreeRoot) {
        return null;
    }
    const resolvedWorktreeRoot = normalizePathForOwnership(worktreeRoot);
    if (!resolvedWorktreeRoot.startsWith(resolvedRoot)) {
        return null;
    }
    const knownWorktrees = await listPaseoWorktrees({
        cwd,
        paseoHome: options?.paseoHome,
        worktreesRoot: options?.worktreesRoot,
    });
    const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
    if (!match) {
        return null;
    }
    return {
        repoRoot: gitCommonDir,
        worktreeRoot: worktreesRoot,
        worktreePath: match.path,
    };
}
export async function deletePaseoWorktree({ cwd, worktreePath, worktreeSlug, worktreesRoot, paseoHome, worktreesBaseRoot, }) {
    if (!worktreePath && !worktreeSlug) {
        throw new Error("worktreePath or worktreeSlug is required");
    }
    // Resolve the worktrees-root. With a repo cwd we hash it the normal way; if
    // git has forgotten about the worktree we expect the caller to hand us the
    // path-derived worktreesRoot from the ownership check.
    let resolvedWorktreesRoot;
    if (worktreesRoot) {
        resolvedWorktreesRoot = worktreesRoot;
    }
    else if (cwd) {
        resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesBaseRoot);
    }
    else {
        throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree");
    }
    const resolvedRoot = normalizePathForOwnership(resolvedWorktreesRoot) + sep;
    const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug);
    const resolvedRequested = normalizePathForOwnership(requestedPath);
    const resolvedWorktree = (await resolvePaseoWorktreeRootForCwd(requestedPath, {
        paseoHome,
        worktreesRoot: worktreesBaseRoot,
    }))?.worktreePath ?? resolvedRequested;
    if (!resolvedWorktree.startsWith(resolvedRoot)) {
        throw new Error("Refusing to delete non-Paseo worktree");
    }
    if (await pathExists(resolvedWorktree)) {
        await runWorktreeTeardownCommands({
            worktreePath: resolvedWorktree,
        });
    }
    if (cwd) {
        try {
            await runGitCommand(["worktree", "remove", resolvedWorktree, "--force"], {
                cwd,
                timeout: 120000,
            });
        }
        catch {
            // `git worktree remove` fails if the admin dir is already gone (e.g. a
            // prior archive attempt removed it before the working tree could be
            // fully cleaned up), or if the repo root has moved. Fall through to the
            // rm retry loop below so the operation stays idempotent.
        }
    }
    await removeDirectoryWithRetries(resolvedWorktree);
    if (cwd) {
        try {
            await runGitCommand(["worktree", "prune"], { cwd, timeout: 30000 });
        }
        catch {
            // not critical; git will prune lazily
        }
    }
}
async function pathExists(path) {
    try {
        await stat(path);
        return true;
    }
    catch (error) {
        if (error.code === "ENOENT") {
            return false;
        }
        throw error;
    }
}
async function removeDirectoryWithRetries(path) {
    if (!(await pathExists(path))) {
        return;
    }
    const delaysMs = [0, 100, 300, 700, 1500];
    let lastError = null;
    for (const delay of delaysMs) {
        if (delay > 0) {
            await new Promise((resolvePromise) => setTimeout(resolvePromise, delay));
        }
        try {
            await rm(path, { recursive: true, force: true });
            if (!(await pathExists(path))) {
                return;
            }
            lastError = new Error(`Directory still present after rm: ${path}`);
        }
        catch (error) {
            lastError = error;
        }
    }
    if (await pathExists(path)) {
        throw lastError instanceof Error
            ? lastError
            : new Error(`Failed to remove worktree directory: ${path}`);
    }
}
/**
 * Create a git worktree with proper naming conventions
 */
export const createWorktree = async ({ cwd, source, worktreeSlug, runSetup, paseoHome, worktreesRoot, }) => {
    const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
    let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug);
    mkdirSync(dirname(worktreePath), { recursive: true });
    // Also handle worktree path collision
    let finalWorktreePath = worktreePath;
    let pathSuffix = 1;
    while (existsSync(finalWorktreePath)) {
        finalWorktreePath = `${worktreePath}-${pathSuffix}`;
        pathSuffix++;
    }
    // Primitive owner for `git worktree add`; callers route through createWorktreeCore.
    await runGitCommand(["worktree", "add", finalWorktreePath, ...sourcePlan.addArguments], {
        cwd,
        timeout: 120000,
    });
    worktreePath = normalizePathForOwnership(finalWorktreePath);
    if (sourcePlan.pushRemote) {
        await configureWorktreePushRemote({
            cwd,
            branchName: sourcePlan.branchName,
            remote: sourcePlan.pushRemote,
        });
    }
    writePaseoWorktreeMetadata(worktreePath, { baseRefName: sourcePlan.metadataBaseRefName });
    // If paseo.json exists in the main repo but wasn't checked into the worktree
    // (e.g. uncommitted on first-time setup), seed the worktree with it so setup
    // commands and scripts pick up the user's intended config.
    const mainConfigPath = join(cwd, "paseo.json");
    const worktreeConfigPath = join(worktreePath, "paseo.json");
    try {
        await stat(worktreeConfigPath);
    }
    catch {
        await copyFile(mainConfigPath, worktreeConfigPath).catch((err) => {
            if (err.code !== "ENOENT")
                throw err;
        });
    }
    if (runSetup) {
        await runWorktreeSetupCommands({
            worktreePath,
            branchName: sourcePlan.branchName,
            cleanupOnFailure: true,
        });
    }
    return {
        branchName: sourcePlan.branchName,
        worktreePath,
    };
};
async function resolveWorktreeSourcePlan({ cwd, source, desiredSlug, }) {
    switch (source.kind) {
        case "branch-off": {
            const branchName = source.branchName;
            validateWorktreeBranchName(branchName);
            const normalizedBaseBranch = normalizeRequiredBaseBranch(source.baseBranch);
            const resolvedBaseBranch = await resolveBaseBranchForWorktree(cwd, normalizedBaseBranch);
            const branchExists = await localBranchExists(cwd, branchName);
            const base = branchExists ? branchName : resolvedBaseBranch;
            const candidateBranch = branchExists ? desiredSlug : branchName;
            const newBranchName = await resolveUniqueLocalBranchName(cwd, candidateBranch);
            return {
                branchName: newBranchName,
                metadataBaseRefName: normalizedBaseBranch,
                addArguments: ["-b", newBranchName, "--no-track", base],
            };
        }
        case "checkout-branch": {
            validateWorktreeBranchName(source.branchName);
            if (!(await localBranchExists(cwd, source.branchName))) {
                try {
                    await runGitCommand(["fetch", "origin", `${source.branchName}:${source.branchName}`], {
                        cwd,
                        timeout: 120000,
                    });
                }
                catch {
                    throw new UnknownBranchError({ branchName: source.branchName, cwd });
                }
            }
            if (await isBranchCheckedOut(cwd, source.branchName)) {
                throw new BranchAlreadyCheckedOutError(source.branchName);
            }
            return {
                branchName: source.branchName,
                metadataBaseRefName: source.branchName,
                addArguments: [source.branchName],
            };
        }
        case "checkout-github-pr": {
            const localBranchCandidate = source.localBranchName ?? source.headRef;
            validateWorktreeBranchName(localBranchCandidate);
            const localBranchName = await resolveUniqueLocalBranchName(cwd, localBranchCandidate);
            const normalizedBaseRefName = normalizeRequiredBaseBranch(source.baseRefName);
            await runGitCommand([
                "fetch",
                "origin",
                `refs/pull/${source.githubPrNumber}/head:refs/heads/${localBranchName}`,
                "--force",
            ], {
                cwd,
                timeout: 120000,
            });
            return {
                branchName: localBranchName,
                metadataBaseRefName: normalizedBaseRefName,
                addArguments: [localBranchName],
                ...(source.pushRemoteUrl
                    ? {
                        pushRemote: {
                            name: `paseo-pr-${source.githubPrNumber}`,
                            url: source.pushRemoteUrl,
                            headRef: source.headRef,
                        },
                    }
                    : {}),
            };
        }
    }
}
async function configureWorktreePushRemote(options) {
    await runGitCommand(["config", `remote.${options.remote.name}.url`, options.remote.url], {
        cwd: options.cwd,
    });
    await runGitCommand(["config", `remote.${options.remote.name}.push`, `HEAD:refs/heads/${options.remote.headRef}`], { cwd: options.cwd });
    await runGitCommand(["config", `branch.${options.branchName}.remote`, options.remote.name], {
        cwd: options.cwd,
    });
    await runGitCommand(["config", `branch.${options.branchName}.merge`, `refs/heads/${options.remote.headRef}`], { cwd: options.cwd });
}
function validateWorktreeBranchName(branchName) {
    const validation = validateBranchSlug(branchName);
    if (!validation.valid) {
        throw new Error(`Invalid branch name: ${validation.error}`);
    }
}
function normalizeRequiredBaseBranch(baseBranch) {
    const normalizedBaseBranch = normalizeBaseRefName(baseBranch);
    if (!normalizedBaseBranch) {
        throw new Error("Base branch is required when creating a Paseo worktree");
    }
    if (normalizedBaseBranch === "HEAD") {
        throw new Error("Base branch cannot be HEAD when creating a Paseo worktree");
    }
    return normalizedBaseBranch;
}
async function resolveBaseBranchForWorktree(cwd, normalizedBaseBranch) {
    try {
        await runGitCommand(["rev-parse", "--verify", `origin/${normalizedBaseBranch}`], { cwd });
        return `origin/${normalizedBaseBranch}`;
    }
    catch {
        try {
            await runGitCommand(["rev-parse", "--verify", normalizedBaseBranch], { cwd });
            return normalizedBaseBranch;
        }
        catch {
            throw new Error(`Base branch not found: ${normalizedBaseBranch}`);
        }
    }
}
async function localBranchExists(cwd, branchName) {
    try {
        await runGitCommand(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], {
            cwd,
        });
        return true;
    }
    catch {
        return false;
    }
}
async function resolveUniqueLocalBranchName(cwd, candidateBranch) {
    let newBranchName = candidateBranch;
    let suffix = 1;
    while (await localBranchExists(cwd, newBranchName)) {
        newBranchName = `${candidateBranch}-${suffix}`;
        suffix++;
    }
    return newBranchName;
}
async function isBranchCheckedOut(cwd, branchName) {
    const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
        cwd,
        envOverlay: READ_ONLY_GIT_ENV,
    });
    return parseWorktreeList(stdout).some((entry) => entry.branchName === branchName);
}
//# sourceMappingURL=worktree.js.map