CLI: restore lightweight root help and scoped status plugin preload

This commit is contained in:
Vincent Koc
2026-03-15 17:37:36 -07:00
parent c455cccd3d
commit f87e7be55e
7 changed files with 127 additions and 38 deletions

View File

@@ -2,14 +2,32 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import { loadConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging.js";
import { loadOpenClawPlugins } from "../plugins/loader.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import type { PluginLogger } from "../plugins/types.js";
const log = createSubsystemLogger("plugins");
let pluginRegistryLoaded = false;
let pluginRegistryLoaded: "none" | "channels" | "all" = "none";
export function ensurePluginRegistryLoaded(): void {
if (pluginRegistryLoaded) {
export type PluginRegistryScope = "channels" | "all";
function resolveChannelPluginIds(params: {
config: ReturnType<typeof loadConfig>;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => plugin.channels.length > 0)
.map((plugin) => plugin.id);
}
export function ensurePluginRegistryLoaded(options?: { scope?: PluginRegistryScope }): void {
const scope = options?.scope ?? "all";
if (pluginRegistryLoaded === "all" || pluginRegistryLoaded === scope) {
return;
}
const active = getActivePluginRegistry();
@@ -19,7 +37,7 @@ export function ensurePluginRegistryLoaded(): void {
active &&
(active.plugins.length > 0 || active.channels.length > 0 || active.tools.length > 0)
) {
pluginRegistryLoaded = true;
pluginRegistryLoaded = "all";
return;
}
const config = loadConfig();
@@ -34,6 +52,15 @@ export function ensurePluginRegistryLoaded(): void {
config,
workspaceDir,
logger,
...(scope === "channels"
? {
onlyPluginIds: resolveChannelPluginIds({
config,
workspaceDir,
env: process.env,
}),
}
: {}),
});
pluginRegistryLoaded = true;
pluginRegistryLoaded = scope;
}

View File

@@ -149,7 +149,7 @@ describe("registerPreActionHooks", () => {
runtime: runtimeMock,
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
expect(process.title).toBe("openclaw-status");
vi.clearAllMocks();
@@ -164,7 +164,7 @@ describe("registerPreActionHooks", () => {
runtime: runtimeMock,
commandPath: ["message", "send"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "all" });
});
it("skips help/version preaction and respects banner opt-out", async () => {

View File

@@ -67,6 +67,10 @@ function loadPluginRegistryModule() {
return pluginRegistryModulePromise;
}
function resolvePluginRegistryScope(commandPath: string[]): "channels" | "all" {
return commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all";
}
function getRootCommand(command: Command): Command {
let current = command;
while (current.parent) {
@@ -136,7 +140,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
// Load plugins for commands that need channel access
if (PLUGIN_REQUIRED_COMMANDS.has(commandPath[0])) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
ensurePluginRegistryLoaded();
ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) });
}
});
}

View File

@@ -37,7 +37,7 @@ describe("tryRouteCli", () => {
vi.resetModules();
({ tryRouteCli } = await import("./route.js"));
findRoutedCommandMock.mockReturnValue({
loadPlugins: false,
loadPlugins: true,
run: runRouteMock,
});
});
@@ -59,6 +59,7 @@ describe("tryRouteCli", () => {
suppressDoctorStdout: true,
}),
);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
it("does not pass suppressDoctorStdout for routed non-json commands", async () => {
@@ -68,6 +69,7 @@ describe("tryRouteCli", () => {
runtime: expect.any(Object),
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
it("routes status when root options precede the command", async () => {
@@ -80,5 +82,6 @@ describe("tryRouteCli", () => {
runtime: expect.any(Object),
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
});

View File

@@ -22,7 +22,12 @@ async function prepareRoutedCommand(params: {
const shouldLoadPlugins =
typeof params.loadPlugins === "function" ? params.loadPlugins(params.argv) : params.loadPlugins;
if (shouldLoadPlugins) {
ensurePluginRegistryLoaded();
ensurePluginRegistryLoaded({
scope:
params.commandPath[0] === "status" || params.commandPath[0] === "health"
? "channels"
: "all",
});
}
}