Skip to content

Commit 99fe86c

Browse files
Route JSON-mode plugin registration logs to stderr
1 parent 1a7669b commit 99fe86c

4 files changed

Lines changed: 127 additions & 36 deletions

File tree

src/cli/json-output-mode.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { loggingState } from "../logging/state.js";
2+
3+
export function hasJsonOutputFlag(argv: readonly string[]): boolean {
4+
for (const arg of argv) {
5+
if (arg === "--") {
6+
return false;
7+
}
8+
if (arg === "--json" || arg.startsWith("--json=")) {
9+
return true;
10+
}
11+
}
12+
return false;
13+
}
14+
15+
export async function withConsoleLogsRoutedToStderrForJson<T>(
16+
argv: readonly string[],
17+
run: () => Promise<T>,
18+
): Promise<T> {
19+
if (!hasJsonOutputFlag(argv)) {
20+
return run();
21+
}
22+
const previousForceStderr = loggingState.forceConsoleToStderr;
23+
loggingState.forceConsoleToStderr = true;
24+
try {
25+
return await run();
26+
} finally {
27+
loggingState.forceConsoleToStderr = previousForceStderr;
28+
}
29+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Command } from "commander";
2+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+
import { loggingState } from "../logging/state.js";
4+
5+
const registerPluginCliCommandsFromValidatedConfig = vi.fn(async () => ({}));
6+
const registerNodesCameraCommands = vi.fn();
7+
const registerNodesInvokeCommands = vi.fn();
8+
const registerNodesLocationCommands = vi.fn();
9+
const registerNodesNotifyCommand = vi.fn();
10+
const registerNodesPairingCommands = vi.fn();
11+
const registerNodesPushCommand = vi.fn();
12+
const registerNodesScreenCommands = vi.fn();
13+
const registerNodesStatusCommands = vi.fn();
14+
15+
vi.mock("../plugins/cli.js", () => ({
16+
registerPluginCliCommandsFromValidatedConfig,
17+
}));
18+
19+
vi.mock("./nodes-cli/register.camera.js", () => ({ registerNodesCameraCommands }));
20+
vi.mock("./nodes-cli/register.invoke.js", () => ({ registerNodesInvokeCommands }));
21+
vi.mock("./nodes-cli/register.location.js", () => ({ registerNodesLocationCommands }));
22+
vi.mock("./nodes-cli/register.notify.js", () => ({ registerNodesNotifyCommand }));
23+
vi.mock("./nodes-cli/register.pairing.js", () => ({ registerNodesPairingCommands }));
24+
vi.mock("./nodes-cli/register.push.js", () => ({ registerNodesPushCommand }));
25+
vi.mock("./nodes-cli/register.screen.js", () => ({ registerNodesScreenCommands }));
26+
vi.mock("./nodes-cli/register.status.js", () => ({ registerNodesStatusCommands }));
27+
28+
const { registerNodesCli } = await import("./nodes-cli/register.js");
29+
30+
describe("registerNodesCli plugin registration", () => {
31+
const originalArgv = process.argv;
32+
let originalForceConsoleToStderr = false;
33+
34+
beforeEach(() => {
35+
originalForceConsoleToStderr = loggingState.forceConsoleToStderr;
36+
loggingState.forceConsoleToStderr = false;
37+
registerPluginCliCommandsFromValidatedConfig.mockClear();
38+
});
39+
40+
afterEach(() => {
41+
process.argv = originalArgv;
42+
loggingState.forceConsoleToStderr = originalForceConsoleToStderr;
43+
});
44+
45+
async function registerWithArgv(argv: string[]) {
46+
process.argv = argv;
47+
const program = new Command();
48+
await registerNodesCli(program);
49+
return program;
50+
}
51+
52+
it("routes plugin registration logs to stderr for nodes --json commands", async () => {
53+
let forceStderrDuringRegistration = false;
54+
registerPluginCliCommandsFromValidatedConfig.mockImplementationOnce(async () => {
55+
forceStderrDuringRegistration = loggingState.forceConsoleToStderr;
56+
return {};
57+
});
58+
59+
const program = await registerWithArgv(["node", "openclaw", "nodes", "list", "--json"]);
60+
61+
expect(registerPluginCliCommandsFromValidatedConfig).toHaveBeenCalledWith(
62+
program,
63+
undefined,
64+
undefined,
65+
{ mode: "lazy", primary: "nodes" },
66+
);
67+
expect(forceStderrDuringRegistration).toBe(true);
68+
expect(loggingState.forceConsoleToStderr).toBe(false);
69+
});
70+
71+
it("does not route pass-through --json after the terminator", async () => {
72+
let forceStderrDuringRegistration = true;
73+
registerPluginCliCommandsFromValidatedConfig.mockImplementationOnce(async () => {
74+
forceStderrDuringRegistration = loggingState.forceConsoleToStderr;
75+
return {};
76+
});
77+
78+
await registerWithArgv(["node", "openclaw", "nodes", "invoke", "--", "--json"]);
79+
80+
expect(forceStderrDuringRegistration).toBe(false);
81+
expect(loggingState.forceConsoleToStderr).toBe(false);
82+
});
83+
});

