/** * Per-repository broker-root resolution for the file watcher. * * The c2c CLI resolves this itself for every command. Pi needs the same * answer independently because `fs.watch` attaches directly to the inbox * directory, rather than going through the CLI. */ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import * as path from "node:path"; export type BrokerRootSource = "override" | "c2c-state-home" | "home"; export interface PerRepoBrokerRoot { root: string; fingerprint: string; source: BrokerRootSource; } function gitOutput(cwd: string, args: string[]): string | undefined { try { const output = execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); return output || undefined; } catch { return undefined; } } /** Match c2c's fingerprint input: origin URL, then repository top-level. */ export function resolveRepoFingerprint(cwd: string): string { const identity = gitOutput(cwd, ["config", "--get", "remote.origin.url"]) ?? gitOutput(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd; return createHash("sha256").update(identity).digest("hex"); } /** * Match c2c's per-repository precedence for direct inbox watching. Generic * XDG_STATE_HOME is deliberately ignored because harnesses can repurpose it * per profile and thereby split the machine-wide broker. */ export function resolvePerRepoBrokerRoot( cwd: string, env: NodeJS.ProcessEnv = process.env, ): PerRepoBrokerRoot { const fingerprint = resolveRepoFingerprint(cwd); const explicit = (env.C2C_MCP_BROKER_ROOT ?? "").trim(); if (explicit) return { root: explicit, fingerprint, source: "override" }; const stateHome = (env.C2C_STATE_HOME ?? "").trim(); if (stateHome) { return { root: path.join(stateHome, "c2c", "repos", fingerprint, "broker"), fingerprint, source: "c2c-state-home", }; } const home = (env.HOME ?? "").trim() || "."; return { root: path.join(home, ".c2c", "repos", fingerprint, "broker"), fingerprint, source: "home", }; }