Plugins: extract CLI lifecycle

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 17:27:26 +00:00
parent 565f9bf2bd
commit b5757a6625
No known key found for this signature in database
3 changed files with 151 additions and 34 deletions

View File

@ -0,0 +1,97 @@
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import type { PluginLogger } from "../plugins/types.js";
import { registerExtensionHostCliCommands } from "./cli-lifecycle.js";
function createLogger(): PluginLogger {
return {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
}
describe("registerExtensionHostCliCommands", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("skips overlapping command registrations", () => {
const program = new Command();
program.command("memory");
const registry = createEmptyPluginRegistry();
const memoryRegister = vi.fn();
const otherRegister = vi.fn();
registry.cliRegistrars.push(
{
pluginId: "memory-core",
register: memoryRegister,
commands: ["memory"],
source: "bundled",
},
{
pluginId: "other",
register: otherRegister,
commands: ["other"],
source: "bundled",
},
);
const logger = createLogger();
registerExtensionHostCliCommands({
program,
registry,
config: {} as never,
workspaceDir: "/tmp/workspace",
logger,
});
expect(memoryRegister).not.toHaveBeenCalled();
expect(otherRegister).toHaveBeenCalledOnce();
expect(logger.debug).toHaveBeenCalledWith(
"plugin CLI register skipped (memory-core): command already registered (memory)",
);
});
it("warns on sync and async registration failures", async () => {
const program = new Command();
const registry = createEmptyPluginRegistry();
registry.cliRegistrars.push(
{
pluginId: "sync-fail",
register: () => {
throw new Error("sync fail");
},
commands: ["sync"],
source: "bundled",
},
{
pluginId: "async-fail",
register: async () => {
throw new Error("async fail");
},
commands: ["async"],
source: "bundled",
},
);
const logger = createLogger();
registerExtensionHostCliCommands({
program,
registry,
config: {} as never,
workspaceDir: "/tmp/workspace",
logger,
});
await Promise.resolve();
expect(logger.warn).toHaveBeenCalledWith(
"plugin CLI register failed (sync-fail): Error: sync fail",
);
expect(logger.warn).toHaveBeenCalledWith(
"plugin CLI register failed (async-fail): Error: async fail",
);
});
});

View File

@ -0,0 +1,46 @@
import type { Command } from "commander";
import type { OpenClawConfig } from "../config/config.js";
import type { PluginRegistry } from "../plugins/registry.js";
import type { PluginLogger } from "../plugins/types.js";
export function registerExtensionHostCliCommands(params: {
program: Command;
registry: PluginRegistry;
config: OpenClawConfig;
workspaceDir: string;
logger: PluginLogger;
}): void {
const existingCommands = new Set(params.program.commands.map((cmd) => cmd.name()));
for (const entry of params.registry.cliRegistrars) {
if (entry.commands.length > 0) {
const overlaps = entry.commands.filter((command) => existingCommands.has(command));
if (overlaps.length > 0) {
params.logger.debug(
`plugin CLI register skipped (${entry.pluginId}): command already registered (${overlaps.join(
", ",
)})`,
);
continue;
}
}
try {
const result = entry.register({
program: params.program,
config: params.config,
workspaceDir: params.workspaceDir,
logger: params.logger,
});
if (result && typeof result.then === "function") {
void result.catch((err) => {
params.logger.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
});
}
for (const command of entry.commands) {
existingCommands.add(command);
}
} catch (err) {
params.logger.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
}
}
}

View File

@ -2,6 +2,7 @@ import type { Command } from "commander";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { registerExtensionHostCliCommands } from "../extension-host/cli-lifecycle.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { loadOpenClawPlugins } from "./loader.js";
import type { PluginLogger } from "./types.js";
@ -27,38 +28,11 @@ export function registerPluginCliCommands(
env,
logger,
});
const existingCommands = new Set(program.commands.map((cmd) => cmd.name()));
for (const entry of registry.cliRegistrars) {
if (entry.commands.length > 0) {
const overlaps = entry.commands.filter((command) => existingCommands.has(command));
if (overlaps.length > 0) {
log.debug(
`plugin CLI register skipped (${entry.pluginId}): command already registered (${overlaps.join(
", ",
)})`,
);
continue;
}
}
try {
const result = entry.register({
program,
config,
workspaceDir,
logger,
});
if (result && typeof result.then === "function") {
void result.catch((err) => {
log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
});
}
for (const command of entry.commands) {
existingCommands.add(command);
}
} catch (err) {
log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
}
}
registerExtensionHostCliCommands({
program,
registry,
config,
workspaceDir,
logger,
});
}