From 944913fc980ed9c32aa747f2fa1095d2500e6fb0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 20:18:00 +0000 Subject: [PATCH] refactor(cli): extract shared command-removal and timeout action helpers --- src/cli/program/command-registry.ts | 14 +------ src/cli/program/command-tree.test.ts | 39 +++++++++++++++++++ src/cli/program/command-tree.ts | 19 +++++++++ .../register.status-health-sessions.ts | 35 +++++++++-------- src/cli/program/register.subclis.ts | 14 +------ 5 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 src/cli/program/command-tree.test.ts create mode 100644 src/cli/program/command-tree.ts diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts index 15626bbc3..72eb7b870 100644 --- a/src/cli/program/command-registry.ts +++ b/src/cli/program/command-registry.ts @@ -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); } } diff --git a/src/cli/program/command-tree.test.ts b/src/cli/program/command-tree.test.ts new file mode 100644 index 000000000..c03e08ea6 --- /dev/null +++ b/src/cli/program/command-tree.test.ts @@ -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"]); + }); +}); diff --git a/src/cli/program/command-tree.ts b/src/cli/program/command-tree.ts new file mode 100644 index 000000000..0f179b5dd --- /dev/null +++ b/src/cli/program/command-tree.ts @@ -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); +} diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index 123dda645..1aa092a4f 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -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, +): Promise { + 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, diff --git a/src/cli/program/register.subclis.ts b/src/cli/program/register.subclis.ts index 1fa981899..77c5cd285 100644 --- a/src/cli/program/register.subclis.ts +++ b/src/cli/program/register.subclis.ts @@ -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; @@ -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 { 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; }