fix: unblock cli startup metadata

This commit is contained in:
Peter Steinberger
2026-04-04 02:35:23 +01:00
parent 143d377c5a
commit 1e6e685347
36 changed files with 674 additions and 79 deletions

View File

@@ -46,13 +46,13 @@ function mergeCliRegistrars(params: {
runtimeRegistry: PluginRegistry;
metadataRegistry: PluginRegistry;
}) {
const metadataCommands = new Set(
params.metadataRegistry.cliRegistrars.flatMap((entry) => entry.commands),
const runtimeCommands = new Set(
params.runtimeRegistry.cliRegistrars.flatMap((entry) => entry.commands),
);
return [
...params.metadataRegistry.cliRegistrars,
...params.runtimeRegistry.cliRegistrars.filter(
(entry) => !entry.commands.some((command) => metadataCommands.has(command)),
...params.runtimeRegistry.cliRegistrars,
...params.metadataRegistry.cliRegistrars.filter(
(entry) => !entry.commands.some((command) => runtimeCommands.has(command)),
),
];
}

View File

@@ -189,6 +189,224 @@ module.exports = {
);
});
it("skips bundled channel full entries that do not provide a dedicated cli-metadata entry", async () => {
const bundledRoot = makeTempDir();
const pluginDir = path.join(bundledRoot, "bundled-skip-channel");
const fullMarker = path.join(pluginDir, "full-loaded.txt");
fs.mkdirSync(pluginDir, { recursive: true });
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/bundled-skip-channel",
openclaw: { extensions: ["./index.cjs"] },
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "bundled-skip-channel",
configSchema: EMPTY_PLUGIN_SCHEMA,
channels: ["bundled-skip-channel"],
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
id: "bundled-skip-channel",
register() {
throw new Error("bundled channel full entry should not load during CLI metadata capture");
},
};`,
"utf-8",
);
const registry = await loadOpenClawPluginCliRegistry({
config: {
plugins: {
allow: ["bundled-skip-channel"],
entries: {
"bundled-skip-channel": {
enabled: true,
},
},
},
},
});
expect(fs.existsSync(fullMarker)).toBe(false);
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
"bundled-skip-channel",
);
expect(registry.plugins.find((entry) => entry.id === "bundled-skip-channel")?.status).toBe(
"loaded",
);
});
it("prefers bundled channel cli-metadata entries over full channel entries", async () => {
const bundledRoot = makeTempDir();
const pluginDir = path.join(bundledRoot, "bundled-cli-channel");
const fullMarker = path.join(pluginDir, "full-loaded.txt");
const cliMarker = path.join(pluginDir, "cli-loaded.txt");
fs.mkdirSync(pluginDir, { recursive: true });
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/bundled-cli-channel",
openclaw: { extensions: ["./index.cjs"] },
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "bundled-cli-channel",
configSchema: EMPTY_PLUGIN_SCHEMA,
channels: ["bundled-cli-channel"],
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
id: "bundled-cli-channel",
register() {
throw new Error("bundled channel full entry should not load during CLI metadata capture");
},
};`,
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "cli-metadata.cjs"),
`module.exports = {
id: "bundled-cli-channel",
register(api) {
require("node:fs").writeFileSync(${JSON.stringify(cliMarker)}, "loaded", "utf-8");
api.registerCli(() => {}, {
descriptors: [
{
name: "bundled-cli-channel",
description: "Bundled channel CLI metadata",
hasSubcommands: true,
},
],
});
},
};`,
"utf-8",
);
const registry = await loadOpenClawPluginCliRegistry({
config: {
plugins: {
allow: ["bundled-cli-channel"],
entries: {
"bundled-cli-channel": {
enabled: true,
},
},
},
},
});
expect(fs.existsSync(fullMarker)).toBe(false);
expect(fs.existsSync(cliMarker)).toBe(true);
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
"bundled-cli-channel",
);
});
it("skips bundled non-channel full entries that do not provide a dedicated cli-metadata entry", async () => {
const bundledRoot = makeTempDir();
const pluginDir = path.join(bundledRoot, "bundled-skip-provider");
const fullMarker = path.join(pluginDir, "full-loaded.txt");
fs.mkdirSync(pluginDir, { recursive: true });
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/bundled-skip-provider",
openclaw: { extensions: ["./index.cjs"] },
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "bundled-skip-provider",
configSchema: EMPTY_PLUGIN_SCHEMA,
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
id: "bundled-skip-provider",
register() {
throw new Error("bundled provider full entry should not load during CLI metadata capture");
},
};`,
"utf-8",
);
const registry = await loadOpenClawPluginCliRegistry({
config: {
plugins: {
allow: ["bundled-skip-provider"],
entries: {
"bundled-skip-provider": {
enabled: true,
},
},
},
},
});
expect(fs.existsSync(fullMarker)).toBe(false);
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
"bundled-skip-provider",
);
expect(registry.plugins.find((entry) => entry.id === "bundled-skip-provider")?.status).toBe(
"loaded",
);
});
it("collects channel CLI metadata during full plugin loads", () => {
useNoBundledPlugins();
const pluginDir = makeTempDir();

View File

@@ -103,6 +103,13 @@ export type PluginLoadOptions = {
throwOnLoadError?: boolean;
};
const CLI_METADATA_ENTRY_BASENAMES = [
"cli-metadata.ts",
"cli-metadata.js",
"cli-metadata.mjs",
"cli-metadata.cjs",
] as const;
export class PluginLoadFailureError extends Error {
readonly pluginIds: string[];
readonly registry: PluginRegistry;
@@ -1810,8 +1817,17 @@ export async function loadOpenClawPluginCliRegistry(
}
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
const sourceForCliMetadata =
candidate.origin === "bundled" ? cliMetadataSource : (cliMetadataSource ?? candidate.source);
if (!sourceForCliMetadata) {
record.status = "loaded";
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
const opened = openBoundaryFileSync({
absolutePath: candidate.source,
absolutePath: sourceForCliMetadata,
rootPath: pluginRoot,
boundaryLabel: "plugin root",
rejectHardlinks: candidate.origin !== "bundled",
@@ -1943,3 +1959,13 @@ function safeRealpathOrResolve(value: string): string {
return path.resolve(value);
}
}
function resolveCliMetadataEntrySource(rootDir: string): string | null {
for (const basename of CLI_METADATA_ENTRY_BASENAMES) {
const candidate = path.join(rootDir, basename);
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}