refactor(cli): extract shared command-removal and timeout action helpers

This commit is contained in:
Peter Steinberger
2026-02-21 20:18:00 +00:00
parent bb490a4b51
commit 944913fc98
5 changed files with 81 additions and 40 deletions

View File

@@ -1,6 +1,7 @@
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 { registerSubCliCommands } from "./register.subclis.js";
@@ -229,22 +230,11 @@ export function getCoreCliCommandsWithSubcommands(): string[] {
return collectCoreCliCommandNames((command) => command.hasSubcommands);
}
function removeCommand(program: Command, command: Command) {
const commands = program.commands as Command[];
const index = commands.indexOf(command);
if (index >= 0) {
commands.splice(index, 1);
}
}
function removeEntryCommands(program: Command, entry: CoreCliEntry) {
// Some registrars install multiple top-level commands (e.g. status/health/sessions).
// Remove placeholders/old registrations for all names in the entry before re-registering.
for (const cmd of entry.commands) {
const existing = program.commands.find((c) => c.name() === cmd.name);
if (existing) {
removeCommand(program, existing);
}
removeCommandByName(program, cmd.name);
}
}

View File

@@ -0,0 +1,39 @@
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import { removeCommand, removeCommandByName } from "./command-tree.js";
describe("command-tree", () => {
it("removes a command instance when present", () => {
const program = new Command();
const alpha = program.command("alpha");
program.command("beta");
expect(removeCommand(program, alpha)).toBe(true);
expect(program.commands.map((command) => command.name())).toEqual(["beta"]);
});
it("returns false when command instance is already absent", () => {
const program = new Command();
program.command("alpha");
const detached = new Command("beta");
expect(removeCommand(program, detached)).toBe(false);
});
it("removes by command name", () => {
const program = new Command();
program.command("alpha");
program.command("beta");
expect(removeCommandByName(program, "alpha")).toBe(true);
expect(program.commands.map((command) => command.name())).toEqual(["beta"]);
});
it("returns false when name does not exist", () => {
const program = new Command();
program.command("alpha");
expect(removeCommandByName(program, "missing")).toBe(false);
expect(program.commands.map((command) => command.name())).toEqual(["alpha"]);
});
});

View File

@@ -0,0 +1,19 @@
import type { Command } from "commander";
export function removeCommand(program: Command, command: Command): boolean {
const commands = program.commands as Command[];
const index = commands.indexOf(command);
if (index < 0) {
return false;
}
commands.splice(index, 1);
return true;
}
export function removeCommandByName(program: Command, name: string): boolean {
const existing = program.commands.find((command) => command.name() === name);
if (!existing) {
return false;
}
return removeCommand(program, existing);
}

View File

@@ -24,6 +24,21 @@ function parseTimeoutMs(timeout: unknown): number | null | undefined {
return parsed;
}
async function runWithVerboseAndTimeout(
opts: { verbose?: boolean; debug?: boolean; timeout?: unknown },
action: (params: { verbose: boolean; timeoutMs: number | undefined }) => Promise<void>,
): Promise<void> {
const verbose = resolveVerbose(opts);
setVerbose(verbose);
const timeoutMs = parseTimeoutMs(opts.timeout);
if (timeoutMs === null) {
return;
}
await runCommandWithRuntime(defaultRuntime, async () => {
await action({ verbose, timeoutMs });
});
}
export function registerStatusHealthSessionsCommands(program: Command) {
program
.command("status")
@@ -56,20 +71,14 @@ export function registerStatusHealthSessionsCommands(program: Command) {
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/status", "docs.openclaw.ai/cli/status")}\n`,
)
.action(async (opts) => {
const verbose = resolveVerbose(opts);
setVerbose(verbose);
const timeout = parseTimeoutMs(opts.timeout);
if (timeout === null) {
return;
}
await runCommandWithRuntime(defaultRuntime, async () => {
await runWithVerboseAndTimeout(opts, async ({ verbose, timeoutMs }) => {
await statusCommand(
{
json: Boolean(opts.json),
all: Boolean(opts.all),
deep: Boolean(opts.deep),
usage: Boolean(opts.usage),
timeoutMs: timeout,
timeoutMs,
verbose,
},
defaultRuntime,
@@ -90,17 +99,11 @@ export function registerStatusHealthSessionsCommands(program: Command) {
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/health", "docs.openclaw.ai/cli/health")}\n`,
)
.action(async (opts) => {
const verbose = resolveVerbose(opts);
setVerbose(verbose);
const timeout = parseTimeoutMs(opts.timeout);
if (timeout === null) {
return;
}
await runCommandWithRuntime(defaultRuntime, async () => {
await runWithVerboseAndTimeout(opts, async ({ verbose, timeoutMs }) => {
await healthCommand(
{
json: Boolean(opts.json),
timeoutMs: timeout,
timeoutMs,
verbose,
},
defaultRuntime,

View File

@@ -3,6 +3,7 @@ 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";
type SubCliRegistrar = (program: Command) => Promise<void> | void;
@@ -296,23 +297,12 @@ export function getSubCliCommandsWithSubcommands(): string[] {
return entries.filter((entry) => entry.hasSubcommands).map((entry) => entry.name);
}
function removeCommand(program: Command, command: Command) {
const commands = program.commands as Command[];
const index = commands.indexOf(command);
if (index >= 0) {
commands.splice(index, 1);
}
}
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {
const entry = entries.find((candidate) => candidate.name === name);
if (!entry) {
return false;
}
const existing = program.commands.find((cmd) => cmd.name() === entry.name);
if (existing) {
removeCommand(program, existing);
}
removeCommandByName(program, entry.name);
await entry.register(program);
return true;
}