import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { chmod, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { userInfo } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { loadPolicies, personalConfigRoot } from "./policy-loader.ts"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const templateRoot = join(packageRoot, "examples", "personal-config"); async function filesUnder(root: string, prefix = ""): Promise { const result: string[] = []; for (const entry of await readdir(join(root, prefix), { withFileTypes: true })) { const path = join(prefix, entry.name); if (entry.isDirectory()) result.push(...await filesUnder(root, path)); else if (entry.isFile()) result.push(path); } return result.sort(); } export function registerInitializationCommands(pi: ExtensionAPI): void { pi.registerCommand("hooks-init", { description: "Preview or initialize trusted personal native-hook policies", handler: async (args, ctx) => { const root = personalConfigRoot(); const files = await filesUnder(templateRoot); const plan = [`Personal config root: ${root}`, "Trusted executable files to create (existing files are preserved):", ...files.map((path) => `- ${join(root, path)}`)].join("\n"); if (args.trim() === "--dry-run" || !args.trim()) { ctx.ui.notify(`${plan}\n\nRun /hooks-init --apply to request authorization.`, "info"); return; } if (args.trim() !== "--apply") { ctx.ui.notify("Usage: /hooks-init --dry-run | --apply", "warning"); return; } if (!ctx.hasUI || !await ctx.ui.confirm("Initialize pi-native-hooks?", `${plan}\n\nThese policies execute with Pi's authority. Continue?`)) { ctx.ui.notify("Initialization cancelled.", "info"); return; } const created: string[] = []; try { for (const path of files) { const target = join(root, path); try { await stat(target); continue; } catch {} await mkdir(dirname(target), { recursive: true, mode: 0o700 }); if (path === "tsconfig.json") { const template = await readFile(join(templateRoot, path), "utf8"); await writeFile(target, template.replaceAll("__PI_NATIVE_HOOKS_PACKAGE_ROOT__", packageRoot), { mode: 0o600 }); } else await copyFile(join(templateRoot, path), target); await chmod(target, 0o600); created.push(target); } ctx.ui.notify(`Created ${created.length} file(s); preserved ${files.length - created.length} existing file(s). Run /hooks-doctor then /reload.`, "info"); } catch (error) { for (const path of created.reverse()) await rm(path, { force: true }).catch(() => {}); ctx.ui.notify(`Initialization failed and newly created files were rolled back: ${error instanceof Error ? error.message : String(error)}`, "error"); } }}); pi.registerCommand("hooks-doctor", { description: "Check native-hooks config trust and ast-grep availability", handler: async (_args, ctx) => { const registry = await loadPolicies(); const version = await pi.exec("ast-grep", ["--version"], { timeout: 5000 }); const lines = [registry.trustedCodeWarning, `Config: ${registry.root}`, `Policies: ${registry.policies.length}`, `Failures: ${registry.failures.length}`, ...registry.failures.map((failure) => `- ${failure.path}: ${failure.error}`), `ast-grep: ${version.code === 0 ? version.stdout.trim() : `unavailable (${version.stderr.trim()})`}`, `OS user: ${userInfo().username} uid ${userInfo().uid}`]; ctx.ui.notify(lines.join("\n"), registry.failures.length || version.code !== 0 ? "warning" : "info"); }}); }