import { access } from "node:fs/promises"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); export type WorktreeContext = { repoRoot: string; worktreePath: string; branch: string; }; async function pathExists(path: string): Promise { try { await access(path); return true; } catch { return false; } } /** Reuse an existing worktree directory when present (crash resume without re-clone). */ export async function reuseWorktreeIfPresent( repoRoot: string, worktreePath: string, branch: string | null, ): Promise { if (!(await pathExists(worktreePath))) { return null; } try { const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: worktreePath, }); if (stdout.trim() !== worktreePath && !(await pathExists(join(worktreePath, ".git")))) { return null; } } catch { return null; } let resolvedBranch = branch ?? "HEAD"; if (!branch) { try { const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath, }); resolvedBranch = stdout.trim() || "HEAD"; } catch { resolvedBranch = "HEAD"; } } return { repoRoot, worktreePath, branch: resolvedBranch }; } export async function ensureWorktree( repoRoot: string, worktreeBase: string, worktreeKey: string, branchSuffix: string, opts?: { needsWorktree?: boolean }, ): Promise { if (opts?.needsWorktree === false) { return null; } const branch = `worker/${branchSuffix}`.replace(/[^a-zA-Z0-9/_-]/g, "-"); const worktreePath = join(worktreeBase, worktreeKey); await mkdir(worktreeBase, { recursive: true }); const existing = await reuseWorktreeIfPresent(repoRoot, worktreePath, branch); if (existing) { return existing; } try { await execFileAsync("git", ["worktree", "add", "-B", branch, worktreePath], { cwd: repoRoot, }); } catch { await execFileAsync("git", ["worktree", "add", worktreePath, branch], { cwd: repoRoot, }); } return { repoRoot, worktreePath, branch }; } export async function removeWorktree(repoRoot: string, worktreePath: string): Promise { try { await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, }); } catch { // Best-effort cleanup; operator can prune stale worktrees manually. } }