From 25ecb2895af80db65e9c3e676fc0e76fc9fcef4d Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 26 Apr 2026 02:23:57 +0100 Subject: [PATCH] fix: preserve bundle format in plugin index --- src/plugins/installed-plugin-index-store.ts | 2 + src/plugins/installed-plugin-index.test.ts | 39 ++++++++++++++- src/plugins/installed-plugin-index.ts | 8 ++++ .../manifest-registry-installed.test.ts | 48 +++++++++++++++++++ src/plugins/manifest-registry-installed.ts | 2 + 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 4d27fb719c0..1fc6814a4b8 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -55,6 +55,8 @@ const InstalledPluginIndexRecordSchema = z packageInstall: z.unknown().optional(), manifestPath: z.string(), manifestHash: z.string(), + format: z.string().optional(), + bundleFormat: z.string().optional(), source: z.string().optional(), setupSource: z.string().optional(), packageJson: z diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 7ab34a4ffc9..f7ae5c0ea8a 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -66,12 +66,16 @@ function createPluginCandidate(params: { packageVersion?: string; packageDir?: string; packageManifest?: OpenClawPackageManifest; + format?: PluginCandidate["format"]; + bundleFormat?: PluginCandidate["bundleFormat"]; }): PluginCandidate { return { idHint: params.idHint ?? "demo", - source: path.join(params.rootDir, "index.ts"), + source: params.format === "bundle" ? params.rootDir : path.join(params.rootDir, "index.ts"), rootDir: params.rootDir, origin: params.origin ?? "global", + format: params.format, + bundleFormat: params.bundleFormat, packageName: params.packageName, packageVersion: params.packageVersion, packageDir: params.packageDir ?? params.rootDir, @@ -198,6 +202,39 @@ describe("installed plugin index", () => { expect(index.plugins[0]?.installRecordHash).toBeUndefined(); }); + it("keeps bundle format metadata needed for manifest reconstruction", () => { + const rootDir = makeTempDir(); + fs.mkdirSync(path.join(rootDir, ".claude-plugin"), { recursive: true }); + fs.mkdirSync(path.join(rootDir, "commands"), { recursive: true }); + fs.writeFileSync( + path.join(rootDir, ".claude-plugin", "plugin.json"), + JSON.stringify({ + name: "Claude Bundle", + commands: "commands", + }), + "utf8", + ); + + const index = loadInstalledPluginIndex({ + candidates: [ + createPluginCandidate({ + rootDir, + idHint: "claude-bundle", + format: "bundle", + bundleFormat: "claude", + }), + ], + env: hermeticEnv(), + }); + + expect(index.plugins[0]).toMatchObject({ + pluginId: "claude-bundle", + format: "bundle", + bundleFormat: "claude", + source: rootDir, + }); + }); + it("keeps packageJson paths root-relative when packageDir is reached through a symlink", () => { const fixture = createRichPluginFixture(); const linkParent = makeTempDir(); diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 0b4e39c6aae..bb713a6a397 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -84,6 +84,8 @@ export type InstalledPluginIndexRecord = { packageInstall?: PluginInstallSourceInfo; manifestPath: string; manifestHash: string; + format?: PluginManifestRecord["format"]; + bundleFormat?: PluginManifestRecord["bundleFormat"]; source?: string; setupSource?: string; packageJson?: { @@ -516,6 +518,12 @@ function buildInstalledPluginIndex( startup: buildStartupInfo(record), compat: collectCompatCodes(record), }; + if (record.format && record.format !== "openclaw") { + indexRecord.format = record.format; + } + if (record.bundleFormat) { + indexRecord.bundleFormat = record.bundleFormat; + } if (record.enabledByDefault === true) { indexRecord.enabledByDefault = true; } diff --git a/src/plugins/manifest-registry-installed.test.ts b/src/plugins/manifest-registry-installed.test.ts index df73caa20d6..353f850894f 100644 --- a/src/plugins/manifest-registry-installed.test.ts +++ b/src/plugins/manifest-registry-installed.test.ts @@ -89,4 +89,52 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => { modelPrefixes: ["installed-"], }); }); + + it("reconstructs bundle candidates with their bundle manifest format", () => { + const rootDir = makeTempDir(); + fs.mkdirSync(path.join(rootDir, ".claude-plugin"), { recursive: true }); + fs.mkdirSync(path.join(rootDir, "commands"), { recursive: true }); + fs.writeFileSync( + path.join(rootDir, ".claude-plugin", "plugin.json"), + JSON.stringify({ + name: "Claude Bundle", + commands: "commands", + }), + "utf8", + ); + + const index = createIndex(rootDir); + const registry = loadPluginManifestRegistryForInstalledIndex({ + index: { + ...index, + plugins: [ + { + ...index.plugins[0], + pluginId: "claude-bundle", + manifestPath: path.join(rootDir, ".claude-plugin", "plugin.json"), + source: rootDir, + format: "bundle", + bundleFormat: "claude", + }, + ], + }, + env: { + OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", + OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1", + OPENCLAW_VERSION: "2026.4.25", + VITEST: "true", + }, + includeDisabled: true, + }); + + expect(registry.diagnostics).toEqual([]); + expect(registry.plugins).toEqual([ + expect.objectContaining({ + id: "claude-bundle", + format: "bundle", + bundleFormat: "claude", + skills: ["commands"], + }), + ]); + }); }); diff --git a/src/plugins/manifest-registry-installed.ts b/src/plugins/manifest-registry-installed.ts index c24182fe2d1..5b5c9f0c3cd 100644 --- a/src/plugins/manifest-registry-installed.ts +++ b/src/plugins/manifest-registry-installed.ts @@ -24,6 +24,8 @@ function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate ...(record.setupSource ? { setupSource: record.setupSource } : {}), rootDir: record.rootDir, origin: record.origin, + ...(record.format ? { format: record.format } : {}), + ...(record.bundleFormat ? { bundleFormat: record.bundleFormat } : {}), ...(record.packageName ? { packageName: record.packageName } : {}), ...(record.packageVersion ? { packageVersion: record.packageVersion } : {}), packageDir: record.rootDir,