From 5859dcd298bf5628a31969e46fd355bbcaf11097 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 05:14:37 -0700 Subject: [PATCH] feat(plugins): list from registry snapshot --- src/cli/plugins-cli-test-helpers.ts | 19 ++++++++ src/cli/plugins-cli.list.test.ts | 13 ++++-- src/cli/plugins-cli.ts | 14 ++++-- src/plugins/status.ts | 72 ++++++++++++++++++++++++++++- 4 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index 0f078736855..56322d84f56 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -35,6 +35,7 @@ export const recordPluginInstall: UnknownMock = vi.fn(); export const clearPluginManifestRegistryCache: UnknownMock = vi.fn(); export const loadPluginManifestRegistry: UnknownMock = vi.fn(); export const buildPluginSnapshotReport: UnknownMock = vi.fn(); +export const buildPluginRegistrySnapshotReport: UnknownMock = vi.fn(); export const buildPluginInspectReport: UnknownMock = vi.fn(); export const buildPluginDiagnosticsReport: UnknownMock = vi.fn(); export const buildPluginCompatibilityNotices: UnknownMock = vi.fn(); @@ -169,6 +170,18 @@ vi.mock("../plugins/status.js", () => ({ buildPluginSnapshotReport, ...args, )) as (typeof import("../plugins/status.js"))["buildPluginSnapshotReport"], + buildPluginRegistrySnapshotReport: (( + ...args: Parameters< + (typeof import("../plugins/status.js"))["buildPluginRegistrySnapshotReport"] + > + ) => + invokeMock< + Parameters<(typeof import("../plugins/status.js"))["buildPluginRegistrySnapshotReport"]>, + ReturnType<(typeof import("../plugins/status.js"))["buildPluginRegistrySnapshotReport"]> + >( + buildPluginRegistrySnapshotReport, + ...args, + )) as (typeof import("../plugins/status.js"))["buildPluginRegistrySnapshotReport"], buildPluginInspectReport: (( ...args: Parameters<(typeof import("../plugins/status.js"))["buildPluginInspectReport"]> ) => @@ -414,6 +427,7 @@ export function resetPluginsCliTestState() { clearPluginManifestRegistryCache.mockReset(); loadPluginManifestRegistry.mockReset(); buildPluginSnapshotReport.mockReset(); + buildPluginRegistrySnapshotReport.mockReset(); buildPluginInspectReport.mockReset(); buildPluginDiagnosticsReport.mockReset(); buildPluginCompatibilityNotices.mockReset(); @@ -477,6 +491,11 @@ export function resetPluginsCliTestState() { diagnostics: [], }; buildPluginSnapshotReport.mockReturnValue(defaultPluginReport); + buildPluginRegistrySnapshotReport.mockReturnValue({ + ...defaultPluginReport, + registrySource: "derived", + registryDiagnostics: [], + }); buildPluginDiagnosticsReport.mockReturnValue(defaultPluginReport); buildPluginCompatibilityNotices.mockReturnValue([]); const defaultRegistryIndex = { diff --git a/src/cli/plugins-cli.list.test.ts b/src/cli/plugins-cli.list.test.ts index 0bde971364b..b2f67bf2235 100644 --- a/src/cli/plugins-cli.list.test.ts +++ b/src/cli/plugins-cli.list.test.ts @@ -3,7 +3,7 @@ import { createPluginRecord } from "../plugins/status.test-helpers.js"; import { buildPluginDiagnosticsReport, buildPluginInspectReport, - buildPluginSnapshotReport, + buildPluginRegistrySnapshotReport, inspectPluginRegistry, resetPluginsCliTestState, refreshPluginRegistry, @@ -17,8 +17,10 @@ describe("plugins cli list", () => { }); it("includes imported state in JSON output", async () => { - buildPluginSnapshotReport.mockReturnValue({ + buildPluginRegistrySnapshotReport.mockReturnValue({ workspaceDir: "/workspace", + registrySource: "persisted", + registryDiagnostics: [], plugins: [ createPluginRecord({ id: "demo", @@ -32,8 +34,9 @@ describe("plugins cli list", () => { await runPluginsCommand(["plugins", "list", "--json"]); - expect(buildPluginSnapshotReport).toHaveBeenCalledWith( + expect(buildPluginRegistrySnapshotReport).toHaveBeenCalledWith( expect.objectContaining({ + config: {}, logger: expect.objectContaining({ info: expect.any(Function), warn: expect.any(Function), @@ -44,6 +47,10 @@ describe("plugins cli list", () => { expect(JSON.parse(runtimeLogs[0] ?? "null")).toEqual({ workspaceDir: "/workspace", + registry: { + source: "persisted", + diagnostics: [], + }, plugins: [ expect.objectContaining({ id: "demo", diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 2e5eb5fc4fe..e645404e9e8 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -15,7 +15,7 @@ import { buildPluginDiagnosticsReport, buildPluginCompatibilityNotices, buildPluginInspectReport, - buildPluginSnapshotReport, + buildPluginRegistrySnapshotReport, formatPluginCompatibilityNotice, } from "../plugins/status.js"; import type { PluginLogger } from "../plugins/types.js"; @@ -174,9 +174,11 @@ export function registerPluginsCli(program: Command) { .option("--enabled", "Only show enabled plugins", false) .option("--verbose", "Show detailed entries", false) .action((opts: PluginsListOptions) => { - const report = buildPluginSnapshotReport( - opts.json ? { logger: quietPluginJsonLogger } : undefined, - ); + const cfg = loadConfig(); + const report = buildPluginRegistrySnapshotReport({ + config: cfg, + ...(opts.json ? { logger: quietPluginJsonLogger } : {}), + }); const list = opts.enabled ? report.plugins.filter((p) => p.status === "loaded") : report.plugins; @@ -184,6 +186,10 @@ export function registerPluginsCli(program: Command) { if (opts.json) { const payload = { workspaceDir: report.workspaceDir, + registry: { + source: report.registrySource, + diagnostics: report.registryDiagnostics, + }, plugins: list, diagnostics: report.diagnostics, }; diff --git a/src/plugins/status.ts b/src/plugins/status.ts index af2d522357c..52d80fd57d2 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -19,8 +19,13 @@ import { } from "./inspect-shape.js"; import { loadOpenClawPlugins } from "./loader.js"; import type { PluginDiagnostic } from "./manifest-types.js"; +import { + loadPluginRegistrySnapshotWithMetadata, + type PluginRegistrySnapshotDiagnostic, + type PluginRegistrySnapshotSource, +} from "./plugin-registry.js"; import { resolveBundledProviderCompatPluginIds } from "./providers.js"; -import type { PluginRegistry } from "./registry.js"; +import { createEmptyPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js"; import { listImportedRuntimePluginIds } from "./runtime.js"; import { buildPluginRuntimeLoadOptions, @@ -33,6 +38,11 @@ export type PluginStatusReport = PluginRegistry & { workspaceDir?: string; }; +export type PluginRegistryStatusReport = PluginStatusReport & { + registrySource: PluginRegistrySnapshotSource; + registryDiagnostics: readonly PluginRegistrySnapshotDiagnostic[]; +}; + export type { PluginCapabilityKind, PluginInspectShape } from "./inspect-shape.js"; export type PluginCompatibilityNotice = { @@ -143,6 +153,66 @@ type PluginReportParams = { logger?: PluginLogger; }; +function buildPluginRecordFromInstalledIndex( + plugin: import("./installed-plugin-index.js").InstalledPluginIndexRecord, +): PluginRecord { + return { + id: plugin.pluginId, + name: plugin.pluginId, + ...(plugin.packageVersion ? { version: plugin.packageVersion } : {}), + format: "openclaw", + source: plugin.manifestPath, + rootDir: plugin.rootDir, + origin: plugin.origin, + enabled: plugin.enabled, + status: plugin.enabled ? "loaded" : "disabled", + toolNames: [], + hookNames: [], + channelIds: [...plugin.contributions.channels], + cliBackendIds: [...plugin.contributions.cliBackends], + providerIds: [...plugin.contributions.providers], + speechProviderIds: [], + realtimeTranscriptionProviderIds: [], + realtimeVoiceProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + videoGenerationProviderIds: [], + musicGenerationProviderIds: [], + webFetchProviderIds: [], + webSearchProviderIds: [], + memoryEmbeddingProviderIds: [], + agentHarnessIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + gatewayDiscoveryServiceIds: [], + commands: [...plugin.contributions.commandAliases], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + contracts: {}, + }; +} + +export function buildPluginRegistrySnapshotReport( + params?: PluginReportParams, +): PluginRegistryStatusReport { + const config = params?.config ?? loadConfig(); + const result = loadPluginRegistrySnapshotWithMetadata({ + config, + env: params?.env, + workspaceDir: params?.workspaceDir, + }); + return { + workspaceDir: params?.workspaceDir, + ...createEmptyPluginRegistry(), + plugins: result.snapshot.plugins.map(buildPluginRecordFromInstalledIndex), + diagnostics: [...result.snapshot.diagnostics], + registrySource: result.source, + registryDiagnostics: result.diagnostics, + }; +} + function buildPluginReport( params: PluginReportParams | undefined, loadModules: boolean,