feat(plugins): unify source installation and simplify plugin settings

This commit is contained in:
Mohamed Boudra
2026-09-16 23:27:31 +02:00
parent c7db3c5a8c
commit 3f0a74d3fb
27 changed files with 912 additions and 289 deletions
+27 -8
View File
@@ -28,15 +28,15 @@ const getPluginLogs = vi.fn(async () => [
message: "ready",
},
]);
const installDirectoryPlugin = vi.fn(async () => ({
const installPluginSource = vi.fn(async () => ({
id: "trusted-plugin",
path: "/plugins/trusted-plugin",
enabled: true,
status: "running" as const,
}));
const installPluginSource = vi.fn(async () => ({
id: "trusted-plugin",
path: "/plugins/trusted-plugin",
const installDirectoryPlugin = vi.fn(async () => ({
id: "legacy-plugin",
path: "/plugins/legacy-plugin",
enabled: true,
status: "running" as const,
}));
@@ -59,7 +59,12 @@ vi.mock("../../utils/client.js", () => ({
}));
import { render } from "../../output/index.js";
import { createPluginCommand, runPluginListCommand, runPluginLogsCommand } from "./index.js";
import {
createPluginCommand,
runPluginInstallCommand,
runPluginListCommand,
runPluginLogsCommand,
} from "./index.js";
describe("plugin management commands", () => {
beforeEach(() => {
@@ -146,7 +151,7 @@ describe("plugin management commands", () => {
});
it("prints the trust acknowledgement before installing", async () => {
features.pluginManagement = true;
features.pluginGitManagement = true;
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
const command = createPluginCommand();
@@ -155,7 +160,22 @@ describe("plugin management commands", () => {
expect(stderr).toHaveBeenCalledWith(
expect.stringContaining("Git build commands run unsandboxed on the daemon host"),
);
expect(installDirectoryPlugin).toHaveBeenCalledWith("/plugins/trusted-plugin", undefined);
expect(installPluginSource).toHaveBeenCalledWith({ source: "/plugins/trusted-plugin" });
stderr.mockRestore();
});
it("requires source support for directory installs without using the legacy RPC", async () => {
features.pluginManagement = true;
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
await expect(
runPluginInstallCommand("/plugins/trusted-plugin", { daemonTarget }, {} as never),
).rejects.toMatchObject({
code: "DAEMON_UPDATE_REQUIRED",
message: "Update the host to install or update plugins.",
});
expect(installPluginSource).not.toHaveBeenCalled();
expect(installDirectoryPlugin).not.toHaveBeenCalled();
stderr.mockRestore();
});
@@ -184,7 +204,6 @@ describe("plugin management commands", () => {
expect(installPluginSource).toHaveBeenCalledWith({
source: "/plugins/monorepo:plugins/review",
});
expect(installDirectoryPlugin).not.toHaveBeenCalled();
stderr.mockRestore();
});
});
+10 -29
View File
@@ -1,14 +1,10 @@
import { Command } from "commander";
import path from "node:path";
import type {
PluginListItem,
PluginLogEntry,
PluginSourceUpdateItem,
} from "@getpaseo/protocol/messages";
import {
formatPluginSourceReference,
parsePluginSourceReference,
} from "@getpaseo/protocol/plugin-source-reference";
import { formatPluginSourceReference } from "@getpaseo/protocol/plugin-source-reference";
import type { CommandOptions, ListResult, OutputSchema, SingleResult } from "../../output/index.js";
import { withOutput } from "../../output/index.js";
import { addJsonAndDaemonHostOptions, addJsonOption } from "../../utils/command-options.js";
@@ -109,7 +105,7 @@ export async function runPluginLogsCommand(
return { type: "list", data, schema: pluginLogsSchema };
}
async function install(
export async function runPluginInstallCommand(
source: string,
options: PluginOptions,
_command: Command,
@@ -117,29 +113,14 @@ async function install(
process.stderr.write(
"Trusting plugin code: server code and Git build commands run unsandboxed on the daemon host; client code runs inside Paseo. Dependencies and future updates are part of the codebase you trust.\n",
);
const isExplicitPath =
path.isAbsolute(source) ||
source === "." ||
source === ".." ||
source.startsWith("./") ||
source.startsWith("../") ||
source.startsWith(".\\") ||
source.startsWith("..\\");
const hasPluginPathSuffix = parsePluginSourceReference(source).pluginPath !== undefined;
const canUseLegacyDirectoryInstall =
isExplicitPath && !hasPluginPathSuffix && !options.ref && !options.path;
const sourceReference = formatPluginSourceReference(source, options.path);
const data = canUseLegacyDirectoryInstall
? await withPluginManagementClient(options.daemonTarget, (client) =>
client.installDirectoryPlugin(source, options.id),
)
: await withPluginSourceClient(options.daemonTarget, (client) =>
client.installPluginSource({
source: sourceReference,
...(options.id ? { id: options.id } : {}),
...(options.ref ? { ref: options.ref } : {}),
}),
);
const data = await withPluginSourceClient(options.daemonTarget, (client) =>
client.installPluginSource({
source: sourceReference,
...(options.id ? { id: options.id } : {}),
...(options.ref ? { ref: options.ref } : {}),
}),
);
return { type: "single", data, schema: pluginSchema };
}
@@ -209,7 +190,7 @@ export function createPluginCommand(): Command {
.option("--id <id>", "Runtime plugin ID (defaults to paseo-plugin.json id)")
.option("--ref <ref>", "Git branch, tag, or commit")
.option("--path <path>", "Legacy form of the :plugin/path source suffix"),
).action(withOutput(install));
).action(withOutput(runPluginInstallCommand));
addJsonAndDaemonHostOptions(
plugin
.command("update")
+1 -1
View File
@@ -55,7 +55,7 @@ export async function withPluginSourceClient<T>(
return withPluginClient(
target,
"pluginGitManagement",
"Update the host to install and update Git plugins.",
"Update the host to install or update plugins.",
run,
);
}