refactor: share cli startup and routing helpers

This commit is contained in:
Peter Steinberger
2026-04-06 13:51:27 +01:00
parent 6ed33d29c8
commit a21709d041
15 changed files with 1189 additions and 336 deletions

View File

@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const ensureConfigReadyMock = vi.hoisted(() => vi.fn(async () => {}));
const ensureCliPluginRegistryLoadedMock = vi.hoisted(() => vi.fn(async () => {}));
vi.mock("./program/config-guard.js", () => ({
ensureConfigReady: ensureConfigReadyMock,
}));
vi.mock("./plugin-registry-loader.js", () => ({
ensureCliPluginRegistryLoaded: ensureCliPluginRegistryLoadedMock,
resolvePluginRegistryScopeForCommandPath: vi.fn((commandPath: string[]) =>
commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all",
),
}));
describe("ensureCliCommandBootstrap", () => {
let ensureCliCommandBootstrap: typeof import("./command-bootstrap.js").ensureCliCommandBootstrap;
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
({ ensureCliCommandBootstrap } = await import("./command-bootstrap.js"));
});
it("runs config guard and plugin loading with shared options", async () => {
const runtime = {} as never;
await ensureCliCommandBootstrap({
runtime,
commandPath: ["agents", "list"],
suppressDoctorStdout: true,
allowInvalid: true,
loadPlugins: true,
});
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
runtime,
commandPath: ["agents", "list"],
allowInvalid: true,
suppressDoctorStdout: true,
});
expect(ensureCliPluginRegistryLoadedMock).toHaveBeenCalledWith({
scope: "all",
routeLogsToStderr: true,
});
});
it("skips config guard without skipping plugin loading", async () => {
await ensureCliCommandBootstrap({
runtime: {} as never,
commandPath: ["status"],
suppressDoctorStdout: true,
skipConfigGuard: true,
loadPlugins: true,
});
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
expect(ensureCliPluginRegistryLoadedMock).toHaveBeenCalledWith({
scope: "channels",
routeLogsToStderr: true,
});
});
it("does nothing extra when plugin loading is disabled", async () => {
await ensureCliCommandBootstrap({
runtime: {} as never,
commandPath: ["config", "validate"],
skipConfigGuard: true,
loadPlugins: false,
});
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
expect(ensureCliPluginRegistryLoadedMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,38 @@
import type { RuntimeEnv } from "../runtime.js";
import {
ensureCliPluginRegistryLoaded,
resolvePluginRegistryScopeForCommandPath,
} from "./plugin-registry-loader.js";
let configGuardModulePromise: Promise<typeof import("./program/config-guard.js")> | undefined;
function loadConfigGuardModule() {
configGuardModulePromise ??= import("./program/config-guard.js");
return configGuardModulePromise;
}
export async function ensureCliCommandBootstrap(params: {
runtime: RuntimeEnv;
commandPath: string[];
suppressDoctorStdout?: boolean;
skipConfigGuard?: boolean;
allowInvalid?: boolean;
loadPlugins?: boolean;
}) {
if (!params.skipConfigGuard) {
const { ensureConfigReady } = await loadConfigGuardModule();
await ensureConfigReady({
runtime: params.runtime,
commandPath: params.commandPath,
...(params.allowInvalid ? { allowInvalid: true } : {}),
...(params.suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
});
}
if (!params.loadPlugins) {
return;
}
await ensureCliPluginRegistryLoaded({
scope: resolvePluginRegistryScopeForCommandPath(params.commandPath),
routeLogsToStderr: params.suppressDoctorStdout,
});
}

View File

@@ -0,0 +1,122 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const emitCliBannerMock = vi.hoisted(() => vi.fn());
const routeLogsToStderrMock = vi.hoisted(() => vi.fn());
const ensureCliCommandBootstrapMock = vi.hoisted(() => vi.fn(async () => {}));
vi.mock("./banner.js", () => ({
emitCliBanner: emitCliBannerMock,
}));
vi.mock("../logging/console.js", () => ({
routeLogsToStderr: routeLogsToStderrMock,
}));
vi.mock("./command-bootstrap.js", () => ({
ensureCliCommandBootstrap: ensureCliCommandBootstrapMock,
}));
describe("command-execution-startup", () => {
let mod: typeof import("./command-execution-startup.js");
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
mod = await import("./command-execution-startup.js");
});
it("resolves startup context from argv and mode", () => {
expect(
mod.resolveCliExecutionStartupContext({
argv: ["node", "openclaw", "status", "--json"],
jsonOutputMode: true,
routeMode: true,
}),
).toEqual({
commandPath: ["status"],
startupPolicy: {
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: true,
loadPlugins: false,
},
});
});
it("routes logs to stderr and emits banner only when allowed", async () => {
await mod.applyCliExecutionStartupPresentation({
startupPolicy: {
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: false,
loadPlugins: true,
},
version: "1.2.3",
argv: ["node", "openclaw", "status"],
});
expect(routeLogsToStderrMock).toHaveBeenCalledTimes(1);
expect(emitCliBannerMock).toHaveBeenCalledWith("1.2.3", {
argv: ["node", "openclaw", "status"],
});
await mod.applyCliExecutionStartupPresentation({
startupPolicy: {
suppressDoctorStdout: false,
hideBanner: true,
skipConfigGuard: false,
loadPlugins: true,
},
version: "1.2.3",
showBanner: true,
});
expect(emitCliBannerMock).toHaveBeenCalledTimes(1);
});
it("forwards startup policy into bootstrap defaults and overrides", async () => {
const statusRuntime = {} as never;
await mod.ensureCliExecutionBootstrap({
runtime: statusRuntime,
commandPath: ["status"],
startupPolicy: {
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: true,
loadPlugins: false,
},
});
expect(ensureCliCommandBootstrapMock).toHaveBeenCalledWith({
runtime: statusRuntime,
commandPath: ["status"],
suppressDoctorStdout: true,
allowInvalid: undefined,
loadPlugins: false,
skipConfigGuard: true,
});
const messageRuntime = {} as never;
await mod.ensureCliExecutionBootstrap({
runtime: messageRuntime,
commandPath: ["message", "send"],
startupPolicy: {
suppressDoctorStdout: false,
hideBanner: false,
skipConfigGuard: false,
loadPlugins: false,
},
allowInvalid: true,
loadPlugins: true,
});
expect(ensureCliCommandBootstrapMock).toHaveBeenLastCalledWith({
runtime: messageRuntime,
commandPath: ["message", "send"],
suppressDoctorStdout: false,
allowInvalid: true,
loadPlugins: true,
skipConfigGuard: false,
});
});
});

View File

@@ -0,0 +1,64 @@
import { routeLogsToStderr } from "../logging/console.js";
import type { RuntimeInterface } from "../runtime.js";
import { getCommandPathWithRootOptions } from "./argv.js";
import { ensureCliCommandBootstrap } from "./command-bootstrap.js";
import { resolveCliStartupPolicy } from "./command-startup-policy.js";
type CliStartupPolicy = ReturnType<typeof resolveCliStartupPolicy>;
export function resolveCliExecutionStartupContext(params: {
argv: string[];
jsonOutputMode: boolean;
env?: NodeJS.ProcessEnv;
routeMode?: boolean;
}) {
const commandPath = getCommandPathWithRootOptions(params.argv, 2);
return {
commandPath,
startupPolicy: resolveCliStartupPolicy({
commandPath,
jsonOutputMode: params.jsonOutputMode,
env: params.env,
routeMode: params.routeMode,
}),
};
}
export async function applyCliExecutionStartupPresentation(params: {
argv?: string[];
routeLogsToStderrOnSuppress?: boolean;
startupPolicy: CliStartupPolicy;
showBanner?: boolean;
version?: string;
}) {
if (params.startupPolicy.suppressDoctorStdout && params.routeLogsToStderrOnSuppress !== false) {
routeLogsToStderr();
}
if (params.startupPolicy.hideBanner || params.showBanner === false || !params.version) {
return;
}
const { emitCliBanner } = await import("./banner.js");
if (params.argv) {
emitCliBanner(params.version, { argv: params.argv });
return;
}
emitCliBanner(params.version);
}
export async function ensureCliExecutionBootstrap(params: {
runtime: RuntimeInterface;
commandPath: string[];
startupPolicy: CliStartupPolicy;
allowInvalid?: boolean;
loadPlugins?: boolean;
skipConfigGuard?: boolean;
}) {
await ensureCliCommandBootstrap({
runtime: params.runtime,
commandPath: params.commandPath,
suppressDoctorStdout: params.startupPolicy.suppressDoctorStdout,
allowInvalid: params.allowInvalid,
loadPlugins: params.loadPlugins ?? params.startupPolicy.loadPlugins,
skipConfigGuard: params.skipConfigGuard ?? params.startupPolicy.skipConfigGuard,
});
}

View File

@@ -0,0 +1,114 @@
import { describe, expect, it } from "vitest";
import {
resolveCliStartupPolicy,
shouldBypassConfigGuardForCommandPath,
shouldEnsureCliPathForCommandPath,
shouldHideCliBannerForCommandPath,
shouldLoadPluginsForCommandPath,
shouldSkipRouteConfigGuardForCommandPath,
} from "./command-startup-policy.js";
describe("command-startup-policy", () => {
it("matches config guard bypass commands", () => {
expect(shouldBypassConfigGuardForCommandPath(["backup", "create"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["config", "validate"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["config", "schema"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["status"])).toBe(false);
});
it("matches route-first config guard skip policy", () => {
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["status"],
suppressDoctorStdout: true,
}),
).toBe(true);
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["gateway", "status"],
suppressDoctorStdout: false,
}),
).toBe(true);
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["status"],
suppressDoctorStdout: false,
}),
).toBe(false);
});
it("matches plugin preload policy", () => {
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["status"],
jsonOutputMode: false,
}),
).toBe(true);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["status"],
jsonOutputMode: true,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["channels", "add"],
jsonOutputMode: false,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["agents", "list"],
jsonOutputMode: false,
}),
).toBe(true);
});
it("matches banner suppression policy", () => {
expect(shouldHideCliBannerForCommandPath(["update", "status"])).toBe(true);
expect(shouldHideCliBannerForCommandPath(["completion"])).toBe(true);
expect(
shouldHideCliBannerForCommandPath(["status"], {
...process.env,
OPENCLAW_HIDE_BANNER: "1",
}),
).toBe(true);
expect(shouldHideCliBannerForCommandPath(["status"], {})).toBe(false);
});
it("matches CLI PATH bootstrap policy", () => {
expect(shouldEnsureCliPathForCommandPath(["status"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["sessions"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["config", "get"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["models", "status"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["message", "send"])).toBe(true);
expect(shouldEnsureCliPathForCommandPath([])).toBe(true);
});
it("aggregates startup policy for commander and route-first callers", () => {
expect(
resolveCliStartupPolicy({
commandPath: ["status"],
jsonOutputMode: true,
}),
).toEqual({
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: false,
loadPlugins: false,
});
expect(
resolveCliStartupPolicy({
commandPath: ["status"],
jsonOutputMode: true,
routeMode: true,
}),
).toEqual({
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: true,
loadPlugins: false,
});
});
});

View File

@@ -0,0 +1,101 @@
import { isTruthyEnvValue } from "../infra/env.js";
const PLUGIN_REQUIRED_COMMANDS = new Set([
"agent",
"message",
"channels",
"directory",
"agents",
"configure",
"status",
"health",
]);
const CONFIG_GUARD_BYPASS_COMMANDS = new Set(["backup", "doctor", "completion", "secrets"]);
export function shouldBypassConfigGuardForCommandPath(commandPath: string[]): boolean {
const [primary, secondary] = commandPath;
if (!primary) {
return false;
}
if (CONFIG_GUARD_BYPASS_COMMANDS.has(primary)) {
return true;
}
return primary === "config" && (secondary === "validate" || secondary === "schema");
}
export function shouldSkipRouteConfigGuardForCommandPath(params: {
commandPath: string[];
suppressDoctorStdout: boolean;
}): boolean {
return (
(params.commandPath[0] === "status" && params.suppressDoctorStdout) ||
(params.commandPath[0] === "gateway" && params.commandPath[1] === "status")
);
}
export function shouldLoadPluginsForCommandPath(params: {
commandPath: string[];
jsonOutputMode: boolean;
}): boolean {
const [primary, secondary] = params.commandPath;
if (!primary || !PLUGIN_REQUIRED_COMMANDS.has(primary)) {
return false;
}
if ((primary === "status" || primary === "health") && params.jsonOutputMode) {
return false;
}
return !(primary === "onboard" || (primary === "channels" && secondary === "add"));
}
export function shouldHideCliBannerForCommandPath(
commandPath: string[],
env: NodeJS.ProcessEnv = process.env,
): boolean {
return (
isTruthyEnvValue(env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||
commandPath[0] === "completion" ||
(commandPath[0] === "plugins" && commandPath[1] === "update")
);
}
export function shouldEnsureCliPathForCommandPath(commandPath: string[]): boolean {
const [primary, secondary] = commandPath;
if (!primary) {
return true;
}
if (primary === "status" || primary === "health" || primary === "sessions") {
return false;
}
if (primary === "config" && (secondary === "get" || secondary === "unset")) {
return false;
}
if (primary === "models" && (secondary === "list" || secondary === "status")) {
return false;
}
return true;
}
export function resolveCliStartupPolicy(params: {
commandPath: string[];
jsonOutputMode: boolean;
env?: NodeJS.ProcessEnv;
routeMode?: boolean;
}) {
const suppressDoctorStdout = params.jsonOutputMode;
return {
suppressDoctorStdout,
hideBanner: shouldHideCliBannerForCommandPath(params.commandPath, params.env),
skipConfigGuard: params.routeMode
? shouldSkipRouteConfigGuardForCommandPath({
commandPath: params.commandPath,
suppressDoctorStdout,
})
: false,
loadPlugins: shouldLoadPluginsForCommandPath({
commandPath: params.commandPath,
jsonOutputMode: params.jsonOutputMode,
}),
};
}

View File

@@ -0,0 +1,66 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const ensurePluginRegistryLoadedMock = vi.hoisted(() => vi.fn());
vi.mock("./plugin-registry.js", () => ({
ensurePluginRegistryLoaded: ensurePluginRegistryLoadedMock,
}));
describe("plugin-registry-loader", () => {
let originalForceStderr: boolean;
let ensureCliPluginRegistryLoaded: typeof import("./plugin-registry-loader.js").ensureCliPluginRegistryLoaded;
let resolvePluginRegistryScopeForCommandPath: typeof import("./plugin-registry-loader.js").resolvePluginRegistryScopeForCommandPath;
let loggingState: typeof import("../logging/state.js").loggingState;
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
({ ensureCliPluginRegistryLoaded, resolvePluginRegistryScopeForCommandPath } =
await import("./plugin-registry-loader.js"));
({ loggingState } = await import("../logging/state.js"));
originalForceStderr = loggingState.forceConsoleToStderr;
loggingState.forceConsoleToStderr = false;
});
afterEach(() => {
loggingState.forceConsoleToStderr = originalForceStderr;
});
it("routes plugin load logs to stderr and restores state", async () => {
const captured: boolean[] = [];
ensurePluginRegistryLoadedMock.mockImplementation(() => {
captured.push(loggingState.forceConsoleToStderr);
});
await ensureCliPluginRegistryLoaded({
scope: "configured-channels",
routeLogsToStderr: true,
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({
scope: "configured-channels",
});
expect(captured).toEqual([true]);
expect(loggingState.forceConsoleToStderr).toBe(false);
});
it("keeps stdout routing unchanged when stderr routing is not requested", async () => {
const captured: boolean[] = [];
ensurePluginRegistryLoadedMock.mockImplementation(() => {
captured.push(loggingState.forceConsoleToStderr);
});
await ensureCliPluginRegistryLoaded({
scope: "all",
});
expect(captured).toEqual([false]);
expect(loggingState.forceConsoleToStderr).toBe(false);
});
it("maps command paths to plugin registry scopes", () => {
expect(resolvePluginRegistryScopeForCommandPath(["status"])).toBe("channels");
expect(resolvePluginRegistryScopeForCommandPath(["health"])).toBe("channels");
expect(resolvePluginRegistryScopeForCommandPath(["agents"])).toBe("all");
});
});

View File

@@ -0,0 +1,31 @@
import { loggingState } from "../logging/state.js";
import type { PluginRegistryScope } from "./plugin-registry.js";
let pluginRegistryModulePromise: Promise<typeof import("./plugin-registry.js")> | undefined;
function loadPluginRegistryModule() {
pluginRegistryModulePromise ??= import("./plugin-registry.js");
return pluginRegistryModulePromise;
}
export function resolvePluginRegistryScopeForCommandPath(
commandPath: string[],
): Exclude<PluginRegistryScope, "configured-channels"> {
return commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all";
}
export async function ensureCliPluginRegistryLoaded(params: {
scope: PluginRegistryScope;
routeLogsToStderr?: boolean;
}) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
const previousForceStderr = loggingState.forceConsoleToStderr;
if (params.routeLogsToStderr) {
loggingState.forceConsoleToStderr = true;
}
try {
ensurePluginRegistryLoaded({ scope: params.scope });
} finally {
loggingState.forceConsoleToStderr = previousForceStderr;
}
}

View File

@@ -1,13 +1,15 @@
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 { loggingState } from "../../logging/state.js";
import { defaultRuntime } from "../../runtime.js";
import { getCommandPathWithRootOptions, getVerboseFlag, hasHelpOrVersion } from "../argv.js";
import { emitCliBanner } from "../banner.js";
import { getVerboseFlag, hasHelpOrVersion } from "../argv.js";
import { resolveCliName } from "../cli-name.js";
import {
applyCliExecutionStartupPresentation,
ensureCliExecutionBootstrap,
resolveCliExecutionStartupContext,
} from "../command-execution-startup.js";
import { shouldBypassConfigGuardForCommandPath } from "../command-startup-policy.js";
import {
resolvePluginInstallInvalidConfigPolicy,
resolvePluginInstallPreactionRequest,
@@ -27,63 +29,6 @@ function setProcessTitleForCommand(actionCommand: Command) {
process.title = `${cliName}-${name}`;
}
// Commands that need plugins loaded before execution.
const PLUGIN_REQUIRED_COMMANDS = new Set([
"agent",
"message",
"channels",
"directory",
"agents",
"configure",
"status",
"health",
]);
const CONFIG_GUARD_BYPASS_COMMANDS = new Set(["backup", "doctor", "completion", "secrets"]);
let configGuardModulePromise: Promise<typeof import("./config-guard.js")> | undefined;
let pluginRegistryModulePromise: Promise<typeof import("../plugin-registry.js")> | undefined;
function shouldBypassConfigGuard(commandPath: string[]): boolean {
const [primary, secondary] = commandPath;
if (!primary) {
return false;
}
if (CONFIG_GUARD_BYPASS_COMMANDS.has(primary)) {
return true;
}
if (primary === "config" && (secondary === "validate" || secondary === "schema")) {
return true;
}
return false;
}
function loadConfigGuardModule() {
configGuardModulePromise ??= import("./config-guard.js");
return configGuardModulePromise;
}
function loadPluginRegistryModule() {
pluginRegistryModulePromise ??= import("../plugin-registry.js");
return pluginRegistryModulePromise;
}
function resolvePluginRegistryScope(commandPath: string[]): "channels" | "all" {
return commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all";
}
function shouldLoadPluginsForCommand(commandPath: string[], jsonOutputMode: boolean): boolean {
const [primary, secondary] = commandPath;
if (!primary || !PLUGIN_REQUIRED_COMMANDS.has(primary)) {
return false;
}
if ((primary === "status" || primary === "health") && jsonOutputMode) {
return false;
}
// Setup wizard and channels add should stay manifest-first and load selected plugins on demand.
if (primary === "onboard" || (primary === "channels" && secondary === "add")) {
return false;
}
return true;
}
function shouldAllowInvalidConfigForAction(actionCommand: Command, commandPath: string[]): boolean {
return (
resolvePluginInstallInvalidConfigPolicy(
@@ -123,19 +68,16 @@ export function registerPreActionHooks(program: Command, programVersion: string)
if (hasHelpOrVersion(argv)) {
return;
}
const commandPath = getCommandPathWithRootOptions(argv, 2);
const jsonOutputMode = isCommandJsonOutputMode(actionCommand, argv);
if (jsonOutputMode) {
routeLogsToStderr();
}
const hideBanner =
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||
commandPath[0] === "completion" ||
(commandPath[0] === "plugins" && commandPath[1] === "update");
if (!hideBanner) {
emitCliBanner(programVersion);
}
const { commandPath, startupPolicy } = resolveCliExecutionStartupContext({
argv,
jsonOutputMode,
env: process.env,
});
await applyCliExecutionStartupPresentation({
startupPolicy,
version: programVersion,
});
const verbose = getVerboseFlag(argv, { includeDebug: true });
setVerbose(verbose);
const cliLogLevel = getCliLogLevel(actionCommand);
@@ -145,31 +87,14 @@ export function registerPreActionHooks(program: Command, programVersion: string)
if (!verbose) {
process.env.NODE_NO_WARNINGS ??= "1";
}
if (shouldBypassConfigGuard(commandPath)) {
if (shouldBypassConfigGuardForCommandPath(commandPath)) {
return;
}
const allowInvalid = shouldAllowInvalidConfigForAction(actionCommand, commandPath);
const { ensureConfigReady } = await loadConfigGuardModule();
await ensureConfigReady({
await ensureCliExecutionBootstrap({
runtime: defaultRuntime,
commandPath,
...(allowInvalid ? { allowInvalid: true } : {}),
...(jsonOutputMode ? { suppressDoctorStdout: true } : {}),
startupPolicy,
allowInvalid: shouldAllowInvalidConfigForAction(actionCommand, commandPath),
});
// Load plugins for commands that need channel access.
// When --json output is active, temporarily route logs to stderr so plugin
// registration messages don't corrupt the JSON payload on stdout.
if (shouldLoadPluginsForCommand(commandPath, jsonOutputMode)) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
const prev = loggingState.forceConsoleToStderr;
if (jsonOutputMode) {
loggingState.forceConsoleToStderr = true;
}
try {
ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) });
} finally {
loggingState.forceConsoleToStderr = prev;
}
}
});
}

View File

@@ -0,0 +1,210 @@
import { describe, expect, it } from "vitest";
import {
parseAgentsListRouteArgs,
parseConfigGetRouteArgs,
parseConfigUnsetRouteArgs,
parseGatewayStatusRouteArgs,
parseHealthRouteArgs,
parseModelsListRouteArgs,
parseModelsStatusRouteArgs,
parseSessionsRouteArgs,
parseStatusRouteArgs,
} from "./route-args.js";
describe("route-args", () => {
it("parses health and status route args", () => {
expect(
parseHealthRouteArgs(["node", "openclaw", "health", "--json", "--timeout", "5000"]),
).toEqual({
json: true,
verbose: false,
timeoutMs: 5000,
});
expect(
parseStatusRouteArgs([
"node",
"openclaw",
"status",
"--json",
"--deep",
"--all",
"--usage",
"--timeout",
"5000",
]),
).toEqual({
json: true,
deep: true,
all: true,
usage: true,
verbose: false,
timeoutMs: 5000,
});
expect(parseStatusRouteArgs(["node", "openclaw", "status", "--timeout"])).toBeNull();
});
it("parses gateway status route args and rejects probe-only ssh flags", () => {
expect(
parseGatewayStatusRouteArgs([
"node",
"openclaw",
"gateway",
"status",
"--url",
"ws://127.0.0.1:18789",
"--token",
"abc",
"--password",
"def",
"--timeout",
"5000",
"--deep",
"--require-rpc",
"--json",
]),
).toEqual({
rpc: {
url: "ws://127.0.0.1:18789",
token: "abc",
password: "def",
timeout: "5000",
},
probe: true,
requireRpc: true,
deep: true,
json: true,
});
expect(
parseGatewayStatusRouteArgs(["node", "openclaw", "gateway", "status", "--ssh", "host"]),
).toBeNull();
expect(
parseGatewayStatusRouteArgs(["node", "openclaw", "gateway", "status", "--ssh-auto"]),
).toBeNull();
});
it("parses sessions and agents list route args", () => {
expect(
parseSessionsRouteArgs([
"node",
"openclaw",
"sessions",
"--json",
"--all-agents",
"--agent",
"default",
"--store",
"sqlite",
"--active",
"true",
]),
).toEqual({
json: true,
allAgents: true,
agent: "default",
store: "sqlite",
active: "true",
});
expect(parseSessionsRouteArgs(["node", "openclaw", "sessions", "--agent"])).toBeNull();
expect(
parseAgentsListRouteArgs(["node", "openclaw", "agents", "list", "--json", "--bindings"]),
).toEqual({
json: true,
bindings: true,
});
});
it("parses config routes", () => {
expect(
parseConfigGetRouteArgs([
"node",
"openclaw",
"--log-level",
"debug",
"config",
"get",
"update.channel",
"--json",
]),
).toEqual({
path: "update.channel",
json: true,
});
expect(
parseConfigUnsetRouteArgs([
"node",
"openclaw",
"config",
"unset",
"--profile",
"work",
"update.channel",
]),
).toEqual({
path: "update.channel",
});
expect(parseConfigGetRouteArgs(["node", "openclaw", "config", "get", "--json"])).toBeNull();
});
it("parses models list and models status route args", () => {
expect(
parseModelsListRouteArgs([
"node",
"openclaw",
"models",
"list",
"--provider",
"openai",
"--all",
"--local",
"--json",
"--plain",
]),
).toEqual({
provider: "openai",
all: true,
local: true,
json: true,
plain: true,
});
expect(
parseModelsStatusRouteArgs([
"node",
"openclaw",
"models",
"status",
"--probe-provider",
"openai",
"--probe-timeout",
"5000",
"--probe-concurrency",
"2",
"--probe-max-tokens",
"64",
"--probe-profile",
"fast",
"--probe-profile",
"safe",
"--agent",
"default",
"--json",
"--plain",
"--check",
"--probe",
]),
).toEqual({
probeProvider: "openai",
probeTimeout: "5000",
probeConcurrency: "2",
probeMaxTokens: "64",
probeProfile: ["fast", "safe"],
agent: "default",
json: true,
plain: true,
check: true,
probe: true,
});
expect(
parseModelsStatusRouteArgs(["node", "openclaw", "models", "status", "--probe-profile"]),
).toBeNull();
});
});

View File

@@ -0,0 +1,244 @@
import { isValueToken } from "../../infra/cli-root-options.js";
import {
getCommandPositionalsWithRootOptions,
getFlagValue,
getPositiveIntFlagValue,
getVerboseFlag,
hasFlag,
} from "../argv.js";
type OptionalFlagParse = {
ok: boolean;
value?: string;
};
function parseOptionalFlagValue(argv: string[], name: string): OptionalFlagParse {
const value = getFlagValue(argv, name);
if (value === null) {
return { ok: false };
}
return { ok: true, value };
}
function parseRepeatedFlagValues(argv: string[], name: string): string[] | null {
const values: string[] = [];
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (arg === name) {
const next = args[i + 1];
if (!isValueToken(next)) {
return null;
}
values.push(next);
i += 1;
continue;
}
if (arg.startsWith(`${name}=`)) {
const value = arg.slice(name.length + 1).trim();
if (!value) {
return null;
}
values.push(value);
}
}
return values;
}
function parseSinglePositional(
argv: string[],
params: {
commandPath: string[];
booleanFlags?: string[];
},
): string | null {
const positionals = getCommandPositionalsWithRootOptions(argv, params);
if (!positionals || positionals.length !== 1) {
return null;
}
return positionals[0] ?? null;
}
export function parseHealthRouteArgs(argv: string[]) {
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) {
return null;
}
return {
json: hasFlag(argv, "--json"),
verbose: getVerboseFlag(argv, { includeDebug: true }),
timeoutMs,
};
}
export function parseStatusRouteArgs(argv: string[]) {
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) {
return null;
}
return {
json: hasFlag(argv, "--json"),
deep: hasFlag(argv, "--deep"),
all: hasFlag(argv, "--all"),
usage: hasFlag(argv, "--usage"),
verbose: getVerboseFlag(argv, { includeDebug: true }),
timeoutMs,
};
}
export function parseGatewayStatusRouteArgs(argv: string[]) {
const url = parseOptionalFlagValue(argv, "--url");
if (!url.ok) {
return null;
}
const token = parseOptionalFlagValue(argv, "--token");
if (!token.ok) {
return null;
}
const password = parseOptionalFlagValue(argv, "--password");
if (!password.ok) {
return null;
}
const timeout = parseOptionalFlagValue(argv, "--timeout");
if (!timeout.ok) {
return null;
}
const ssh = parseOptionalFlagValue(argv, "--ssh");
if (!ssh.ok || ssh.value !== undefined) {
return null;
}
const sshIdentity = parseOptionalFlagValue(argv, "--ssh-identity");
if (!sshIdentity.ok || sshIdentity.value !== undefined) {
return null;
}
if (hasFlag(argv, "--ssh-auto")) {
return null;
}
return {
rpc: {
url: url.value,
token: token.value,
password: password.value,
timeout: timeout.value,
},
deep: hasFlag(argv, "--deep"),
json: hasFlag(argv, "--json"),
requireRpc: hasFlag(argv, "--require-rpc"),
probe: !hasFlag(argv, "--no-probe"),
};
}
export function parseSessionsRouteArgs(argv: string[]) {
const agent = parseOptionalFlagValue(argv, "--agent");
if (!agent.ok) {
return null;
}
const store = parseOptionalFlagValue(argv, "--store");
if (!store.ok) {
return null;
}
const active = parseOptionalFlagValue(argv, "--active");
if (!active.ok) {
return null;
}
return {
json: hasFlag(argv, "--json"),
allAgents: hasFlag(argv, "--all-agents"),
agent: agent.value,
store: store.value,
active: active.value,
};
}
export function parseAgentsListRouteArgs(argv: string[]) {
return {
json: hasFlag(argv, "--json"),
bindings: hasFlag(argv, "--bindings"),
};
}
export function parseConfigGetRouteArgs(argv: string[]) {
const path = parseSinglePositional(argv, {
commandPath: ["config", "get"],
booleanFlags: ["--json"],
});
if (!path) {
return null;
}
return {
path,
json: hasFlag(argv, "--json"),
};
}
export function parseConfigUnsetRouteArgs(argv: string[]) {
const path = parseSinglePositional(argv, {
commandPath: ["config", "unset"],
});
if (!path) {
return null;
}
return { path };
}
export function parseModelsListRouteArgs(argv: string[]) {
const provider = parseOptionalFlagValue(argv, "--provider");
if (!provider.ok) {
return null;
}
return {
provider: provider.value,
all: hasFlag(argv, "--all"),
local: hasFlag(argv, "--local"),
json: hasFlag(argv, "--json"),
plain: hasFlag(argv, "--plain"),
};
}
export function parseModelsStatusRouteArgs(argv: string[]) {
const probeProvider = parseOptionalFlagValue(argv, "--probe-provider");
if (!probeProvider.ok) {
return null;
}
const probeTimeout = parseOptionalFlagValue(argv, "--probe-timeout");
if (!probeTimeout.ok) {
return null;
}
const probeConcurrency = parseOptionalFlagValue(argv, "--probe-concurrency");
if (!probeConcurrency.ok) {
return null;
}
const probeMaxTokens = parseOptionalFlagValue(argv, "--probe-max-tokens");
if (!probeMaxTokens.ok) {
return null;
}
const agent = parseOptionalFlagValue(argv, "--agent");
if (!agent.ok) {
return null;
}
const probeProfileValues = parseRepeatedFlagValues(argv, "--probe-profile");
if (probeProfileValues === null) {
return null;
}
const probeProfile =
probeProfileValues.length === 0
? undefined
: probeProfileValues.length === 1
? probeProfileValues[0]
: probeProfileValues;
return {
probeProvider: probeProvider.value,
probeTimeout: probeTimeout.value,
probeConcurrency: probeConcurrency.value,
probeMaxTokens: probeMaxTokens.value,
agent: agent.value,
probeProfile,
json: hasFlag(argv, "--json"),
plain: hasFlag(argv, "--plain"),
check: hasFlag(argv, "--check"),
probe: hasFlag(argv, "--probe"),
};
}

