import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { LOCATION_SETTINGS, type S3ConfigResolution, s3Config, } from "./config.js"; import { closeDebug, dbg } from "./src/debug.ts"; import { parseBooleanSetting, resolveSetting, type SettingDeclaration, } from "./src/pi-ext-settings.ts"; const SUBCOMMANDS = [ "init", "backup", "restore", "search", "refresh", "status", ]; const AUTO_BACKUP_SETTING: SettingDeclaration = { key: "bak.autoBackup", env: "PI_BAK_AUTO_BACKUP", parse: parseBooleanSetting, default: true, }; type BakRuntime = ReturnType< typeof import("./implementation.js")["createBakRuntime"] >; export default function bakExtension(pi: ExtensionAPI) { // Pi transpiles eager extension imports during startup, so defer backup logic // until an operation actually needs it. let settings: | { resolved: S3ConfigResolution; autoBackup: boolean } | undefined; let runtime: BakRuntime | undefined; let runtimePromise: Promise | undefined; let activeContext: ExtensionContext | undefined; let missingWarningShown = false; // Resolved per session_start; commands before it fall back to one resolution. const loadSettings = (ctx: ExtensionContext) => { if (settings) return settings; const resolve = (declaration: SettingDeclaration): T => { const result = resolveSetting(pi, ctx, declaration); if (result.ok) return result.value; ctx.ui.notify(`bak: ${result.error}; using default`, "warning"); return declaration.default; }; settings = { resolved: s3Config(process.env, { endpoint: resolve(LOCATION_SETTINGS.endpoint), bucket: resolve(LOCATION_SETTINGS.bucket), region: resolve(LOCATION_SETTINGS.region), }), autoBackup: resolve(AUTO_BACKUP_SETTING), }; return settings; }; const loadRuntime = (ctx: ExtensionContext): Promise => { if (runtime) return Promise.resolve(runtime); if (runtimePromise) return runtimePromise; runtimePromise = import("./implementation.js").then( ({ createBakRuntime }) => { runtime = createBakRuntime( loadSettings(ctx).resolved, missingWarningShown, ); runtime.sessionStart(activeContext ?? ctx); return runtime; }, ); return runtimePromise; }; pi.on("session_start", (_event, ctx) => { activeContext = ctx; dbg?.("session.start"); settings = undefined; const { resolved } = loadSettings(ctx); if (!resolved.config && !missingWarningShown) { ctx.ui.notify( `bak: disabled; missing ${resolved.missing.join(", ")}`, "warning", ); missingWarningShown = true; } }); const automaticBackup = async (ctx: ExtensionContext) => { if (loadSettings(ctx).autoBackup) (await loadRuntime(ctx)).automaticBackup(ctx); }; pi.on("agent_settled", (_event, ctx) => automaticBackup(ctx)); pi.on("session_info_changed", (_event, ctx) => automaticBackup(ctx)); pi.on("session_shutdown", async (_event, ctx) => { let closeWhenIdle: (() => void) | undefined; try { const loadedRuntime = await loadRuntime(ctx); await loadedRuntime.sessionShutdown(ctx, loadSettings(ctx).autoBackup); closeWhenIdle = loadedRuntime.closeDebugWhenIdle; } finally { activeContext = undefined; dbg?.("session.shutdown"); if (closeWhenIdle) closeWhenIdle(); else closeDebug(); } }); pi.registerCommand("bak", { description: "Back up and restore Pi sessions", getArgumentCompletions(prefix) { if (runtime) return runtime.command.getArgumentCompletions(prefix); const normalized = prefix.trimStart(); if (normalized.includes(" ")) return null; const items = SUBCOMMANDS.filter((command) => command.startsWith(normalized.toLowerCase()), ).map((command) => ({ value: command, label: command })); return items.length > 0 ? items : null; }, async handler(args, ctx) { await (await loadRuntime(ctx)).command.handler(args, ctx); }, }); }