Files
harness-tools/extensions/native-hooks/src/initializer.ts
T

22 lines
3.5 KiB
TypeScript

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<string[]> { 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"); }});
}