From 25defbafb4a9f67d633a850cff9825bf279f800f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 3 Sep 2026 13:22:21 +0200 Subject: [PATCH] Make plugin upgrade failures actionable (#4265) * fix(plugins): clarify upgrade diagnostics Keep plugin inspection network-free and consolidate runtime, source, and installed revision details under plugin ls. Legacy entry failures now explain the required v0.8 migration in user-facing language. * test(cli): align hidden plugin status command --- docs/plugins.md | 6 +-- packages/cli/src/cli-surface.test.ts | 2 +- .../cli/src/commands/plugin/index.test.ts | 52 ++++++++++++++++++- packages/cli/src/commands/plugin/index.ts | 42 +++++---------- .../src/server/plugins/index.posix.test.ts | 12 +++++ .../src/server/plugins/runtime.posix.test.ts | 7 +-- packages/server/src/server/plugins/runtime.ts | 2 +- public-docs/cli.md | 8 +-- public-docs/plugins/v0.7/index.md | 3 +- public-docs/plugins/v0.7/reference.md | 1 - public-docs/plugins/v0.8/index.md | 4 +- public-docs/plugins/v0.8/migration.md | 2 +- public-docs/plugins/v0.8/reference.md | 8 +-- 13 files changed, 95 insertions(+), 54 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index b1051c3a7f..2b523a7c1c 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -91,7 +91,7 @@ paseo plugin add https://gitlab.com/group/repository.git paseo plugin add https://git.example.com/owner/repository.git paseo plugin add owner/monorepo:plugins/review paseo plugin add owner/repository --ref main -paseo plugin status +paseo plugin ls paseo plugin update review paseo plugin update --all ``` @@ -99,8 +99,8 @@ paseo plugin update --all Append `:relative/path` to the source when the plugin lives below the repository root. Omitting `--ref` tracks the remote's default branch. A branch passed with `--ref` also tracks; -tags and commits stay pinned. `status` fetches tracked refs and reports the installed and available -commits. Removing a Git source deletes Paseo's managed checkout. +tags and commits stay pinned. `ls` reports the installed commit without contacting the remote. +Removing a Git source deletes Paseo's managed checkout. ### Declare Git preparation diff --git a/packages/cli/src/cli-surface.test.ts b/packages/cli/src/cli-surface.test.ts index 3243529c23..676f79234d 100644 --- a/packages/cli/src/cli-surface.test.ts +++ b/packages/cli/src/cli-surface.test.ts @@ -79,9 +79,9 @@ describe("canonical CLI surface", () => { expect(plugin?.commands.map((command) => command.name())).toEqual([ "init", "ls", + "status", "logs", "install", - "status", "update", "reload", "enable", diff --git a/packages/cli/src/commands/plugin/index.test.ts b/packages/cli/src/commands/plugin/index.test.ts index 11e80480f4..02c6c846ce 100644 --- a/packages/cli/src/commands/plugin/index.test.ts +++ b/packages/cli/src/commands/plugin/index.test.ts @@ -1,6 +1,23 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const listPlugins = vi.fn(async () => []); +const listPlugins = vi.fn(async () => [ + { + id: "git-plugin", + path: "/plugins/git-plugin", + enabled: true, + status: "running" as const, + source: "git" as const, + commit: "1557a34c91e2abcdef", + ref: "main", + }, + { + id: "legacy-plugin", + path: "/plugins/legacy-plugin", + enabled: true, + status: "failed" as const, + error: "This plugin was made for an older version of Paseo", + }, +]); const getPluginLogs = vi.fn(async () => [ { sequence: 1, @@ -51,7 +68,7 @@ describe("plugin management commands", () => { }); it("requires host support before attempting a management RPC", async () => { - await expect(runPluginListCommand({}, {} as never)).rejects.toMatchObject({ + await expect(runPluginListCommand(undefined, {}, {} as never)).rejects.toMatchObject({ code: "DAEMON_UPDATE_REQUIRED", message: "Update the host to use plugin management.", }); @@ -59,6 +76,37 @@ describe("plugin management commands", () => { expect(close).toHaveBeenCalledTimes(1); }); + it("keeps status as a hidden alias for the network-free plugin list", async () => { + features.pluginManagement = true; + const command = createPluginCommand(); + + await command.parseAsync(["status"], { from: "user" }); + + expect(listPlugins).toHaveBeenCalledTimes(1); + expect(command.helpInformation()).not.toContain("status"); + }); + + it("lists runtime state and the installed commit without an upstream commit", async () => { + features.pluginManagement = true; + + const result = await runPluginListCommand(undefined, {}, {} as never); + const output = render(result, { noColor: true }); + + expect(output).toContain("SOURCE"); + expect(output).toContain("COMMIT"); + expect(output).not.toContain("LATEST"); + expect(output).toContain("1557a34c91e2"); + expect(output).toContain("This plugin was made for an older version of Paseo"); + }); + + it("filters the shared ls and status command by plugin ID", async () => { + features.pluginManagement = true; + + const result = await runPluginListCommand("legacy-plugin", {}, {} as never); + + expect(result.data.map((plugin) => plugin.id)).toEqual(["legacy-plugin"]); + }); + it("requires plugin log support before attempting the RPC", async () => { await expect(runPluginLogsCommand("example", {}, {} as never)).rejects.toMatchObject({ code: "DAEMON_UPDATE_REQUIRED", diff --git a/packages/cli/src/commands/plugin/index.ts b/packages/cli/src/commands/plugin/index.ts index 016938d947..608a57e5d1 100644 --- a/packages/cli/src/commands/plugin/index.ts +++ b/packages/cli/src/commands/plugin/index.ts @@ -3,7 +3,6 @@ import path from "node:path"; import type { PluginListItem, PluginLogEntry, - PluginSourceStatusItem, PluginSourceUpdateItem, } from "@getpaseo/protocol/messages"; import { @@ -34,6 +33,9 @@ const pluginSchema: OutputSchema = { { header: "PLUGIN", field: "id", width: 20 }, { header: "STATUS", field: "status", width: 10 }, { header: "ENABLED", field: (plugin) => (plugin.enabled ? "yes" : "no"), width: 8 }, + { header: "SOURCE", field: (plugin) => plugin.source ?? "directory", width: 10 }, + { header: "COMMIT", field: (plugin) => shortCommit(plugin.commit), width: 14 }, + { header: "REF", field: (plugin) => plugin.ref ?? "-", width: 24 }, { header: "DIRECTORY", field: "path", width: 40 }, { header: "ERROR", field: (plugin) => plugin.error ?? "", width: 40 }, ], @@ -60,18 +62,6 @@ function shortCommit(commit: string | undefined): string { return commit?.slice(0, 12) ?? "-"; } -const pluginStatusSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "PLUGIN", field: "id", width: 20 }, - { header: "SOURCE", field: "source", width: 10 }, - { header: "CURRENT", field: (plugin) => shortCommit(plugin.currentCommit), width: 14 }, - { header: "LATEST", field: (plugin) => shortCommit(plugin.latestCommit), width: 14 }, - { header: "COMMITS", field: (plugin) => String(plugin.commitsBehind ?? 0), width: 8 }, - { header: "REF", field: (plugin) => plugin.ref ?? "-", width: 24 }, - ], -}; - const pluginUpdateSchema: OutputSchema = { idField: "id", columns: [ @@ -96,10 +86,13 @@ export async function runPluginInitCommand( } export async function runPluginListCommand( + pluginId: string | undefined, options: PluginOptions, _command: Command, ): Promise> { - const data = await withPluginManagementClient(options.host, (client) => client.listPlugins()); + const plugins = await withPluginManagementClient(options.host, (client) => client.listPlugins()); + const data = pluginId ? plugins.filter((plugin) => plugin.id === pluginId) : plugins; + if (pluginId && data.length === 0) throw new Error(`Plugin is not configured: ${pluginId}`); return { type: "list", data, schema: pluginSchema }; } @@ -146,17 +139,6 @@ async function install( return { type: "single", data, schema: pluginSchema }; } -async function status( - pluginId: string | undefined, - options: PluginOptions, - _command: Command, -): Promise> { - const data = await withPluginSourceClient(options.host, (client) => - client.getPluginSourceStatus(pluginId), - ); - return { type: "list", data, schema: pluginStatusSchema }; -} - async function update( pluginId: string | undefined, options: PluginOptions, @@ -205,7 +187,10 @@ export function createPluginCommand(): Command { .argument("") .option("--id ", "Manifest plugin ID (defaults to the directory name)"), ).action(withOutput(runPluginInitCommand)); - addJsonAndDaemonHostOptions(plugin.command("ls").description("List configured plugins")).action( + addJsonAndDaemonHostOptions( + plugin.command("ls").description("List configured plugins").argument("[id]"), + ).action(withOutput(runPluginListCommand)); + addJsonAndDaemonHostOptions(plugin.command("status", { hidden: true }).argument("[id]")).action( withOutput(runPluginListCommand), ); addJsonAndDaemonHostOptions( @@ -221,13 +206,10 @@ export function createPluginCommand(): Command { .option("--ref ", "Git branch, tag, or commit") .option("--path ", "Legacy form of the :plugin/path source suffix"), ).action(withOutput(install)); - addJsonAndDaemonHostOptions( - plugin.command("status").description("Check plugin source updates").argument("[id]"), - ).action(withOutput(status)); addJsonAndDaemonHostOptions( plugin .command("update") - .description("Update a Git-managed plugin") + .description("Fetch and install Git-managed plugin updates") .argument("[id]") .option("--all", "Update every Git-managed plugin"), ).action(withOutput(update)); diff --git a/packages/server/src/server/plugins/index.posix.test.ts b/packages/server/src/server/plugins/index.posix.test.ts index 33d790a59b..e90663fb72 100644 --- a/packages/server/src/server/plugins/index.posix.test.ts +++ b/packages/server/src/server/plugins/index.posix.test.ts @@ -684,6 +684,9 @@ export default function contribute(plugin: unknown) { await writeFile(path.join(invalid, "paseo-plugin.json"), JSON.stringify({})); const missingEntry = await createPlugin("missing-entry", "export default () => () => {};"); await rm(path.join(missingEntry, "index.server.ts")); + const legacy = await createPlugin("legacy-plugin", "export default () => () => {};"); + await rm(path.join(legacy, "index.server.ts")); + await writeFile(path.join(legacy, "index.ts"), "export default () => () => {};"); const startupFailure = await createPlugin( "startup-failure", `export default function contribute(plugin: unknown) { void plugin; throw new Error("startup exploded"); }`, @@ -695,10 +698,19 @@ export default function contribute(plugin: unknown) { await expect(service.installDirectory({ path: missingEntry })).rejects.toThrow( "Plugin entry points are missing", ); + await expect(service.installDirectory({ path: legacy })).rejects.toThrow( + "This plugin was made for an older version of Paseo and cannot run on Paseo v0.8. Ask its author to update it. Plugin authors can follow the migration guide: https://paseo.sh/docs/plugins/v0.8/migration", + ); await expect(service.installDirectory({ path: startupFailure })).rejects.toThrow( "startup exploded", ); expect(service.listPlugins()).toEqual([ + expect.objectContaining({ + id: "legacy-plugin", + status: "failed", + error: + "This plugin was made for an older version of Paseo and cannot run on Paseo v0.8. Ask its author to update it. Plugin authors can follow the migration guide: https://paseo.sh/docs/plugins/v0.8/migration", + }), expect.objectContaining({ id: "missing-entry", status: "failed", diff --git a/packages/server/src/server/plugins/runtime.posix.test.ts b/packages/server/src/server/plugins/runtime.posix.test.ts index 6980a6f7e3..021f695ff7 100644 --- a/packages/server/src/server/plugins/runtime.posix.test.ts +++ b/packages/server/src/server/plugins/runtime.posix.test.ts @@ -494,7 +494,7 @@ export default function contribute(plugin: unknown) { await runtime.stopAll(); }); - it("rejects an index.ts plugin with the migration path", async () => { + it("explains that an index.ts plugin was made for an older Paseo version", async () => { const directory = await mkdtemp(path.join(tmpdir(), "paseo-plugin-")); temporaryDirectories.push(directory); await Promise.all([ @@ -504,10 +504,7 @@ export default function contribute(plugin: unknown) { const runtime = createTestRuntime(); await expect(runtime.startPlugin("legacy", directory)).rejects.toThrow( - "Plugin entry split is required", - ); - await expect(runtime.startPlugin("legacy", directory)).rejects.toThrow( - "https://paseo.sh/docs/plugins/v0.8/migration", + "This plugin was made for an older version of Paseo and cannot run on Paseo v0.8. Ask its author to update it. Plugin authors can follow the migration guide: https://paseo.sh/docs/plugins/v0.8/migration", ); }); diff --git a/packages/server/src/server/plugins/runtime.ts b/packages/server/src/server/plugins/runtime.ts index 11951ecad3..1afb0655e5 100644 --- a/packages/server/src/server/plugins/runtime.ts +++ b/packages/server/src/server/plugins/runtime.ts @@ -207,7 +207,7 @@ async function resolveEntryPaths(directory: string): Promise<{ const legacyEntry = await findEntry(directory, ["index.ts", "index.tsx"]); if (legacyEntry) { throw new Error( - "Plugin entry split is required: migrate index.ts to index.client.tsx and/or index.server.ts. See https://paseo.sh/docs/plugins/v0.8/migration", + "This plugin was made for an older version of Paseo and cannot run on Paseo v0.8. Ask its author to update it. Plugin authors can follow the migration guide: https://paseo.sh/docs/plugins/v0.8/migration", ); } throw new Error( diff --git a/public-docs/cli.md b/public-docs/cli.md index e0ec925bfe..10065793de 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -154,10 +154,9 @@ paseo plugin install /absolute/path/to/plugin paseo plugin add owner/repository paseo plugin add https://gitlab.com/group/repository.git --ref main paseo plugin add owner/monorepo:plugins/review -paseo plugin status +paseo plugin ls [id] paseo plugin update my-plugin paseo plugin update --all -paseo plugin ls paseo plugin reload my-plugin paseo plugin logs my-plugin paseo plugin disable my-plugin @@ -166,8 +165,9 @@ paseo plugin remove my-plugin ``` GitHub shorthand checks an existing host directory first. Append `:` for a plugin in a -monorepo. `paseo plugin logs ` returns the plugin's recent daemon-side stdout and stderr. Add `--json` for -structured entries, or run `paseo --host plugin logs ` for another daemon. See the +monorepo. `paseo plugin ls [id]` does not contact the remote. `paseo plugin logs ` returns the +plugin's recent daemon-side stdout and stderr. Add `--json` for structured entries, or run +`paseo --host plugin logs ` for another daemon. See the [Plugin reference](/docs/plugins/v0.7/reference) for installation, trust, lifecycle, and log-retention behavior. diff --git a/public-docs/plugins/v0.7/index.md b/public-docs/plugins/v0.7/index.md index dafe44620a..618f7b7b31 100644 --- a/public-docs/plugins/v0.7/index.md +++ b/public-docs/plugins/v0.7/index.md @@ -137,10 +137,9 @@ paseo plugin add owner/repository --ref main Append `:relative/path` to the source when the plugin lives below the repository root. An omitted `--ref` tracks the default branch. Explicit branches track updates; tags and commits are -pinned. Check and apply updates with: +pinned. Apply updates with: ```bash -paseo plugin status paseo plugin update workspace-plugin paseo plugin update --all ``` diff --git a/public-docs/plugins/v0.7/reference.md b/public-docs/plugins/v0.7/reference.md index 1ff7511fda..c29a7792ec 100644 --- a/public-docs/plugins/v0.7/reference.md +++ b/public-docs/plugins/v0.7/reference.md @@ -957,7 +957,6 @@ paseo plugin install /absolute/path/to/plugin --id another-runtime-id paseo plugin add owner/repository paseo plugin add https://git.example.com/owner/repository.git --ref main paseo plugin add owner/monorepo:plugins/review -paseo plugin status [id] paseo plugin update paseo plugin update --all paseo plugin ls diff --git a/public-docs/plugins/v0.8/index.md b/public-docs/plugins/v0.8/index.md index 8a167bfbac..fe0915f0c2 100644 --- a/public-docs/plugins/v0.8/index.md +++ b/public-docs/plugins/v0.8/index.md @@ -231,11 +231,13 @@ Append `:relative/path` when the plugin lives below the repository root. Without default branch is tracked; a branch tracks updates, while a tag or commit stays pinned. ```bash -paseo plugin status +paseo plugin ls paseo plugin update workspace-plugin paseo plugin update --all ``` +`ls` reports runtime state, source details, and the installed commit without contacting the remote. + Paseo compiles TypeScript itself, so most plugins need no build step. A repository that must install a dependency Paseo does not provide, or generate files, declares [`build` commands](/docs/plugins/v0.8/reference#cli-reference) in its manifest. diff --git a/public-docs/plugins/v0.8/migration.md b/public-docs/plugins/v0.8/migration.md index 9cb7ea98c7..bfe844877d 100644 --- a/public-docs/plugins/v0.8/migration.md +++ b/public-docs/plugins/v0.8/migration.md @@ -101,7 +101,7 @@ its registration; that registration belongs in the client entry. | Compiler or load error | Meaning and fix | | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `Plugin entry split is required` | The directory still has only the old root entry. Create a runtime entry, move registrations, then delete the old file. | +| `This plugin was made for an older version of Paseo` | The directory still has only the old root entry. Create a runtime entry, move registrations, then delete the old file. | | `Plugin entry points are missing: expected index.client.ts or index.client.tsx and/or index.server.ts or index.server.tsx` | No supported entry exists. Add at least one exact filename. | | `server-only module cannot be imported into the plugin client bundle: ` | A client import reaches `server/`. Move the call behind an RPC and import its contract from `shared/`. | | `client-only module cannot be imported into the plugin server bundle: ` | A server import reaches `client/`. Move that registration and import to the client entry. | diff --git a/public-docs/plugins/v0.8/reference.md b/public-docs/plugins/v0.8/reference.md index 963968166b..139def7da2 100644 --- a/public-docs/plugins/v0.8/reference.md +++ b/public-docs/plugins/v0.8/reference.md @@ -1016,10 +1016,9 @@ paseo plugin install /absolute/path/to/plugin --id another-runtime-id paseo plugin add owner/repository paseo plugin add https://git.example.com/owner/repository.git --ref main paseo plugin add owner/monorepo:plugins/review -paseo plugin status [id] +paseo plugin ls [id] paseo plugin update paseo plugin update --all -paseo plugin ls paseo plugin reload my-plugin paseo plugin logs my-plugin paseo plugin disable my-plugin @@ -1027,6 +1026,9 @@ paseo plugin enable my-plugin paseo plugin remove my-plugin ``` +`ls` reports runtime state, source details, and the installed commit without contacting the remote. +Use `update` when you want Paseo to contact a tracked Git remote and install an available update. + Put `--host ` before a management command when the target is not the CLI's default daemon. `remove` never deletes a directory source; it deletes the managed checkout for a Git source. The install-time `--id` is the runtime ID and allows the same directory or repository to be installed more than once. @@ -1070,7 +1072,7 @@ Use `paseo plugin ls` to read the current status and error. | Symptom | Check | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `Plugin entry split is required` | The directory has only an `index.ts` entry. Follow the [migration guide](/docs/plugins/v0.8/migration). | +| `This plugin was made for an older version of Paseo` | The directory has only an `index.ts` entry. Follow the [migration guide](/docs/plugins/v0.8/migration). | | `Plugin entry points are missing` | Neither `index.client.tsx` nor `index.server.ts` exists with that exact name. | | `server-only module cannot be imported into the plugin client bundle` | Client code imports `server/` or a `*.server.*` file. Move the work behind an RPC and import its contract from `shared/`. | | `client-only module cannot be imported into the plugin server bundle` | Server code imports `client/` or a `*.client.*` file. Register that contribution from `index.client.tsx` instead. |