mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
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
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<PluginListItem> = {
|
||||
{ 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<PluginSourceStatusItem> = {
|
||||
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<PluginSourceUpdateItem> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
@@ -96,10 +86,13 @@ export async function runPluginInitCommand(
|
||||
}
|
||||
|
||||
export async function runPluginListCommand(
|
||||
pluginId: string | undefined,
|
||||
options: PluginOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<PluginListItem>> {
|
||||
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<ListResult<PluginSourceStatusItem>> {
|
||||
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("<directory>")
|
||||
.option("--id <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 <ref>", "Git branch, tag, or commit")
|
||||
.option("--path <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));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+4
-4
@@ -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 `:<directory>` for a plugin in a
|
||||
monorepo. `paseo plugin logs <id>` returns the plugin's recent daemon-side stdout and stderr. Add `--json` for
|
||||
structured entries, or run `paseo --host <target> plugin logs <id>` for another daemon. See the
|
||||
monorepo. `paseo plugin ls [id]` does not contact the remote. `paseo plugin logs <id>` returns the
|
||||
plugin's recent daemon-side stdout and stderr. Add `--json` for structured entries, or run
|
||||
`paseo --host <target> plugin logs <id>` for another daemon. See the
|
||||
[Plugin reference](/docs/plugins/v0.7/reference) for installation, trust, lifecycle, and log-retention
|
||||
behavior.
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 <id>
|
||||
paseo plugin update --all
|
||||
paseo plugin ls
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: <file>` | 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: <file>` | A server import reaches `client/`. Move that registration and import to the client entry. |
|
||||
|
||||
@@ -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 <id>
|
||||
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 <url>` 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. |
|
||||
|
||||
Reference in New Issue
Block a user