Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/bak/index.ts

Raw
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<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);
		},
	});
}