import { execFile } from "node:child_process"; import { realpath } from "node:fs/promises"; import { z } from "zod"; export async function command(args: string[], cwd: string, signal: AbortSignal) { return new Promise((resolve, reject) => { execFile( args[0]!, args.slice(1), { cwd, signal, timeout: 30_000, maxBuffer: 2_000_000 }, (error, stdout, stderr) => { if (error) reject(new Error(`${args[0]} failed: ${stderr || error.message}`)); else resolve(stdout.trim()); }, ); }); } const inventory = z.object({ result: z.object({ source: z.object({ repo_key: z.string(), repo_root: z.string(), source_checkout_path: z.string(), source_workspace_id: z.string(), }), worktrees: z.array( z.object({ path: z.string(), is_linked_worktree: z.boolean(), is_bare: z.boolean(), open_workspace_id: z.string().nullish(), }), ), }), }); const workspaces = z.object({ result: z.object({ workspaces: z.array(z.object({ workspace_id: z.string() })) }), }); const created = z.object({ result: z.object({ workspace: z.object({ workspace_id: z.string() }), worktree: z.object({ path: z.string(), branch: z.string(), is_linked_worktree: z.literal(true), }), }), }); export const rootSchema = z.object({ directory: z.string(), workspaceID: z.string(), repo: z.string(), }); export type HerdrRoot = z.infer; // The command seam also lets tests use an isolated CLI without touching a live server. export class Herdr { constructor(private readonly run = command) {} async root(directory: string, signal: AbortSignal): Promise { signal = AbortSignal.any([signal, AbortSignal.timeout(5_000)]); const canonical = await realpath(directory); const list = workspaces.parse( JSON.parse(await this.run(["herdr", "workspace", "list"], canonical, signal)), ); const matches: HerdrRoot[] = []; for (const workspace of list.result.workspaces) { let value: z.infer; try { value = inventory.parse( JSON.parse( await this.run( ["herdr", "worktree", "list", "--workspace", workspace.workspace_id], canonical, signal, ), ), ); } catch (error) { if (signal.aborted) throw error; // Non-repository workspaces cannot provide a worktree inventory. continue; } const source = value.result.source; if (source.source_workspace_id !== workspace.workspace_id) continue; if ( (await realpath(source.source_checkout_path)) !== canonical || (await realpath(source.repo_root)) !== canonical ) continue; const checkout = value.result.worktrees.find( (item) => item.open_workspace_id === workspace.workspace_id && !item.is_linked_worktree && !item.is_bare, ); if (!checkout || (await realpath(checkout.path)) !== canonical) continue; const repo = await realpath(source.repo_key); const gitRepo = await this.run( ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], canonical, signal, ); if ((await realpath(gitRepo)) !== repo) continue; matches.push({ directory: canonical, workspaceID: workspace.workspace_id, repo }); } // Never guess which Herdr workspace owns a duplicated checkout. return matches.length === 1 ? matches[0] : undefined; } async create( root: HerdrRoot, base: string, branch: string, signal: AbortSignal, label?: string, ) { const current = await this.root(root.directory, signal); if (!current || current.workspaceID !== root.workspaceID || current.repo !== root.repo) throw new Error("Herdr root workspace is no longer eligible"); const ref = await this.run( ["git", "rev-parse", "--symbolic-full-name", "--verify", "--end-of-options", base], root.directory, signal, ); if (!ref.startsWith("refs/heads/") && !ref.startsWith("refs/remotes/")) throw new Error( "base must identify a locally available branch or remote-tracking branch", ); const commit = await this.run( ["git", "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], root.directory, signal, ); await this.run(["git", "check-ref-format", "--branch", branch], root.directory, signal); if ( await this.run( ["git", "branch", "--list", "--format=%(refname)", branch], root.directory, signal, ) ) throw new Error(`Destination branch already exists: ${branch}`); const response = created.parse( JSON.parse( await this.run( [ "herdr", "worktree", "create", // Pin the repository even if a user repoints the workspace after discovery. "--cwd", root.directory, "--branch", branch, "--base", commit, ...(label === undefined ? [] : ["--label", label]), "--no-focus", ], root.directory, signal, ), ), ); const directory = await realpath(response.result.worktree.path); const workspaceID = response.result.workspace.workspace_id; try { if ( response.result.worktree.branch !== branch || directory === root.directory || workspaceID === root.workspaceID ) throw new Error("Herdr returned an unexpected checkout"); const actual = await this.run(["git", "rev-parse", "HEAD"], directory, signal); const actualBranch = await this.run( ["git", "symbolic-ref", "--short", "HEAD"], directory, signal, ); const repo = await this.run( ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], directory, signal, ); if ( actual !== commit || actualBranch !== branch || (await realpath(repo)) !== root.repo ) throw new Error( "Created checkout does not match the requested repository and base", ); await this.verify(directory, workspaceID, root, branch, signal); } catch (error) { throw new Error( `Retained Herdr workspace ${workspaceID}, checkout ${directory}, branch ${branch}`, { cause: error }, ); } return { directory, workspaceID, branch }; } async verify( directory: string, workspaceID: string, root: HerdrRoot, branch: string, signal: AbortSignal, ) { const value = inventory.parse( JSON.parse( await this.run( ["herdr", "worktree", "list", "--workspace", workspaceID], directory, signal, ), ), ); const checkout = value.result.worktrees.find( (item) => item.open_workspace_id === workspaceID && item.is_linked_worktree, ); const sourceCheckout = value.result.worktrees.find( (item) => item.open_workspace_id === value.result.source.source_workspace_id && !item.is_linked_worktree && !item.is_bare, ); if ( !checkout || !sourceCheckout || value.result.source.source_workspace_id !== root.workspaceID || (await realpath(value.result.source.source_checkout_path)) !== root.directory || (await realpath(sourceCheckout.path)) !== root.directory || (await realpath(checkout.path)) !== directory || (await realpath(value.result.source.repo_key)) !== root.repo ) throw new Error("Bound Herdr checkout is no longer available"); if ( (await this.run(["git", "symbolic-ref", "--short", "HEAD"], directory, signal)) !== branch ) throw new Error("Bound Herdr checkout changed branches"); } }