Files
harness-tools/extensions/update/index.ts
T
2026-08-22 20:11:11 -04:00

97 lines
3.2 KiB
TypeScript

import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { UpdateFailure, updatePiAndExtensions, type UpdateReporter, type UpdateResult } from "./updater.ts";
const STATUS_KEY = "pi-update";
function conciseError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
const lines = message.split(/\r?\n/).filter(Boolean);
return lines.slice(Math.max(0, lines.length - 12)).join("\n");
}
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
if (ctx.hasUI) {
ctx.ui.notify(message, level);
} else if (level === "error") {
console.error(message);
} else if (level === "warning") {
console.warn(message);
} else {
console.log(message);
}
}
function makeReporter(ctx: ExtensionContext): UpdateReporter {
return {
info(message) {
ctx.ui.setStatus(STATUS_KEY, message);
notify(ctx, message, "info");
},
retry(message) {
ctx.ui.setStatus(STATUS_KEY, message);
notify(ctx, message, "warning");
},
};
}
function formatResult(result: UpdateResult): string {
const versionChange =
result.beforeVersion === result.afterVersion
? `Pi remains at v${result.afterVersion}`
: `Pi updated: v${result.beforeVersion} → v${result.afterVersion}`;
const extensionStatus = result.extensionsChecked
? "Configured Pi extension packages were checked successfully."
: "Extension package checking did not complete.";
return `${versionChange}\nInstall method: ${result.method}\n${extensionStatus}\nRestart Pi to load updated code.`;
}
export default function updateExtension(pi: ExtensionAPI): void {
let activeUpdate: Promise<UpdateResult> | undefined;
let updateFlagHandled = false;
pi.registerFlag("update", {
description: "Update Pi and all configured extension packages, then exit",
type: "boolean",
default: false,
});
async function runUpdate(ctx: ExtensionContext): Promise<UpdateResult> {
if (!activeUpdate) {
ctx.ui.setStatus(STATUS_KEY, "Preparing Pi update");
activeUpdate = updatePiAndExtensions(pi, makeReporter(ctx), {
projectTrusted: ctx.isProjectTrusted(),
}).finally(() => {
ctx.ui.setStatus(STATUS_KEY, undefined);
activeUpdate = undefined;
});
}
return activeUpdate;
}
async function handleUpdate(ctx: ExtensionContext, exitAfterUpdate: boolean): Promise<void> {
try {
const result = await runUpdate(ctx);
notify(ctx, formatResult(result), "info");
if (exitAfterUpdate || result.requiresShutdown) ctx.shutdown();
} catch (error) {
notify(ctx, `Update failed\n${conciseError(error)}`, "error");
if (exitAfterUpdate) process.exitCode = 1;
if (exitAfterUpdate || (error instanceof UpdateFailure && error.requiresShutdown)) ctx.shutdown();
}
}
pi.registerCommand("update", {
description: "Update Pi and all configured extension packages",
handler: async (_args: string, ctx: ExtensionCommandContext) => {
await ctx.waitForIdle();
await handleUpdate(ctx, false);
},
});
pi.on("session_start", async (_event, ctx) => {
if (updateFlagHandled || pi.getFlag("update") !== true) return;
updateFlagHandled = true;
await handleUpdate(ctx, true);
});
}