View File

@@ -1,12 +1,17 @@
import { isValueToken } from "../../infra/cli-root-options.js";
import { defaultRuntime } from "../../runtime.js";
import { hasFlag } from "../argv.js";
import { shouldLoadPluginsForCommandPath } from "../command-startup-policy.js";
import {
getCommandPositionalsWithRootOptions,
getFlagValue,
getPositiveIntFlagValue,
getVerboseFlag,
hasFlag,
} from "../argv.js";
parseAgentsListRouteArgs,
parseConfigGetRouteArgs,
parseConfigUnsetRouteArgs,
parseGatewayStatusRouteArgs,
parseHealthRouteArgs,
parseModelsListRouteArgs,
parseModelsStatusRouteArgs,
parseSessionsRouteArgs,
parseStatusRouteArgs,
} from "./route-args.js";
export type RouteSpec = {
match: (path: string[]) => boolean;
@@ -18,16 +23,18 @@ const routeHealth: RouteSpec = {
match: (path) => path[0] === "health",
// `health --json` only relays gateway RPC output and does not need local plugin metadata.
// Keep plugin preload for text output where channel diagnostics/logSelfId are rendered.
loadPlugins: (argv) => !hasFlag(argv, "--json"),
loadPlugins: (argv) =>
shouldLoadPluginsForCommandPath({
commandPath: ["health"],
jsonOutputMode: hasFlag(argv, "--json"),
}),
run: async (argv) => {
const json = hasFlag(argv, "--json");
const verbose = getVerboseFlag(argv, { includeDebug: true });
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) {
const args = parseHealthRouteArgs(argv);
if (!args) {
return false;
}
const { healthCommand } = await import("../../commands/health.js");
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
await healthCommand(args, defaultRuntime);
return true;
},
};
@@ -36,24 +43,31 @@ const routeStatus: RouteSpec = {
match: (path) => path[0] === "status",
// `status --json` can defer channel plugin loading until config/env inspection
// proves it is needed, which keeps the fast-path startup lightweight.
loadPlugins: (argv) => !hasFlag(argv, "--json"),
loadPlugins: (argv) =>
shouldLoadPluginsForCommandPath({
commandPath: ["status"],
jsonOutputMode: hasFlag(argv, "--json"),
}),
run: async (argv) => {
const json = hasFlag(argv, "--json");
const deep = hasFlag(argv, "--deep");
const all = hasFlag(argv, "--all");
const usage = hasFlag(argv, "--usage");
const verbose = getVerboseFlag(argv, { includeDebug: true });
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) {
const args = parseStatusRouteArgs(argv);
if (!args) {
return false;
}
if (json) {
if (args.json) {
const { statusJsonCommand } = await import("../../commands/status-json.js");
await statusJsonCommand({ deep, all, usage, timeoutMs }, defaultRuntime);
await statusJsonCommand(
{
deep: args.deep,
all: args.all,
usage: args.usage,
timeoutMs: args.timeoutMs,
},
defaultRuntime,
);
return true;
}
const { statusCommand } = await import("../../commands/status.js");
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
await statusCommand(args, defaultRuntime);
return true;
},
};
@@ -61,56 +75,12 @@ const routeStatus: RouteSpec = {
const routeGatewayStatus: RouteSpec = {
match: (path) => path[0] === "gateway" && path[1] === "status",
run: async (argv) => {
const url = getFlagValue(argv, "--url");
if (url === null) {
const args = parseGatewayStatusRouteArgs(argv);
if (!args) {
return false;
}
const token = getFlagValue(argv, "--token");
if (token === null) {
return false;
}
const password = getFlagValue(argv, "--password");
if (password === null) {
return false;
}
const timeout = getFlagValue(argv, "--timeout");
if (timeout === null) {
return false;
}
const ssh = getFlagValue(argv, "--ssh");
if (ssh === null) {
return false;
}
if (ssh !== undefined) {
return false;
}
const sshIdentity = getFlagValue(argv, "--ssh-identity");
if (sshIdentity === null) {
return false;
}
if (sshIdentity !== undefined) {
return false;
}
if (hasFlag(argv, "--ssh-auto")) {
return false;
}
const deep = hasFlag(argv, "--deep");
const json = hasFlag(argv, "--json");
const requireRpc = hasFlag(argv, "--require-rpc");
const probe = !hasFlag(argv, "--no-probe");
const { runDaemonStatus } = await import("../daemon-cli/status.js");
await runDaemonStatus({
rpc: {
url: url ?? undefined,
token: token ?? undefined,
password: password ?? undefined,
timeout: timeout ?? undefined,
},
probe,
requireRpc,
deep,
json,
});
await runDaemonStatus(args);
return true;
},
};
@@ -120,22 +90,12 @@ const routeSessions: RouteSpec = {
// must fall through to Commander so nested handlers run.
match: (path) => path[0] === "sessions" && !path[1],
run: async (argv) => {
const json = hasFlag(argv, "--json");
const allAgents = hasFlag(argv, "--all-agents");
const agent = getFlagValue(argv, "--agent");
if (agent === null) {
return false;
}
const store = getFlagValue(argv, "--store");
if (store === null) {
return false;
}
const active = getFlagValue(argv, "--active");
if (active === null) {
const args = parseSessionsRouteArgs(argv);
if (!args) {
return false;
}
const { sessionsCommand } = await import("../../commands/sessions.js");
await sessionsCommand({ json, store, agent, allAgents, active }, defaultRuntime);
await sessionsCommand(args, defaultRuntime);
return true;
},
};
@@ -143,59 +103,21 @@ const routeSessions: RouteSpec = {
const routeAgentsList: RouteSpec = {
match: (path) => path[0] === "agents" && path[1] === "list",
run: async (argv) => {
const json = hasFlag(argv, "--json");
const bindings = hasFlag(argv, "--bindings");
const { agentsListCommand } = await import("../../commands/agents.js");
await agentsListCommand({ json, bindings }, defaultRuntime);
await agentsListCommand(parseAgentsListRouteArgs(argv), defaultRuntime);
return true;
},
};
function getFlagValues(argv: string[], name: string): string[] | null {
const values: string[] = [];
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (arg === name) {
const next = args[i + 1];
if (!isValueToken(next)) {
return null;
}
values.push(next);
i += 1;
continue;
}
if (arg.startsWith(`${name}=`)) {
const value = arg.slice(name.length + 1).trim();
if (!value) {
return null;
}
values.push(value);
}
}
return values;
}
const routeConfigGet: RouteSpec = {
match: (path) => path[0] === "config" && path[1] === "get",
run: async (argv) => {
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath: ["config", "get"],
booleanFlags: ["--json"],
});
if (!positionals || positionals.length !== 1) {
const args = parseConfigGetRouteArgs(argv);
if (!args) {
return false;
}
const pathArg = positionals[0];
if (!pathArg) {
return false;
}
const json = hasFlag(argv, "--json");
const { runConfigGet } = await import("../config-cli.js");
await runConfigGet({ path: pathArg, json });
await runConfigGet(args);
return true;
},
};
@@ -203,18 +125,12 @@ const routeConfigGet: RouteSpec = {
const routeConfigUnset: RouteSpec = {
match: (path) => path[0] === "config" && path[1] === "unset",
run: async (argv) => {
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath: ["config", "unset"],
});
if (!positionals || positionals.length !== 1) {
return false;
}
const pathArg = positionals[0];
if (!pathArg) {
const args = parseConfigUnsetRouteArgs(argv);
if (!args) {
return false;
}
const { runConfigUnset } = await import("../config-cli.js");
await runConfigUnset({ path: pathArg });
await runConfigUnset(args);
return true;
},
};
@@ -222,16 +138,12 @@ const routeConfigUnset: RouteSpec = {
const routeModelsList: RouteSpec = {
match: (path) => path[0] === "models" && path[1] === "list",
run: async (argv) => {
const provider = getFlagValue(argv, "--provider");
if (provider === null) {
const args = parseModelsListRouteArgs(argv);
if (!args) {
return false;
}
const all = hasFlag(argv, "--all");
const local = hasFlag(argv, "--local");
const json = hasFlag(argv, "--json");
const plain = hasFlag(argv, "--plain");
const { modelsListCommand } = await import("../../commands/models.js");
await modelsListCommand({ all, local, provider, json, plain }, defaultRuntime);
await modelsListCommand(args, defaultRuntime);
return true;
},
};
@@ -239,56 +151,12 @@ const routeModelsList: RouteSpec = {
const routeModelsStatus: RouteSpec = {
match: (path) => path[0] === "models" && path[1] === "status",
run: async (argv) => {
const probeProvider = getFlagValue(argv, "--probe-provider");
if (probeProvider === null) {
const args = parseModelsStatusRouteArgs(argv);
if (!args) {
return false;
}
const probeTimeout = getFlagValue(argv, "--probe-timeout");
if (probeTimeout === null) {
return false;
}
const probeConcurrency = getFlagValue(argv, "--probe-concurrency");
if (probeConcurrency === null) {
return false;
}
const probeMaxTokens = getFlagValue(argv, "--probe-max-tokens");
if (probeMaxTokens === null) {
return false;
}
const agent = getFlagValue(argv, "--agent");
if (agent === null) {
return false;
}
const probeProfileValues = getFlagValues(argv, "--probe-profile");
if (probeProfileValues === null) {
return false;
}
const probeProfile =
probeProfileValues.length === 0
? undefined
: probeProfileValues.length === 1
? probeProfileValues[0]
: probeProfileValues;
const json = hasFlag(argv, "--json");
const plain = hasFlag(argv, "--plain");
const check = hasFlag(argv, "--check");
const probe = hasFlag(argv, "--probe");
const { modelsStatusCommand } = await import("../../commands/models.js");
await modelsStatusCommand(
{
json,
plain,
check,
probe,
probeProvider,
probeProfile,
probeTimeout,
probeConcurrency,
probeMaxTokens,
agent,
},
defaultRuntime,
);
await modelsStatusCommand(args, defaultRuntime);
return true;
},
};

View File

@@ -38,12 +38,15 @@ describe("tryRouteCli", () => {
// Capture the same reference that route.js uses.
let loggingState: typeof import("../logging/state.js").loggingState;
let originalDisableRouteFirst: string | undefined;
let originalHideBanner: string | undefined;
let originalForceStderr: boolean;
beforeEach(async () => {
vi.clearAllMocks();
originalDisableRouteFirst = process.env.OPENCLAW_DISABLE_ROUTE_FIRST;
originalHideBanner = process.env.OPENCLAW_HIDE_BANNER;
delete process.env.OPENCLAW_DISABLE_ROUTE_FIRST;
delete process.env.OPENCLAW_HIDE_BANNER;
vi.resetModules();
({ tryRouteCli } = await import("./route.js"));
({ loggingState } = await import("../logging/state.js"));
@@ -64,6 +67,11 @@ describe("tryRouteCli", () => {
} else {
process.env.OPENCLAW_DISABLE_ROUTE_FIRST = originalDisableRouteFirst;
}
if (originalHideBanner === undefined) {
delete process.env.OPENCLAW_HIDE_BANNER;
} else {
process.env.OPENCLAW_HIDE_BANNER = originalHideBanner;
}
});
it("skips config guard for routed status --json commands", async () => {
@@ -133,4 +141,12 @@ describe("tryRouteCli", () => {
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
it("respects OPENCLAW_HIDE_BANNER for routed commands", async () => {
process.env.OPENCLAW_HIDE_BANNER = "1";
await expect(tryRouteCli(["node", "openclaw", "status"])).resolves.toBe(true);
expect(emitCliBannerMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,11 @@
import { isTruthyEnvValue } from "../infra/env.js";
import { loggingState } from "../logging/state.js";
import { defaultRuntime } from "../runtime.js";
import { getCommandPathWithRootOptions, hasFlag, hasHelpOrVersion } from "./argv.js";
import {
applyCliExecutionStartupPresentation,
ensureCliExecutionBootstrap,
resolveCliExecutionStartupContext,
} from "./command-execution-startup.js";
import { findRoutedCommand } from "./program/routes.js";
async function prepareRoutedCommand(params: {
@@ -9,44 +13,28 @@ async function prepareRoutedCommand(params: {
commandPath: string[];
loadPlugins?: boolean | ((argv: string[]) => boolean);
}) {
const suppressDoctorStdout = hasFlag(params.argv, "--json");
const skipConfigGuard =
(params.commandPath[0] === "status" && suppressDoctorStdout) ||
(params.commandPath[0] === "gateway" && params.commandPath[1] === "status");
if (!suppressDoctorStdout && process.stdout.isTTY) {
const [{ emitCliBanner }, { VERSION }] = await Promise.all([
import("./banner.js"),
import("../version.js"),
]);
emitCliBanner(VERSION, { argv: params.argv });
}
if (!skipConfigGuard) {
const { ensureConfigReady } = await import("./program/config-guard.js");
await ensureConfigReady({
runtime: defaultRuntime,
commandPath: params.commandPath,
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
});
}
const { startupPolicy } = resolveCliExecutionStartupContext({
argv: params.argv,
jsonOutputMode: hasFlag(params.argv, "--json"),
env: process.env,
routeMode: true,
});
const { VERSION } = await import("../version.js");
await applyCliExecutionStartupPresentation({
argv: params.argv,
routeLogsToStderrOnSuppress: false,
startupPolicy,
showBanner: process.stdout.isTTY && !startupPolicy.suppressDoctorStdout,
version: VERSION,
});
const shouldLoadPlugins =
typeof params.loadPlugins === "function" ? params.loadPlugins(params.argv) : params.loadPlugins;
if (shouldLoadPlugins) {
const { ensurePluginRegistryLoaded } = await import("./plugin-registry.js");
const prev = loggingState.forceConsoleToStderr;
if (suppressDoctorStdout) {
loggingState.forceConsoleToStderr = true;
}
try {
ensurePluginRegistryLoaded({
scope:
params.commandPath[0] === "status" || params.commandPath[0] === "health"
? "channels"
: "all",
});
} finally {
loggingState.forceConsoleToStderr = prev;
}
}
await ensureCliExecutionBootstrap({
runtime: defaultRuntime,
commandPath: params.commandPath,
startupPolicy,
loadPlugins: shouldLoadPlugins ?? startupPolicy.loadPlugins,
});
}
export async function tryRouteCli(argv: string[]): Promise<boolean> {

View File

@@ -1,4 +1,5 @@
import { hasPotentialConfiguredChannels } from "../channels/config-presence.js";
import { ensureCliPluginRegistryLoaded } from "../cli/plugin-registry-loader.js";
import type { RuntimeEnv } from "../runtime.js";
import { executeStatusScanFromOverview } from "./status.scan-execute.ts";
import {
@@ -7,12 +8,6 @@ import {
} from "./status.scan-memory.ts";
import { collectStatusScanOverview } from "./status.scan-overview.ts";
import type { StatusScanResult } from "./status.scan-result.ts";
let pluginRegistryModulePromise: Promise<typeof import("../cli/plugin-registry.js")> | undefined;
function loadPluginRegistryModule() {
pluginRegistryModulePromise ??= import("../cli/plugin-registry.js");
return pluginRegistryModulePromise;
}
type StatusJsonScanPolicy = {
commandName: string;
@@ -41,15 +36,10 @@ export async function scanStatusJsonWithPolicy(
includeChannelsData: false,
});
if (overview.hasConfiguredChannels) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
const { loggingState } = await import("../logging/state.js");
const previousForceStderr = loggingState.forceConsoleToStderr;
loggingState.forceConsoleToStderr = true;
try {
ensurePluginRegistryLoaded({ scope: "configured-channels" });
} finally {
loggingState.forceConsoleToStderr = previousForceStderr;
}
await ensureCliPluginRegistryLoaded({
scope: "configured-channels",
routeLogsToStderr: true,
});
}
return await executeStatusScanFromOverview({