diff --git a/src/cli/program/preaction.ts b/src/cli/program/preaction.ts index 021e9ea72..1607e0634 100644 --- a/src/cli/program/preaction.ts +++ b/src/cli/program/preaction.ts @@ -4,6 +4,7 @@ import { emitCliBanner } from "../banner.js"; import { getCommandPath, getVerboseFlag, hasHelpOrVersion } from "../argv.js"; import { ensureConfigReady } from "./config-guard.js"; import { ensurePluginRegistryLoaded } from "../plugin-registry.js"; +import { isTruthyEnvValue } from "../../infra/env.js"; import { setVerbose } from "../../globals.js"; function setProcessTitleForCommand(actionCommand: Command) { @@ -22,15 +23,21 @@ const PLUGIN_REQUIRED_COMMANDS = new Set(["message", "channels", "directory"]); export function registerPreActionHooks(program: Command, programVersion: string) { program.hook("preAction", async (_thisCommand, actionCommand) => { setProcessTitleForCommand(actionCommand); - emitCliBanner(programVersion); const argv = process.argv; if (hasHelpOrVersion(argv)) return; + const commandPath = getCommandPath(argv, 2); + const hideBanner = + isTruthyEnvValue(process.env.CLAWDBOT_HIDE_BANNER) || + commandPath[0] === "update" || + (commandPath[0] === "plugins" && commandPath[1] === "update"); + if (!hideBanner) { + emitCliBanner(programVersion); + } const verbose = getVerboseFlag(argv, { includeDebug: true }); setVerbose(verbose); if (!verbose) { process.env.NODE_NO_WARNINGS ??= "1"; } - const commandPath = getCommandPath(argv, 2); if (commandPath[0] === "doctor") return; await ensureConfigReady({ runtime: defaultRuntime, commandPath }); // Load plugins for commands that need channel access diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 99da9ac0d..02f92d01e 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -19,7 +19,6 @@ import { type UpdateStepInfo, type UpdateStepResult, type UpdateStepProgress, - type UpdateStepResult, } from "../infra/update-runner.js"; import { detectGlobalInstallManagerByPresence, diff --git a/src/plugins/cli.test.ts b/src/plugins/cli.test.ts new file mode 100644 index 000000000..59c9c469c --- /dev/null +++ b/src/plugins/cli.test.ts @@ -0,0 +1,45 @@ +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + memoryRegister: vi.fn(), + otherRegister: vi.fn(), +})); + +vi.mock("./loader.js", () => ({ + loadClawdbotPlugins: () => ({ + cliRegistrars: [ + { + pluginId: "memory-core", + register: mocks.memoryRegister, + commands: ["memory"], + source: "bundled", + }, + { + pluginId: "other", + register: mocks.otherRegister, + commands: ["other"], + source: "bundled", + }, + ], + }), +})); + +import { registerPluginCliCommands } from "./cli.js"; + +describe("registerPluginCliCommands", () => { + beforeEach(() => { + mocks.memoryRegister.mockClear(); + mocks.otherRegister.mockClear(); + }); + + it("skips plugin CLI registrars when commands already exist", () => { + const program = new Command(); + program.command("memory"); + + registerPluginCliCommands(program, {} as any); + + expect(mocks.memoryRegister).not.toHaveBeenCalled(); + expect(mocks.otherRegister).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index 6a3571b2d..a8c92fd56 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -24,7 +24,20 @@ export function registerPluginCliCommands(program: Command, cfg?: ClawdbotConfig 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, @@ -37,6 +50,9 @@ export function registerPluginCliCommands(program: Command, cfg?: ClawdbotConfig 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)}`); }