feat(plugins): list from registry snapshot

This commit is contained in:
Vincent Koc
2026-04-25 05:14:37 -07:00
parent caf25fac91
commit 5859dcd298
4 changed files with 110 additions and 8 deletions

View File

@@ -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 = {

View File

@@ -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",

View File

@@ -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,
};

View File

@@ -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,