import type { TemplateResult } from "lit"; import type { HtmlTemplateTag, PiWebComponentStatus, PiWebInstallationInfo, PiWebPlugin, PiWebStatusMessage, PiWebStatusResponse, PluginRuntimeState, WorkspacePanelTerminal } from "@jmfederico/pi-web/plugin-api"; interface CommandEntry { label: string; command: string; } function runCommandInTerminal(terminal: WorkspacePanelTerminal, label: string, command: string): void { void terminal.runCommand({ title: label, command, open: true, metadata: { "pi.plugin": "updates" }, }).catch((error: unknown) => { console.error(`Updates plugin failed to run "${label}"`, error); }); } // The single command users should run when they do not want to think: if an // update is available, `commands.update` already chains the update and a full // restart; otherwise, when anything is stale, a full restart is enough. function recommendedCommand(status: PiWebStatusResponse): CommandEntry | undefined { const { commands, release, components } = status; if (release.updateAvailable && typeof commands.update === "string" && commands.update !== "") { return { label: "Update & restart everything", command: commands.update }; } const restartNeeded = components.web.stale || components.sessiond.stale || !components.sessiond.available; if (restartNeeded && typeof commands.restart === "string" && commands.restart !== "") { return { label: "Restart everything", command: commands.restart }; } return undefined; } function additionalCommands(status: PiWebStatusResponse, recommended: CommandEntry | undefined): CommandEntry[] { return [ ["Update", status.commands.update], ["Restart all", status.commands.restart], ["Restart Web/UI", status.commands.restartWeb], ["Restart session daemon", status.commands.restartSessiond], ["Status", status.commands.status], ] .filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1] !== "") .filter(([, command]) => command !== recommended?.command) .map(([label, command]) => ({ label, command })); } function messagesFor(state: PluginRuntimeState | undefined): PiWebStatusMessage[] { return state?.piWebStatus?.messages ?? []; } function statusFor(state: PluginRuntimeState | undefined): PiWebStatusResponse | undefined { return state?.piWebStatus; } function messageCount(state: PluginRuntimeState | undefined): number { return messagesFor(state).length; } function isLocalOrUnknownInstallation(installation: PiWebInstallationInfo | undefined): boolean { return installation === undefined || installation.kind === "local" || installation.kind === "unknown"; } function shouldShowUpdatesPanel(state: PluginRuntimeState | undefined): boolean { const status = statusFor(state); if (messageCount(state) > 0) return true; if (status === undefined) return false; return isLocalOrUnknownInstallation(status.components.web.installation) || isLocalOrUnknownInstallation(status.components.sessiond.installation); } function formatVersion(version: string | undefined): string { return version === undefined || version === "" ? "unknown" : version; } function installationLabel(installation: PiWebInstallationInfo | undefined): string { if (installation === undefined) return "installation unknown"; if (installation.kind === "pi-package") { const scope = installation.scope === undefined ? "" : ` · ${installation.scope}`; const source = installation.source ?? "Pi package"; return `${source}${scope}`; } if (installation.kind === "npm-global") return "global npm package"; if (installation.kind === "local") return "local checkout"; return "installation unknown"; } function renderComponent(html: HtmlTemplateTag, component: PiWebComponentStatus): TemplateResult { const status = !component.available ? "unavailable" : component.stale ? "restart needed" : "current"; return html`
${component.label} ${status} running ${formatVersion(component.runtimeVersion)} · installed ${formatVersion(component.installedVersion)} ${installationLabel(component.installation)}${component.installation?.path === undefined ? "" : ` · ${component.installation.path}`}
`; } function renderCommandActions(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, label: string, command: string): TemplateResult { return html` ${terminal === undefined ? null : html``} `; } function renderCommand(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, label: string, command: string): TemplateResult { return html`
${label} ${command} ${renderCommandActions(html, terminal, label, command)}
`; } function renderCommands(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, status: PiWebStatusResponse): TemplateResult | undefined { const recommended = recommendedCommand(status); const additional = additionalCommands(status, recommended); if (recommended === undefined && additional.length === 0) return undefined; return html` ${recommended === undefined ? null : html` `} ${additional.length === 0 ? null : html`
${recommended === undefined ? "Suggested commands" : "Additional commands (optional)"} ${recommended === undefined ? null : html`

Only needed for finer control, such as restarting a single service.

`} ${additional.map((entry) => renderCommand(html, terminal, entry.label, entry.command))}
`} `; } function renderUpdatesPanel(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, state: PluginRuntimeState | undefined): TemplateResult { const status = statusFor(state); if (status === undefined) { return html`
Updates

Checking PI WEB update status…

`; } const messages = status.messages; return html`
Updatesbeta${messages.length > 0 ? html`${String(messages.length)}` : null}
${messages.length === 0 ? html`

No PI WEB update or restart messages.

` : messages.map((message) => html`
${message.title}${message.severity}

${message.body}

${message.command === undefined ? null : html`
${message.command} ${renderCommandActions(html, terminal, message.title, message.command)}
`}
`)}
Installed services ${renderComponent(html, status.components.web)} ${renderComponent(html, status.components.sessiond)}
${renderCommands(html, terminal, status)}
Generated ${status.generatedAt} ${status.release.latestVersion === undefined ? null : html`Latest npm release ${status.release.latestVersion}`} ${status.release.skipped === true ? html`Remote version check skipped.` : null} ${status.release.error === undefined ? null : html`Remote version check failed: ${status.release.error}`}
`; } const plugin: PiWebPlugin = { apiVersion: 1, name: "Updates", activate: ({ html, svg }) => ({ contributions: { workspacePanels: [ { id: "workspace.updates", title: "Updates", icon: svg` `, order: 100, visible: (context) => shouldShowUpdatesPanel(context.state), badge: (context) => { const count = messageCount(context.state); return html`beta${count > 0 ? html` · ${String(count)}` : null}`; }, render: (context) => renderUpdatesPanel(html, context.terminal, context.state), }, ], }, }), }; export default plugin;