fix(cli): route logs to stderr in --json mode to keep stdout clean

This commit is contained in:
Charles Dusek 2026-03-22 15:14:39 -05:00 committed by Peter Steinberger
parent b863e1c315
commit ebb4cc0128
2 changed files with 38 additions and 0 deletions

View File

@ -5,6 +5,7 @@ const setVerboseMock = vi.fn();
const emitCliBannerMock = vi.fn();
const ensureConfigReadyMock = vi.fn(async () => {});
const ensurePluginRegistryLoadedMock = vi.fn();
const routeLogsToStderrMock = vi.fn();
const runtimeMock = {
log: vi.fn(),
@ -24,6 +25,10 @@ vi.mock("../banner.js", () => ({
emitCliBanner: emitCliBannerMock,
}));
vi.mock("../../logging/console.js", () => ({
routeLogsToStderr: routeLogsToStderrMock,
}));
vi.mock("../cli-name.js", () => ({
resolveCliName: () => "openclaw",
}));
@ -270,6 +275,35 @@ describe("registerPreActionHooks", () => {
});
});
it("routes logs to stderr in --json mode so stdout stays clean", async () => {
await runPreAction({
parseArgv: ["agents"],
processArgv: ["node", "openclaw", "agents", "--json"],
});
expect(routeLogsToStderrMock).toHaveBeenCalledOnce();
vi.clearAllMocks();
// config set --json is parse-only (not JSON output mode), should not route
await runPreAction({
parseArgv: ["config", "set", "gateway.auth.mode", "local", "--json"],
processArgv: ["node", "openclaw", "config", "set", "gateway.auth.mode", "local", "--json"],
});
expect(routeLogsToStderrMock).not.toHaveBeenCalled();
vi.clearAllMocks();
// non-json command should not route
await runPreAction({
parseArgv: ["agents"],
processArgv: ["node", "openclaw", "agents"],
});
expect(routeLogsToStderrMock).not.toHaveBeenCalled();
});
it("bypasses config guard for config validate", async () => {
await runPreAction({
parseArgv: ["config", "validate"],

View File

@ -1,6 +1,7 @@
import type { Command } from "commander";
import { setVerbose } from "../../globals.js";
import { isTruthyEnvValue } from "../../infra/env.js";
import { routeLogsToStderr } from "../../logging/console.js";
import type { LogLevel } from "../../logging/levels.js";
import { defaultRuntime } from "../../runtime.js";
import {
@ -123,6 +124,9 @@ export function registerPreActionHooks(program: Command, programVersion: string)
return;
}
const commandPath = getCommandPathWithRootOptions(argv, 2);
if (isJsonOutputMode(commandPath, argv)) {
routeLogsToStderr();
}
const hideBanner =
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||