From fc0a2bc87deb1fedda023b23c8ac7574d211c4ac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 03:46:05 +0100 Subject: [PATCH] fix: show banner on gateway fast path --- src/cli/run-main.exit.test.ts | 69 +++++++++++++++++++++++++++++++++++ src/cli/run-main.ts | 30 +++++++++++++-- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index a05a1d31f76..3f5ab87a82e 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -23,6 +23,9 @@ const restoreTerminalStateMock = vi.hoisted(() => vi.fn()); const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false)); const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn()); const runCrestodianMock = vi.hoisted(() => vi.fn(async () => {})); +const commanderParseAsyncMock = vi.hoisted(() => vi.fn(async () => {})); +const addGatewayRunCommandMock = vi.hoisted(() => vi.fn((command: unknown) => command)); +const emitCliBannerMock = vi.hoisted(() => vi.fn()); const progressDoneMock = vi.hoisted(() => vi.fn()); const createCliProgressMock = vi.hoisted(() => vi.fn(() => ({ @@ -35,10 +38,49 @@ const maybeRunCliInContainerMock = vi.hoisted(() => >((argv: string[]) => ({ handled: false, argv })), ); +vi.mock("commander", () => { + class MockCommanderError extends Error { + exitCode: number; + code: string; + + constructor(exitCode: number, code: string, message: string) { + super(message); + this.exitCode = exitCode; + this.code = code; + } + } + + class MockCommand { + name = vi.fn(() => this); + enablePositionalOptions = vi.fn(() => this); + exitOverride = vi.fn(() => this); + description = vi.fn(() => this); + command = vi.fn(() => new MockCommand()); + parseAsync = commanderParseAsyncMock; + } + + return { + Command: MockCommand, + CommanderError: MockCommanderError, + }; +}); + vi.mock("./route.js", () => ({ tryRouteCli: tryRouteCliMock, })); +vi.mock("./gateway-cli/run.js", () => ({ + addGatewayRunCommand: addGatewayRunCommandMock, +})); + +vi.mock("../version.js", () => ({ + VERSION: "9.9.9-test", +})); + +vi.mock("./banner.js", () => ({ + emitCliBanner: emitCliBannerMock, +})); + vi.mock("./container-target.js", () => ({ maybeRunCliInContainer: maybeRunCliInContainerMock, parseCliContainerArgs: (argv: string[]) => ({ ok: true, container: null, argv }), @@ -132,6 +174,7 @@ describe("runCli exit behavior", () => { hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false); getProgramContextMock.mockReturnValue(null); delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH; + delete process.env.OPENCLAW_HIDE_BANNER; }); it("does not force process.exit after successful routed command", async () => { @@ -151,6 +194,32 @@ describe("runCli exit behavior", () => { exitSpy.mockRestore(); }); + it("emits the startup banner before gateway foreground fast-path startup", async () => { + await runCli(["node", "openclaw", "gateway", "--force"]); + + expect(tryRouteCliMock).not.toHaveBeenCalled(); + expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test", { + argv: ["node", "openclaw", "gateway", "--force"], + }); + expect(addGatewayRunCommandMock).toHaveBeenCalledTimes(2); + expect(commanderParseAsyncMock).toHaveBeenCalledWith([ + "node", + "openclaw", + "gateway", + "--force", + ]); + }); + + it("honors banner suppression on the gateway foreground fast path", async () => { + process.env.OPENCLAW_HIDE_BANNER = "1"; + + await runCli(["node", "openclaw", "gateway"]); + + expect(tryRouteCliMock).not.toHaveBeenCalled(); + expect(emitCliBannerMock).not.toHaveBeenCalled(); + expect(commanderParseAsyncMock).toHaveBeenCalledWith(["node", "openclaw", "gateway"]); + }); + it("renders browser help from startup metadata without building the full program", async () => { outputPrecomputedBrowserHelpTextMock.mockReturnValueOnce(true); const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 32af67862d5..419bb4bcf4a 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -83,6 +83,10 @@ export function isGatewayRunFastPathArgv(argv: string[]): boolean { return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run"; } +function hasJsonOutputFlag(argv: string[]): boolean { + return argv.some((arg) => arg === "--json" || arg.startsWith("--json=")); +} + async function tryRunGatewayRunFastPath( argv: string[], startupTrace: ReturnType, @@ -90,10 +94,30 @@ async function tryRunGatewayRunFastPath( if (!isGatewayRunFastPathArgv(argv)) { return false; } - const [{ Command }, { addGatewayRunCommand }] = await startupTrace.measure( - "gateway-run-imports", - () => Promise.all([import("commander"), import("./gateway-cli/run.js")]), + const [ + { Command }, + { addGatewayRunCommand }, + { VERSION }, + { emitCliBanner }, + { resolveCliStartupPolicy }, + ] = await startupTrace.measure("gateway-run-imports", () => + Promise.all([ + import("commander"), + import("./gateway-cli/run.js"), + import("../version.js"), + import("./banner.js"), + import("./command-startup-policy.js"), + ]), ); + const invocation = resolveCliArgvInvocation(argv); + const startupPolicy = resolveCliStartupPolicy({ + commandPath: invocation.commandPath, + jsonOutputMode: hasJsonOutputFlag(argv), + routeMode: true, + }); + if (!startupPolicy.hideBanner) { + emitCliBanner(VERSION, { argv }); + } const program = new Command(); program.name("openclaw"); program.enablePositionalOptions();