import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' import { join } from 'node:path' import { promisify } from 'node:util' import { createLogger } from '../shared/log.ts' const exec = promisify(execFile) const log = createLogger({ component: 'worktree' }) export interface WorktreeStatus { head?: string | undefined dirty: boolean diverged: boolean fastForwarded: boolean cloned: boolean error?: string | undefined } /** * Cheap local read of the current worktree state (rev-parse + status; no * fetch, no merge). Used to refresh wake facts between boot syncs so they * never report a stale HEAD. Never throws; failures land in `error`. */ export async function readWorktreeState(workDir: string): Promise { const status: WorktreeStatus = { dirty: false, diverged: false, fastForwarded: false, cloned: false, } try { const git = (...args: string[]) => exec('git', args, { cwd: workDir, maxBuffer: 10 << 20 }) status.head = (await git('rev-parse', 'HEAD')).stdout.trim() const dirty = await git('status', '--porcelain') status.dirty = dirty.stdout.trim().length > 0 } catch (err) { status.error = err instanceof Error ? err.message : String(err) } return status } /** * Startup worktree sync (spec AC15): fetch; fast-forward only when clean; * dirty or divergent work is preserved and reported, never reset. */ export async function syncWorktree( workDir: string, gitUrl: string | undefined, branch: string, ): Promise { const status: WorktreeStatus = { dirty: false, diverged: false, fastForwarded: false, cloned: false, } const git = (cwd: string | undefined, ...args: string[]) => exec('git', args, { cwd, maxBuffer: 10 << 20 }) try { if (!existsSync(join(workDir, '.git'))) { if (!gitUrl) { status.error = 'worktree missing and no git url configured' return status } await exec('git', ['clone', '--branch', branch, gitUrl, workDir], { maxBuffer: 10 << 20 }) status.cloned = true } if (gitUrl) await git(workDir, 'fetch', 'origin', branch).catch(() => undefined) const head = await git(workDir, 'rev-parse', 'HEAD') status.head = head.stdout.trim() const dirty = await git(workDir, 'status', '--porcelain') status.dirty = dirty.stdout.trim().length > 0 if (gitUrl && !status.dirty) { const behind = await git(workDir, 'rev-list', '--count', `HEAD..origin/${branch}`) const ahead = await git(workDir, 'rev-list', '--count', `origin/${branch}..HEAD`) if (Number(behind.stdout.trim()) > 0 && Number(ahead.stdout.trim()) === 0) { await git(workDir, 'merge', '--ff-only', `origin/${branch}`) status.fastForwarded = true status.head = (await git(workDir, 'rev-parse', 'HEAD')).stdout.trim() } else if (Number(ahead.stdout.trim()) > 0) { status.diverged = true } } return status } catch (err) { status.error = err instanceof Error ? err.message : String(err) log.warn('worktree sync failed', { error: status.error }) return status } }