repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/bak/index.ts
Rawimport 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<boolean> = {
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<BakRuntime> | 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 = <T>(declaration: SettingDeclaration<T>): 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<BakRuntime> => {
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);
},
});
}