Plugins/CLI: add descriptor-backed lazy root command registration (#57165)

Merged via squash.

Prepared head SHA: ad1dee32eb
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-03-29 16:02:59 -04:00
committed by GitHub
parent d330782ed1
commit 9b4f26e70a
17 changed files with 413 additions and 71 deletions

View File

@@ -1,6 +1,5 @@
import type { Command } from "commander";
import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js";
import { reparseProgramFromActionArgs } from "./action-reparse.js";
import { removeCommandByName } from "./command-tree.js";
import type { ProgramContext } from "./context.js";
import {
@@ -8,6 +7,7 @@ import {
getCoreCliCommandDescriptors,
getCoreCliCommandsWithSubcommands,
} from "./core-command-descriptors.js";
import { registerLazyCommand } from "./register-lazy-command.js";
import { registerSubCliCommands } from "./register.subclis.js";
export { getCoreCliCommandDescriptors, getCoreCliCommandsWithSubcommands };
@@ -228,13 +228,14 @@ function registerLazyCoreCommand(
entry: CoreCliEntry,
command: CoreCliCommandDescriptor,
) {
const placeholder = program.command(command.name).description(command.description);
placeholder.allowUnknownOption(true);
placeholder.allowExcessArguments(true);
placeholder.action(async (...actionArgs) => {
removeEntryCommands(program, entry);
await entry.register({ program, ctx, argv: process.argv });
await reparseProgramFromActionArgs(program, actionArgs);
registerLazyCommand({
program,
name: command.name,
description: command.description,
removeNames: entry.commands.map((cmd) => cmd.name),
register: async () => {
await entry.register({ program, ctx, argv: process.argv });
},
});
}

View File

@@ -0,0 +1,30 @@
import type { Command } from "commander";
import { reparseProgramFromActionArgs } from "./action-reparse.js";
import { removeCommandByName } from "./command-tree.js";
type RegisterLazyCommandParams = {
program: Command;
name: string;
description: string;
removeNames?: string[];
register: () => Promise<void> | void;
};
export function registerLazyCommand({
program,
name,
description,
removeNames,
register,
}: RegisterLazyCommandParams): void {
const placeholder = program.command(name).description(description);
placeholder.allowUnknownOption(true);
placeholder.allowExcessArguments(true);
placeholder.action(async (...actionArgs) => {
for (const commandName of new Set(removeNames ?? [name])) {
removeCommandByName(program, commandName);
}
await register();
await reparseProgramFromActionArgs(program, actionArgs);
});
}

View File

@@ -2,8 +2,8 @@ import type { Command } from "commander";
import type { OpenClawConfig } from "../../config/config.js";
import { isTruthyEnvValue } from "../../infra/env.js";
import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js";
import { reparseProgramFromActionArgs } from "./action-reparse.js";
import { removeCommand, removeCommandByName } from "./command-tree.js";
import { removeCommandByName } from "./command-tree.js";
import { registerLazyCommand as registerLazyCommandPlaceholder } from "./register-lazy-command.js";
import {
getSubCliCommandsWithSubcommands,
getSubCliEntries as getSubCliEntryDescriptors,
@@ -228,7 +228,7 @@ const entries: SubCliEntry[] = [
const { registerPluginCliCommands } = await import("../../plugins/cli.js");
const config = await loadValidatedConfigForPluginRegistration();
if (config) {
registerPluginCliCommands(program, config);
await registerPluginCliCommands(program, config);
}
const mod = await import("../pairing-cli.js");
mod.registerPairingCli(program);
@@ -244,7 +244,7 @@ const entries: SubCliEntry[] = [
const { registerPluginCliCommands } = await import("../../plugins/cli.js");
const config = await loadValidatedConfigForPluginRegistration();
if (config) {
registerPluginCliCommands(program, config);
await registerPluginCliCommands(program, config);
}
},
},
@@ -328,13 +328,13 @@ export async function registerSubCliByName(program: Command, name: string): Prom
}
function registerLazyCommand(program: Command, entry: SubCliEntry) {
const placeholder = program.command(entry.name).description(entry.description);
placeholder.allowUnknownOption(true);
placeholder.allowExcessArguments(true);
placeholder.action(async (...actionArgs) => {
removeCommand(program, placeholder);
await entry.register(program);
await reparseProgramFromActionArgs(program, actionArgs);
registerLazyCommandPlaceholder({
program,
name: entry.name,
description: entry.description,
register: async () => {
await entry.register(program);
},
});
}

View File

@@ -0,0 +1,46 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("./core-command-descriptors.js", () => ({
getCoreCliCommandDescriptors: () => [
{
name: "status",
description: "Show status",
hasSubcommands: false,
},
],
getCoreCliCommandsWithSubcommands: () => [],
}));
vi.mock("./subcli-descriptors.js", () => ({
getSubCliEntries: () => [
{
name: "config",
description: "Manage config",
hasSubcommands: true,
},
],
getSubCliCommandsWithSubcommands: () => ["config"],
}));
vi.mock("../../plugins/cli.js", () => ({
getPluginCliCommandDescriptors: () => [
{
name: "matrix",
description: "Matrix channel utilities",
hasSubcommands: true,
},
],
}));
const { renderRootHelpText } = await import("./root-help.js");
describe("root help", () => {
it("includes plugin CLI descriptors alongside core and sub-CLI commands", () => {
const text = renderRootHelpText();
expect(text).toContain("status");
expect(text).toContain("config");
expect(text).toContain("matrix");
expect(text).toContain("Matrix channel utilities");
});
});

View File

@@ -1,4 +1,5 @@
import { Command } from "commander";
import { getPluginCliCommandDescriptors } from "../../plugins/cli.js";
import { VERSION } from "../../version.js";
import { getCoreCliCommandDescriptors } from "./core-command-descriptors.js";
import { configureProgramHelp } from "./help.js";
@@ -25,6 +26,13 @@ function buildRootHelpProgram(): Command {
program.command(command.name).description(command.description);
existingCommands.add(command.name);
}
for (const command of getPluginCliCommandDescriptors()) {
if (existingCommands.has(command.name)) {
continue;
}
program.command(command.name).description(command.description);
existingCommands.add(command.name);
}
return program;
}