mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 23:14:47 +00:00
fix(cli): keep plugin json output parseable
Co-authored-by: Eric Milgram, PhD <4348294+ScientificProgrammer@users.noreply.github.com>
This commit is contained in:
@@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping `openclaw plugins list` descriptions readable.
|
||||
- Process execution: collapse case-insensitive duplicate child environment keys on Windows so caller-provided overrides such as `PATH` cannot be shadowed by host `Path`.
|
||||
- Browser CLI: request the existing `operator.admin` gateway scope explicitly for browser control commands, avoiding unnecessary scope-upgrade approval loops. Fixes #81555. (#81716) Thanks @joshavant.
|
||||
- CLI/plugins: route lazy plugin command-registration chatter to stderr only during JSON-output command registration, keeping plugin-backed `--json` stdout parseable without changing parse-only or pass-through `--json` behavior. Fixes #81535. (#81536) Thanks @ScientificProgrammer and @vincentkoc.
|
||||
- Web: honor explicitly configured global `web_search` providers during provider ownership resolution while keeping sandboxed `web_fetch` limited to bundled providers.
|
||||
- Plugins/doctor: repair configured legacy npm declaration stubs by reinstalling their npm packages into the managed plugin root instead of loading workspace `node_modules`, and warn when discovery sees those stubs. Fixes #79632. Thanks @Dylanzhang1128 and @vincentkoc.
|
||||
- Channels: keep configured third-party channel plugins visible in `openclaw channels list` when their manifest declares `channels` but has not added `channelConfigs` metadata yet. Fixes #81334. (#81340) Thanks @AllynSheep and @vincentkoc.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import process from "node:process";
|
||||
import { CommanderError } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import { runCli, shouldStartProxyForCli } from "./run-main.js";
|
||||
|
||||
const tryRouteCliMock = vi.hoisted(() => vi.fn());
|
||||
@@ -269,6 +270,7 @@ describe("runCli exit behavior", () => {
|
||||
resolveManifestCliCommandSurfaceOwnerMock.mockReturnValue(undefined);
|
||||
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
||||
delete process.env.OPENCLAW_HIDE_BANNER;
|
||||
loggingState.forceConsoleToStderr = false;
|
||||
});
|
||||
|
||||
it("does not force process.exit after successful routed command", async () => {
|
||||
@@ -596,6 +598,68 @@ describe("runCli exit behavior", () => {
|
||||
expect(parseAsync).toHaveBeenCalledWith(argv);
|
||||
});
|
||||
|
||||
it("routes lazy plugin registration logs to stderr only during --json registration", async () => {
|
||||
tryRouteCliMock.mockResolvedValueOnce(false);
|
||||
resolvePluginCliRootOwnerIdsMock.mockImplementation(
|
||||
({ primaryCommand }: { primaryCommand?: string }) =>
|
||||
primaryCommand === "memory" ? ["memory"] : [],
|
||||
);
|
||||
let stderrDuringPluginRegistration = false;
|
||||
let stderrDuringParse = true;
|
||||
registerPluginCliCommandsFromValidatedConfigMock.mockImplementationOnce(async () => {
|
||||
stderrDuringPluginRegistration = loggingState.forceConsoleToStderr;
|
||||
return {};
|
||||
});
|
||||
const parseAsync = vi.fn().mockImplementationOnce(async () => {
|
||||
stderrDuringParse = loggingState.forceConsoleToStderr;
|
||||
});
|
||||
buildProgramMock.mockReturnValueOnce({
|
||||
commands: [],
|
||||
parseAsync,
|
||||
});
|
||||
|
||||
await runCli(["node", "openclaw", "memory", "search", "query", "--json"]);
|
||||
|
||||
expect(registerPluginCliCommandsFromValidatedConfigMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
{ mode: "lazy", primary: "memory" },
|
||||
);
|
||||
expect(stderrDuringPluginRegistration).toBe(true);
|
||||
expect(stderrDuringParse).toBe(false);
|
||||
expect(loggingState.forceConsoleToStderr).toBe(false);
|
||||
});
|
||||
|
||||
it("does not route lazy plugin registration logs for pass-through --json after terminator", async () => {
|
||||
tryRouteCliMock.mockResolvedValueOnce(false);
|
||||
resolvePluginCliRootOwnerIdsMock.mockImplementation(
|
||||
({ primaryCommand }: { primaryCommand?: string }) =>
|
||||
primaryCommand === "memory" ? ["memory"] : [],
|
||||
);
|
||||
let stderrDuringPluginRegistration = true;
|
||||
registerPluginCliCommandsFromValidatedConfigMock.mockImplementationOnce(async () => {
|
||||
stderrDuringPluginRegistration = loggingState.forceConsoleToStderr;
|
||||
return {};
|
||||
});
|
||||
const parseAsync = vi.fn().mockResolvedValueOnce(undefined);
|
||||
buildProgramMock.mockReturnValueOnce({
|
||||
commands: [],
|
||||
parseAsync,
|
||||
});
|
||||
|
||||
await runCli(["node", "openclaw", "memory", "--", "--json"]);
|
||||
|
||||
expect(registerPluginCliCommandsFromValidatedConfigMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
{ mode: "lazy", primary: "memory" },
|
||||
);
|
||||
expect(stderrDuringPluginRegistration).toBe(false);
|
||||
expect(loggingState.forceConsoleToStderr).toBe(false);
|
||||
});
|
||||
|
||||
it("fails protected commands when managed proxy activation fails", async () => {
|
||||
startProxyMock.mockRejectedValueOnce(new Error("proxy: enabled but no HTTP proxy URL"));
|
||||
|
||||
|
||||
@@ -134,7 +134,15 @@ export function isGatewayRunFastPathArgv(argv: string[]): boolean {
|
||||
}
|
||||
|
||||
function hasJsonOutputFlag(argv: string[]): boolean {
|
||||
return argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
|
||||
for (const arg of argv) {
|
||||
if (arg === "--") {
|
||||
return false;
|
||||
}
|
||||
if (arg === "--json" || arg.startsWith("--json=")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function tryRunGatewayRunFastPath(
|
||||
@@ -725,10 +733,33 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
const config = await startupTrace.measure("register-plugin-commands", async () => {
|
||||
const { registerPluginCliCommandsFromValidatedConfig } =
|
||||
await import("../plugins/cli.js");
|
||||
return await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
|
||||
mode: "lazy",
|
||||
primary,
|
||||
});
|
||||
if (!hasJsonOutputFlag(parseArgv)) {
|
||||
return await registerPluginCliCommandsFromValidatedConfig(
|
||||
program,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
mode: "lazy",
|
||||
primary,
|
||||
},
|
||||
);
|
||||
}
|
||||
const { loggingState } = await import("../logging/state.js");
|
||||
const previousForceStderr = loggingState.forceConsoleToStderr;
|
||||
loggingState.forceConsoleToStderr = true;
|
||||
try {
|
||||
return await registerPluginCliCommandsFromValidatedConfig(
|
||||
program,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
mode: "lazy",
|
||||
primary,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
loggingState.forceConsoleToStderr = previousForceStderr;
|
||||
}
|
||||
});
|
||||
if (config) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user