import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import type { SyncDocParams, DocgraphToolDetails } from "../types.js"; import { readFileSafe, writeFileSafe, parseMetadata, validateLinks, isInitialized, contentText, } from "../utils.js"; import { DOC_NAMES } from "../types.js"; export function registerDocgraphSync(pi: ExtensionAPI): void { pi.registerTool({ name: "docgraph_sync", label: "Docgraph Sync", description: "Synchronize a documentation file with the codebase. Validates cross-references, updates metadata timestamps, and reports staleness. Use after making code changes.", parameters: Type.Object({ path: Type.String({ description: "Document path to sync, or 'all' to validate all managed documents", }), dryRun: Type.Optional( Type.Boolean({ description: "If true, only validate without writing" }), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const { path, dryRun } = params as SyncDocParams; if (!isInitialized(ctx)) { return { content: [ { type: "text", text: "Documentation not initialized. Run `docgraph_init` first.", }, ], details: { action: "sync", error: "not_initialized", state: { initialized: false, schemaVersion: 1 }, } as DocgraphToolDetails, }; } const targets: string[] = path === "all" ? [...DOC_NAMES] : [path]; const results: Array<{ path: string; found: boolean; linksBroken: string[]; updated: boolean; }> = []; for (const target of targets) { const raw = readFileSafe(target, ctx.cwd); if (raw === null) { results.push({ path: target, found: false, linksBroken: [], updated: false, }); continue; } const { broken } = validateLinks(raw, target, ctx.cwd); // Update the Last Updated date in metadata const today = new Date().toISOString().slice(0, 10); const metadata = parseMetadata(raw, target); let updated = false; if (metadata.lastUpdated !== today && !dryRun) { // Rewrite the metadata block with the new date const lines = raw.split("\n"); const newLines: string[] = []; let inMeta = false; for (const line of lines) { if (line.startsWith("> **Last Updated:**") && inMeta) { newLines.push(`> **Last Updated:** ${today}`); updated = true; continue; } if (line.startsWith("> **")) inMeta = true; if (inMeta && !line.startsWith(">")) inMeta = false; newLines.push(line); } writeFileSafe(target, newLines.join("\n"), ctx.cwd); } results.push({ path: target, found: true, linksBroken: broken, updated, }); } const missing = results.filter((r) => !r.found); const withBroken = results.filter((r) => r.linksBroken.length > 0); const updatedCount = results.filter((r) => r.updated).length; const lines: string[] = []; lines.push(`Synced ${results.length} document(s).`); if (updatedCount > 0) lines.push(`Updated Last Updated on ${updatedCount} file(s).`); if (withBroken.length > 0) { lines.push(`\nBroken cross-references found:`); for (const r of withBroken) { lines.push(` ${r.path}: ${r.linksBroken.join(", ")}`); } } if (missing.length > 0) { lines.push(`\nMissing documents:`); for (const r of missing) lines.push(` ${r.path}`); } if (dryRun) lines.push("\n(Dry run — no files were modified)"); return { content: [ { type: "text", text: lines.join("\n"), }, ], details: { action: "sync", results, dryRun, state: { initialized: true, schemaVersion: 1 }, } as DocgraphToolDetails & { results: typeof results; dryRun: boolean }, }; }, renderCall(args, theme) { return new Text( theme.fg("toolTitle", theme.bold("docgraph-sync ")) + theme.fg("muted", args.path), 0, 0, ); }, renderResult(result, _opts, theme) { const text = contentText(result.content?.[0]); const firstLine = text.split("\n")[0] ?? ""; return new Text( theme.fg("muted", firstLine), 0, 0, ); }, }); }