src/cli/nodes-cli/register.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Command } from "commander";
22
import { formatDocsLink } from "../../terminal/links.js";
33
import { theme } from "../../terminal/theme.js";
44
import { formatHelpExamples } from "../help-format.js";
5+
import { withConsoleLogsRoutedToStderrForJson } from "../json-output-mode.js";
56
import { registerNodesCameraCommands } from "./register.camera.js";
67
import { registerNodesInvokeCommands } from "./register.invoke.js";
78
import { registerNodesLocationCommands } from "./register.location.js";
@@ -40,8 +41,10 @@ export async function registerNodesCli(program: Command) {
4041
registerNodesLocationCommands(nodes);
4142

4243
const { registerPluginCliCommandsFromValidatedConfig } = await import("../../plugins/cli.js");
43-
await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
44-
mode: "lazy",
45-
primary: "nodes",
46-
});
44+
await withConsoleLogsRoutedToStderrForJson(process.argv, () =>
45+
registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
46+
mode: "lazy",
47+
primary: "nodes",
48+
}),
49+
);
4750
}

src/cli/run-main.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
consumeGatewayFastPathRootOptionToken,
2323
consumeGatewayRunOptionToken,
2424
} from "./gateway-run-argv.js";
25+
import {
26+
hasJsonOutputFlag,
27+
withConsoleLogsRoutedToStderrForJson,
28+
} from "./json-output-mode.js";
2529
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
2630
import { getCoreCliCommandNames } from "./program/core-command-descriptors.js";
2731
import { getSubCliEntries } from "./program/subcli-descriptors.js";
@@ -133,18 +137,6 @@ export function isGatewayRunFastPathArgv(argv: string[]): boolean {
133137
return sawGateway;
134138
}
135139

136-
function hasJsonOutputFlag(argv: string[]): boolean {
137-
for (const arg of argv) {
138-
if (arg === "--") {
139-
return false;
140-
}
141-
if (arg === "--json" || arg.startsWith("--json=")) {
142-
return true;
143-
}
144-
}
145-
return false;
146-
}
147-
148140
async function tryRunGatewayRunFastPath(
149141
argv: string[],
150142
startupTrace: ReturnType<typeof createGatewayCliMainStartupTrace>,
@@ -740,33 +732,17 @@ export async function runCli(argv: string[] = process.argv) {
740732
const config = await startupTrace.measure("register-plugin-commands", async () => {
741733
const { registerPluginCliCommandsFromValidatedConfig } =
742734
await import("../plugins/cli.js");
743-
if (!hasJsonOutputFlag(parseArgv)) {
744-
return await registerPluginCliCommandsFromValidatedConfig(
735+
return await withConsoleLogsRoutedToStderrForJson(parseArgv, () =>
736+
registerPluginCliCommandsFromValidatedConfig(
745737
program,
746738
undefined,
747739
undefined,
748740
{
749741
mode: "lazy",
750742
primary,
751743
},
752-
);
753-
}
754-
const { loggingState } = await import("../logging/state.js");
755-
const previousForceStderr = loggingState.forceConsoleToStderr;
756-
loggingState.forceConsoleToStderr = true;
757-
try {
758-
return await registerPluginCliCommandsFromValidatedConfig(
759-
program,
760-
undefined,
761-
undefined,
762-
{
763-
mode: "lazy",
764-
primary,
765-
},
766-
);
767-
} finally {
768-
loggingState.forceConsoleToStderr = previousForceStderr;
769-
}
744+
),
745+
);
770746
});
771747
if (config) {
772748
if (

0 commit comments

Comments
 (0)