Files
paseo/packages/cli/src/commands/plugin/update.ts
T
Mohamed Boudra 047f40e623 Install npm plugins and review source updates (#4975)
* feat(plugins): unify source installation and simplify plugin settings

* feat(plugins): install npm sources and review CLI updates

Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm.

* fix(plugins): preserve CLI diagnostics and exercise reviewed updates

Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes.

* test(plugins): remove unused pending helper

* test(plugins): pack registry fixtures with the Windows command runner

* test(plugins): assert failed client contribution setup

* docs(plugins): consolidate guides and document npm publishing

* docs(plugins): introduce publishing choices before npm instructions

* test(plugins): own browser journey cleanup in a fixture

* feat(plugins): prepare scaffolded packages for publishing
2026-09-17 10:22:20 +02:00

103 lines
3.5 KiB
TypeScript

import type {
PluginUpdatePreview,
PluginUpdateResult,
PluginUpdateSelection,
PluginUpdateProposal,
} from "@getpaseo/protocol/messages";
interface UpdateClient {
previewPluginUpdates(input: {
pluginId?: string;
target?: PluginUpdateSelection;
}): Promise<PluginUpdatePreview[]>;
applyPluginUpdates(proposals: PluginUpdateProposal[]): Promise<PluginUpdateResult[]>;
}
export interface UpdateOptions {
pluginId?: string;
all?: boolean;
ref?: string;
version?: string;
check?: boolean;
yes?: boolean;
}
export interface UpdateInteraction {
interactive: boolean;
structured: boolean;
write(text: string): void;
confirm(message: string): Promise<boolean>;
}
export interface UpdateOutcome {
id: string;
outcome: PluginUpdatePreview["outcome"] | PluginUpdateResult["outcome"] | "declined";
current?: PluginUpdatePreview["current"];
target?: PluginUpdatePreview["target"];
links?: string[];
proposal?: PluginUpdateProposal;
plugin?: PluginUpdateResult["plugin"];
error?: string;
warning?: string;
}
export async function reviewPluginUpdates(
client: UpdateClient,
options: UpdateOptions,
interaction: UpdateInteraction,
): Promise<UpdateOutcome[]> {
if ((options.pluginId === undefined) === (options.all !== true))
throw new Error("Choose one plugin ID or pass --all");
if (options.ref && options.version) throw new Error("Choose --ref or --version, not both");
let target: PluginUpdateSelection | undefined;
if (options.ref) target = { kind: "git", ref: options.ref };
if (options.version) target = { kind: "npm", version: options.version };
if (target && options.all)
throw new Error("An explicit target requires one plugin ID, without --all");
const preview = await client.previewPluginUpdates({ pluginId: options.pluginId, target });
if (!interaction.structured) interaction.write(formatUpdatePreview(preview));
if (options.check) return preview;
const proposals = preview.flatMap((item) => (item.proposal ? [item.proposal] : []));
if (!proposals.length) return preview;
if (!options.yes && !target) {
if (interaction.structured || !interaction.interactive)
throw new Error("Confirmation required; rerun with --yes.");
if (
!(await interaction.confirm(
options.all ? "Update these plugins? [y/N] " : `Update ${options.pluginId}? [y/N] `,
))
) {
return preview.map((item) => (item.proposal ? { id: item.id, outcome: "declined" } : item));
}
}
const applied = new Map(
(await client.applyPluginUpdates(proposals)).map((item) => [item.id, item]),
);
return preview.map((item) => applied.get(item.id) ?? item);
}
export function formatUpdatePreview(items: PluginUpdatePreview[]): string {
return items
.map((item) => {
const current = item.current?.currentRevision ?? "unknown";
const target = item.target?.kind === "git" ? item.target.commit : item.target?.version;
let text: string;
switch (item.outcome) {
case "update":
text = `${current} → ${target}`;
break;
case "current":
text = `up to date (${current})`;
break;
case "installed-newer":
text = `installed ${current} is newer than latest ${target}; keeping installed version`;
break;
case "local":
text = "local directory; use Reload after editing";
break;
case "error":
text = item.error ?? "Check failed";
break;
}
return `${item.id}: ${text}\n${item.links.map((link) => `Review: ${link}\n`).join("")}`;
})
.join("\n");
}