mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* perf(state): preserve prepared queries during healthy reads Inspect the legacy Workshop index without toggling SQLite catalog flags on healthy handles. Retain the existing bounded repair admission when malformed schema blocks ordinary reads. A native-authorizer regression covers prepared-query reuse. * fix(state): keep legacy catalog repair in Doctor * refactor(state): use native repair connection options * test(doctor): verify maintenance-only catalog admission * fix(doctor): admit update ledger reads before service stop Use Doctor's bounded read admission for pre-stop ledger inspection and its live rechecks on fresh private snapshots. Reset SQLite's schema cache when ending admission so unknown malformed indexes remain refused. Keep legacy catalog repair in Doctor and preserve updater ownership, future-version refusal, and the existing continuation writer fence. * fix(e2e): register Workshop recovery assertions Allow the existing Workshop Doctor recovery scenario through shared successful-update validation so it can reach the fresh candidate repair stage. Extend the real assertion CLI regression while preserving failed-step checks. * fix(e2e): recognize the Workshop frozen catalog * fix(doctor): repair legacy catalogs before migration reads Keep the prepared Doctor repair inside the migration transaction, ahead of publication-ledger reads. This lets standalone Doctor repair handle the v16 Workshop catalog while regular Gateway reads continue to refuse it.
1746 lines
62 KiB
TypeScript
1746 lines
62 KiB
TypeScript
// Main CLI entry orchestration: fast paths, env setup, plugin aliases, and Commander dispatch.
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
|
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
|
import type { Command as CommanderCommand, Option as CommanderOption } from "commander";
|
|
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
|
import {
|
|
createInvalidConfigError,
|
|
formatInvalidConfigDetails,
|
|
} from "../config/io.invalid-config.js";
|
|
import { resolveGatewayPort, resolveStateDir } from "../config/paths.js";
|
|
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { isLoopbackAddress, isSecureWebSocketUrl } from "../gateway/net.js";
|
|
import { normalizeWebSocketProtocol } from "../gateway/websocket-protocol.js";
|
|
import { FLAG_TERMINATOR, isValueToken } from "../infra/cli-root-options.js";
|
|
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
|
|
import type { ProxyHandle } from "../infra/net/proxy/proxy-lifecycle.js";
|
|
import { tryProcessCwd } from "../infra/safe-cwd.js";
|
|
import type { PluginCliLoadSession } from "../plugins/cli-registry-loader.js";
|
|
import { createPluginCache, getPluginCache, withPluginCache } from "../plugins/plugin-cache.js";
|
|
import { resolveCliArgvInvocation } from "./argv-invocation.js";
|
|
import {
|
|
normalizeGeneratedHelpCommandArgv,
|
|
normalizeRootHelpTargetArgv,
|
|
normalizeRootLogLevelArgv,
|
|
normalizeRootNoColorArgv,
|
|
} from "./argv.js";
|
|
import {
|
|
isReservedNonPluginCommandRoot,
|
|
shouldSkipPluginCommandRegistration,
|
|
} from "./command-registration-policy.js";
|
|
import { resolveCliStartupPolicy as resolveCliStartupPolicyForArgv } from "./command-startup-policy.js";
|
|
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
|
|
import { tryRunGatewayServiceUpdateCapabilityProbe } from "./daemon-cli/update-capability.js";
|
|
import { shouldStartLocalOnboarding } from "./fresh-install-config.js";
|
|
import {
|
|
consumeGatewayFastPathRootOptionToken,
|
|
consumeGatewayRunOptionToken,
|
|
resolveGatewayCatalogCommandPath,
|
|
resolveGatewayRunPreBootstrapOptions,
|
|
} from "./gateway-run-argv.js";
|
|
import {
|
|
hasJsonOutputFlag,
|
|
isJsonOutputModeActive,
|
|
withConsoleLogsRoutedToStderr,
|
|
withConsoleLogsRoutedToStderrForJson,
|
|
} from "./json-output-mode.js";
|
|
import { isMachineOutputStdoutTTY } from "./machine-output-argv.js";
|
|
import { requestExitAfterOneShotOutput } from "./one-shot-exit.js";
|
|
import { tryOutputPrecomputedCommandHelp } from "./precomputed-help.js";
|
|
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
|
|
import {
|
|
getCoreCliCommandDescriptors,
|
|
getCoreCliCommandNamesCore,
|
|
} from "./program/core-command-descriptors.js";
|
|
import { getSubCliEntriesCore } from "./program/subcli-descriptors.js";
|
|
import {
|
|
resolveMissingPluginCommandMessage,
|
|
rewriteUpdateFlagArgv,
|
|
shouldHandleBareRoot,
|
|
shouldEnsureCliPath,
|
|
shouldStartProxyForCli,
|
|
shouldUseRootHelpFastPath,
|
|
shouldUseSetupOnboardConfigureHelpFastPath,
|
|
} from "./run-main-policy.js";
|
|
import { withCliCommandCleanup, type CliHarnessCleanup } from "./runtime-cleanup-scope.js";
|
|
import { closeCliResources, runCliDisposer } from "./runtime-cleanup.js";
|
|
import { registerSignalExitBarrier, waitForSignalExitBarriers } from "./signal-exit-barrier.js";
|
|
import {
|
|
configureGatewayStartupTraceConsoleFormatting,
|
|
createGatewayDispatchStartupTrace,
|
|
} from "./startup-trace.js";
|
|
import { normalizeWindowsArgv } from "./windows-argv.js";
|
|
|
|
export {
|
|
rewriteUpdateFlagArgv,
|
|
shouldHandleBareRoot,
|
|
shouldEnsureCliPath,
|
|
shouldStartProxyForCli,
|
|
shouldUseRootHelpFastPath,
|
|
shouldUseSetupOnboardConfigureHelpFastPath,
|
|
} from "./run-main-policy.js";
|
|
|
|
const CLI_PROXY_ENV_KEYS = [
|
|
"HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"ALL_PROXY",
|
|
"http_proxy",
|
|
"https_proxy",
|
|
"all_proxy",
|
|
] as const;
|
|
const UNKNOWN_COMMAND_DISPLAY_LIMIT = 128;
|
|
|
|
const loadRootHelpLiveConfigModule = async () => await import("./root-help-live-config.js");
|
|
const loadRootHelpMetadataModule = async () => await import("./root-help-metadata.js");
|
|
const loadLoggingModule = async () => await import("../logging.js");
|
|
const loadCliRegistryLoaderModule = async () => await import("../plugins/cli-registry-loader.js");
|
|
const loadManifestCommandAliasesRuntimeModule = async () =>
|
|
await import("../plugins/manifest-command-aliases.runtime.js");
|
|
const loadProxyLifecycleModule = async () => await import("../infra/net/proxy/proxy-lifecycle.js");
|
|
const loadProgressModule = async () => await import("./progress.js");
|
|
|
|
function isRemoteAgentDispatchInvocation(argv: string[], primary: string | null): boolean {
|
|
return primary === "agent" && !argv.includes("--local");
|
|
}
|
|
|
|
export function isGatewayRunFastPathArgv(argv: string[]): boolean {
|
|
const invocation = resolveCliArgvInvocation(argv);
|
|
if (invocation.hasHelpOrVersion) {
|
|
return false;
|
|
}
|
|
const args = argv.slice(2);
|
|
let sawGateway = false;
|
|
let sawRun = false;
|
|
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
if (!arg || arg === "--") {
|
|
return false;
|
|
}
|
|
if (!sawGateway) {
|
|
const consumed = consumeGatewayFastPathRootOptionToken(args, index);
|
|
if (consumed > 0) {
|
|
index += consumed - 1;
|
|
continue;
|
|
}
|
|
if (arg !== "gateway") {
|
|
return false;
|
|
}
|
|
sawGateway = true;
|
|
continue;
|
|
}
|
|
|
|
const rootConsumed = consumeGatewayFastPathRootOptionToken(args, index);
|
|
if (rootConsumed > 0) {
|
|
index += rootConsumed - 1;
|
|
continue;
|
|
}
|
|
const consumed = consumeGatewayRunOptionToken(args, index);
|
|
if (consumed > 0) {
|
|
index += consumed - 1;
|
|
continue;
|
|
}
|
|
if (!sawRun && arg === "run") {
|
|
sawRun = true;
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return sawGateway;
|
|
}
|
|
|
|
function isGatewayRunInvocationArgv(argv: string[]): boolean {
|
|
const commandPath = resolveGatewayCatalogCommandPath(argv);
|
|
return (
|
|
commandPath?.length === 1 ||
|
|
(commandPath?.length === 2 && commandPath[0] === "gateway" && commandPath[1] === "run")
|
|
);
|
|
}
|
|
|
|
async function tryRunGatewayRunFastPath(
|
|
argv: string[],
|
|
startupTrace: ReturnType<typeof createGatewayDispatchStartupTrace>,
|
|
): Promise<boolean> {
|
|
if (!isGatewayRunFastPathArgv(argv)) {
|
|
return false;
|
|
}
|
|
const [
|
|
{ Command },
|
|
{ addGatewayRunCommand },
|
|
{ VERSION },
|
|
{ emitCliBanner },
|
|
{ ensureCliExecutionBootstrap },
|
|
{ defaultRuntime },
|
|
] = await startupTrace.measure("gateway-run-imports", () =>
|
|
Promise.all([
|
|
import("commander"),
|
|
import("./gateway-cli/run-command.js"),
|
|
import("../version.js"),
|
|
import("./banner.js"),
|
|
import("./command-execution-startup.js"),
|
|
import("../runtime.js"),
|
|
]),
|
|
);
|
|
const commandPath = resolveGatewayCatalogCommandPath(argv) ?? ["gateway"];
|
|
const startupPolicy = resolveCliStartupPolicyForArgv({
|
|
argv,
|
|
commandPath,
|
|
jsonOutputMode: hasJsonOutputFlag(argv),
|
|
});
|
|
if (!startupPolicy.hideBanner) {
|
|
emitCliBanner(VERSION, { argv });
|
|
}
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
program.enablePositionalOptions();
|
|
program.option("--no-color", "Disable ANSI colors", false);
|
|
program.exitOverride((err) => {
|
|
process.exitCode = typeof err.exitCode === "number" ? err.exitCode : 1;
|
|
throw err;
|
|
});
|
|
const beforeRun = async (opts: { force?: boolean; reset?: boolean }) => {
|
|
let beforeStateMigrations: ((snapshot?: ConfigFileSnapshot) => Promise<boolean>) | undefined;
|
|
let skipPristineStartupStateMigrations = false;
|
|
let skipPristineCoreStateMigrations = false;
|
|
const shouldBootstrap = await startupTrace.measure("gateway-run-pre-bootstrap", async () => {
|
|
const {
|
|
prepareGatewayRunBootstrap,
|
|
recheckGatewayRunBootstrap,
|
|
wasPreparedGatewayRunCoreStatePristine,
|
|
wasPreparedGatewayRunStatePristine,
|
|
} = await import("./gateway-cli/pre-bootstrap.js");
|
|
const prepared = await prepareGatewayRunBootstrap({ opts, runtime: defaultRuntime });
|
|
if (prepared) {
|
|
skipPristineStartupStateMigrations = wasPreparedGatewayRunStatePristine();
|
|
skipPristineCoreStateMigrations = wasPreparedGatewayRunCoreStatePristine();
|
|
beforeStateMigrations = (snapshot) =>
|
|
recheckGatewayRunBootstrap({
|
|
opts,
|
|
runtime: defaultRuntime,
|
|
...(snapshot ? { snapshot } : {}),
|
|
});
|
|
}
|
|
return prepared;
|
|
});
|
|
if (!shouldBootstrap) {
|
|
return;
|
|
}
|
|
await startupTrace.measure("gateway-run-bootstrap", async () => {
|
|
await ensureCliExecutionBootstrap({
|
|
runtime: defaultRuntime,
|
|
commandPath,
|
|
startupPolicy,
|
|
loadPlugins: false,
|
|
...(beforeStateMigrations ? { beforeStateMigrations } : {}),
|
|
...(skipPristineStartupStateMigrations ? { skipPristineStartupStateMigrations: true } : {}),
|
|
...(skipPristineCoreStateMigrations ? { skipPristineCoreStateMigrations: true } : {}),
|
|
});
|
|
const { reloadTrustedGatewayRunEnvironment } = await import("./gateway-cli/pre-bootstrap.js");
|
|
await reloadTrustedGatewayRunEnvironment({ runtime: defaultRuntime });
|
|
});
|
|
};
|
|
const gateway = addGatewayRunCommand(
|
|
program.command("gateway").description("Run, inspect, and query the WebSocket Gateway"),
|
|
{ beforeRun },
|
|
);
|
|
addGatewayRunCommand(
|
|
gateway.command("run").description("Run the WebSocket Gateway (foreground)"),
|
|
{ beforeRun },
|
|
);
|
|
const parseArgv = normalizeRootNoColorArgvForProgram(argv, program);
|
|
try {
|
|
await startupTrace.measure("gateway-run-parse", () => program.parseAsync(parseArgv), {
|
|
timeline: false,
|
|
});
|
|
} catch (error) {
|
|
if (!isCommanderParseExit(error)) {
|
|
throw error;
|
|
}
|
|
process.exitCode = error.exitCode;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export async function shouldStartOnboardingForFreshInstall(argv: string[]): Promise<boolean> {
|
|
if (!shouldHandleBareRoot(argv)) {
|
|
return false;
|
|
}
|
|
const { readConfigFileSnapshot } = await import("../config/config.js");
|
|
const snapshot = await readConfigFileSnapshot();
|
|
return shouldStartLocalOnboarding(snapshot);
|
|
}
|
|
|
|
type BareRootLaunchTarget =
|
|
| { kind: "onboarding"; classic?: boolean }
|
|
| {
|
|
kind: "remote-gateway-inference";
|
|
target: {
|
|
config: OpenClawConfig;
|
|
gatewayUrl: string;
|
|
token?: string;
|
|
password?: string;
|
|
tlsFingerprint?: string;
|
|
};
|
|
}
|
|
| { kind: "tui"; local: true }
|
|
| {
|
|
kind: "tui";
|
|
local: false;
|
|
config: OpenClawConfig;
|
|
gatewayUrl: string;
|
|
token?: string;
|
|
password?: string;
|
|
tlsFingerprint?: string;
|
|
};
|
|
|
|
async function resolveBareRootLaunchTarget(argv: string[]): Promise<BareRootLaunchTarget | null> {
|
|
if (!shouldHandleBareRoot(argv)) {
|
|
return null;
|
|
}
|
|
const { readConfigFileSnapshot } = await import("../config/config.js");
|
|
const snapshot = await readConfigFileSnapshot();
|
|
if (await shouldStartLocalOnboarding(snapshot)) {
|
|
return { kind: "onboarding" };
|
|
}
|
|
if (!snapshot.valid) {
|
|
return { kind: "onboarding", classic: true };
|
|
}
|
|
return resolveConfiguredTuiLaunchTarget(snapshot.config ?? snapshot.sourceConfig, {
|
|
hasConfiguredGateway: snapshot.sourceConfig.gateway !== undefined,
|
|
});
|
|
}
|
|
|
|
async function resolveConfiguredTuiLaunchTarget(
|
|
config: OpenClawConfig,
|
|
options: { hasConfiguredGateway: boolean },
|
|
): Promise<BareRootLaunchTarget> {
|
|
const gatewayResolution = await resolveReachableGateway(config, options);
|
|
if (
|
|
gatewayResolution.kind === "configured" ||
|
|
gatewayResolution.kind === "reachable-unverified" ||
|
|
gatewayResolution.kind === "configured-unreachable"
|
|
) {
|
|
const gateway = gatewayResolution.gateway;
|
|
const target: BareRootLaunchTarget = {
|
|
kind: "tui",
|
|
local: false,
|
|
config,
|
|
gatewayUrl: gateway.url,
|
|
};
|
|
if (gateway.token) {
|
|
target.token = gateway.token;
|
|
}
|
|
if (gateway.password) {
|
|
target.password = gateway.password;
|
|
}
|
|
if (gateway.tlsFingerprint) {
|
|
target.tlsFingerprint = gateway.tlsFingerprint;
|
|
}
|
|
return target;
|
|
}
|
|
if (gatewayResolution.kind === "missing-configured-model") {
|
|
// The connected Gateway is authoritative. Never fall back to a model in
|
|
// this client's local config when that server still needs inference.
|
|
if (gatewayResolution.gateway.remote) {
|
|
const target: Extract<BareRootLaunchTarget, { kind: "remote-gateway-inference" }> = {
|
|
kind: "remote-gateway-inference",
|
|
target: {
|
|
config,
|
|
gatewayUrl: gatewayResolution.gateway.url,
|
|
...(gatewayResolution.gateway.token ? { token: gatewayResolution.gateway.token } : {}),
|
|
...(gatewayResolution.gateway.password
|
|
? { password: gatewayResolution.gateway.password }
|
|
: {}),
|
|
...(gatewayResolution.gateway.tlsFingerprint
|
|
? { tlsFingerprint: gatewayResolution.gateway.tlsFingerprint }
|
|
: {}),
|
|
},
|
|
};
|
|
return target;
|
|
}
|
|
return { kind: "onboarding" };
|
|
}
|
|
const { listAgentIds, resolveAgentEffectiveModelPrimary } =
|
|
await import("../agents/agent-scope.js");
|
|
if (!listAgentIds(config).some((agentId) => resolveAgentEffectiveModelPrimary(config, agentId))) {
|
|
return { kind: "onboarding" };
|
|
}
|
|
return { kind: "tui", local: true };
|
|
}
|
|
|
|
type GatewayProbeTarget = {
|
|
url: string;
|
|
scope: "local-loopback" | "local-configured" | "remote";
|
|
tlsFingerprint?: string;
|
|
preauthHandshakeTimeoutMs?: number;
|
|
};
|
|
|
|
type ReachableGateway = {
|
|
url: string;
|
|
remote: boolean;
|
|
token?: string;
|
|
password?: string;
|
|
tlsFingerprint?: string;
|
|
};
|
|
|
|
type GatewayResolution =
|
|
| { kind: "configured"; gateway: ReachableGateway }
|
|
| { kind: "missing-configured-model"; gateway: ReachableGateway }
|
|
| { kind: "reachable-unverified"; gateway: ReachableGateway }
|
|
| { kind: "configured-unreachable"; gateway: ReachableGateway }
|
|
| { kind: "unreachable" };
|
|
|
|
type GatewayProbeAuth = {
|
|
token?: string;
|
|
password?: string;
|
|
};
|
|
|
|
function toReachableGateway(target: GatewayProbeTarget, auth: GatewayProbeAuth): ReachableGateway {
|
|
return {
|
|
url: target.url,
|
|
remote: target.scope === "remote",
|
|
...(auth.token ? { token: auth.token } : {}),
|
|
...(auth.password ? { password: auth.password } : {}),
|
|
...(target.tlsFingerprint ? { tlsFingerprint: target.tlsFingerprint } : {}),
|
|
};
|
|
}
|
|
|
|
async function resolveReachableGateway(
|
|
config: OpenClawConfig,
|
|
options: { hasConfiguredGateway: boolean },
|
|
): Promise<GatewayResolution> {
|
|
const { targets, auth } = await resolveGatewayProbePlan(config);
|
|
if (targets.length === 0) {
|
|
return { kind: "unreachable" };
|
|
}
|
|
const { probeGatewayConfiguredModel } = await import("../commands/onboard-helpers.js");
|
|
let missingModelGateway: ReachableGateway | undefined;
|
|
let reachableUnverifiedGateway: ReachableGateway | undefined;
|
|
let configuredGateway: ReachableGateway | undefined;
|
|
for (const target of targets) {
|
|
if (!isSafeGatewayProbeTarget(target)) {
|
|
continue;
|
|
}
|
|
// A cold-restarting configured Gateway remains the authoritative route.
|
|
// Keep its safe endpoint so one failed probe cannot reopen local onboarding.
|
|
if (options.hasConfiguredGateway && !configuredGateway) {
|
|
configuredGateway = toReachableGateway(target, auth);
|
|
}
|
|
const probeOptions: Parameters<typeof probeGatewayConfiguredModel>[0] = {
|
|
url: target.url,
|
|
// A configured remote origin stays remote through a loopback tunnel.
|
|
...(target.scope === "remote" ? { originScopedDeviceAuth: true } : {}),
|
|
};
|
|
if (config.gateway?.remote?.edgeAuth) {
|
|
probeOptions.config = config;
|
|
}
|
|
if (auth.token) {
|
|
probeOptions.token = auth.token;
|
|
}
|
|
if (auth.password) {
|
|
probeOptions.password = auth.password;
|
|
}
|
|
if (target.tlsFingerprint) {
|
|
probeOptions.tlsFingerprint = target.tlsFingerprint;
|
|
}
|
|
if (target.preauthHandshakeTimeoutMs) {
|
|
probeOptions.preauthHandshakeTimeoutMs = target.preauthHandshakeTimeoutMs;
|
|
}
|
|
const probe = await probeGatewayConfiguredModel(probeOptions);
|
|
if (probe.kind === "configured") {
|
|
return { kind: "configured", gateway: toReachableGateway(target, auth) };
|
|
}
|
|
if (probe.kind === "missing-configured-model") {
|
|
missingModelGateway ??= toReachableGateway(target, auth);
|
|
continue;
|
|
}
|
|
if (probe.kind === "reachable-unverified" && !reachableUnverifiedGateway) {
|
|
reachableUnverifiedGateway = toReachableGateway(target, auth);
|
|
}
|
|
}
|
|
if (missingModelGateway) {
|
|
return { kind: "missing-configured-model", gateway: missingModelGateway };
|
|
}
|
|
if (reachableUnverifiedGateway) {
|
|
return { kind: "reachable-unverified", gateway: reachableUnverifiedGateway };
|
|
}
|
|
if (configuredGateway) {
|
|
return { kind: "configured-unreachable", gateway: configuredGateway };
|
|
}
|
|
return { kind: "unreachable" };
|
|
}
|
|
|
|
async function resolveGatewayProbePlan(
|
|
config: OpenClawConfig,
|
|
): Promise<{ targets: GatewayProbeTarget[]; auth: GatewayProbeAuth }> {
|
|
const remoteUrl = normalizeOptionalString(config.gateway?.remote?.url);
|
|
if (normalizeOptionalString(config.gateway?.mode) === "remote" && remoteUrl) {
|
|
try {
|
|
const { resolveGatewayClientBootstrap } = await import("../gateway/client-bootstrap.js");
|
|
const bootstrap = await resolveGatewayClientBootstrap({
|
|
config,
|
|
authPolicy: "probe",
|
|
modeOverride: "remote",
|
|
ignoreEnvUrlOverride: true,
|
|
});
|
|
return {
|
|
targets: [
|
|
{
|
|
url: bootstrap.url,
|
|
scope: "remote",
|
|
...(bootstrap.tlsFingerprint ? { tlsFingerprint: bootstrap.tlsFingerprint } : {}),
|
|
},
|
|
],
|
|
auth: bootstrap.auth,
|
|
};
|
|
} catch {
|
|
return { targets: [], auth: {} };
|
|
}
|
|
}
|
|
return resolveLocalGatewayProbeTargets(config);
|
|
}
|
|
|
|
function isSafeGatewayProbeTarget(target: GatewayProbeTarget): boolean {
|
|
if (target.scope === "remote") {
|
|
return isSafeRemoteGatewayProbeUrl(target.url);
|
|
}
|
|
return isSecureWebSocketUrl(target.url, {
|
|
allowPrivateWs: process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1",
|
|
});
|
|
}
|
|
|
|
function isSafeRemoteGatewayProbeUrl(url: string): boolean {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
return false;
|
|
}
|
|
const protocol = normalizeWebSocketProtocol(parsed.protocol);
|
|
if (protocol === "wss:") {
|
|
return true;
|
|
}
|
|
if (protocol !== "ws:") {
|
|
return false;
|
|
}
|
|
if (isLoopbackGatewayHost(parsed.hostname)) {
|
|
return true;
|
|
}
|
|
return (
|
|
process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1" &&
|
|
isSecureWebSocketUrl(url, { allowPrivateWs: true })
|
|
);
|
|
}
|
|
|
|
function isLoopbackGatewayHost(hostname: string): boolean {
|
|
const normalized = hostname.toLowerCase().replace(/\.+$/, "");
|
|
if (normalized === "localhost") {
|
|
return true;
|
|
}
|
|
const hostForIpCheck =
|
|
normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
return isLoopbackAddress(hostForIpCheck);
|
|
}
|
|
|
|
async function resolveLocalGatewayProbeTargets(
|
|
config: OpenClawConfig,
|
|
): Promise<{ targets: GatewayProbeTarget[]; auth: GatewayProbeAuth }> {
|
|
const [
|
|
{ resolveControlUiLinks },
|
|
{ resolveGatewayClientBootstrap },
|
|
{ readActiveGatewayLockPort },
|
|
] = await Promise.all([
|
|
import("../gateway/control-ui-links.js"),
|
|
import("../gateway/client-bootstrap.js"),
|
|
import("../infra/gateway-lock.js"),
|
|
]);
|
|
const gateway = config.gateway;
|
|
const configuredPort = resolveGatewayPort(config);
|
|
const hasExplicitPort = Boolean(normalizeOptionalString(process.env.OPENCLAW_GATEWAY_PORT));
|
|
const activePort = hasExplicitPort ? undefined : await readActiveGatewayLockPort();
|
|
const port = activePort ?? configuredPort;
|
|
// Supplying the selected local port keeps inherited remote URL overrides out
|
|
// of bare-root routing while reusing canonical local TLS/fingerprint logic.
|
|
const connection = await resolveGatewayClientBootstrap({
|
|
config,
|
|
authPolicy: "probe",
|
|
modeOverride: "local",
|
|
ignoreEnvUrlOverride: true,
|
|
localPortOverride: port,
|
|
});
|
|
const baseParams = {
|
|
port,
|
|
basePath: gateway?.controlUi?.basePath,
|
|
tlsEnabled: gateway?.tls?.enabled === true,
|
|
};
|
|
const sharedTarget = {
|
|
...(connection.tlsFingerprint ? { tlsFingerprint: connection.tlsFingerprint } : {}),
|
|
...(connection.preauthHandshakeTimeoutMs
|
|
? { preauthHandshakeTimeoutMs: connection.preauthHandshakeTimeoutMs }
|
|
: {}),
|
|
};
|
|
const loopbackTarget: GatewayProbeTarget = {
|
|
...sharedTarget,
|
|
url: connection.url,
|
|
scope: "local-loopback",
|
|
};
|
|
const bind = gateway?.bind;
|
|
if (bind !== "tailnet" && bind !== "custom") {
|
|
return { targets: [loopbackTarget], auth: connection.auth };
|
|
}
|
|
const configuredLinks = resolveControlUiLinks({
|
|
...baseParams,
|
|
bind,
|
|
customBindHost: gateway?.customBindHost,
|
|
});
|
|
const targets =
|
|
configuredLinks.wsUrl === connection.url
|
|
? [loopbackTarget]
|
|
: [
|
|
loopbackTarget,
|
|
{
|
|
...sharedTarget,
|
|
url: configuredLinks.wsUrl,
|
|
scope: "local-configured" as const,
|
|
},
|
|
];
|
|
return { targets, auth: connection.auth };
|
|
}
|
|
|
|
function pauseNonTtyStdinForCliExit(): void {
|
|
const stdin = process.stdin;
|
|
if (stdin.isTTY) {
|
|
return;
|
|
}
|
|
try {
|
|
stdin.pause();
|
|
} catch {
|
|
// Best-effort cleanup for command paths that only inspected stdin.
|
|
}
|
|
}
|
|
|
|
function shouldLoadCliDotEnv(
|
|
loadGlobalEnv: boolean,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): boolean {
|
|
const cwd = tryProcessCwd();
|
|
if (cwd && existsSync(path.join(cwd, ".env"))) {
|
|
return true;
|
|
}
|
|
return loadGlobalEnv && existsSync(path.join(resolveStateDir(env), ".env"));
|
|
}
|
|
|
|
function isAgentExecInvocation(commandPath: string[]): boolean {
|
|
return commandPath[0] === "agent" && commandPath[1] === "exec";
|
|
}
|
|
|
|
function isCommanderParseExit(error: unknown): error is { exitCode: number } {
|
|
if (!error || typeof error !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = error as { code?: unknown; exitCode?: unknown };
|
|
return (
|
|
typeof candidate.exitCode === "number" &&
|
|
Number.isInteger(candidate.exitCode) &&
|
|
typeof candidate.code === "string" &&
|
|
candidate.code.startsWith("commander.")
|
|
);
|
|
}
|
|
|
|
function findCommandOption(command: CommanderCommand, token: string): CommanderOption | undefined {
|
|
const equalsIndex = token.indexOf("=");
|
|
const flag = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
|
|
return command.options.find((option) => option.long === flag || option.short === flag);
|
|
}
|
|
|
|
function findSubcommand(command: CommanderCommand, name: string): CommanderCommand | undefined {
|
|
return command.commands.find(
|
|
(subcommand) => subcommand.name() === name || subcommand.aliases().includes(name),
|
|
);
|
|
}
|
|
|
|
function shouldOptionConsumeFollowingToken(
|
|
option: CommanderOption | undefined,
|
|
token: string,
|
|
next: string | undefined,
|
|
): boolean {
|
|
if (!option || token.includes("=")) {
|
|
return false;
|
|
}
|
|
if (option.required) {
|
|
return true;
|
|
}
|
|
return option.optional && isValueToken(next);
|
|
}
|
|
|
|
function resolveRootOptionRole(
|
|
program: CommanderCommand,
|
|
remainingArgs: readonly string[],
|
|
optionIndex: number,
|
|
): "root" | "command" | "value" {
|
|
let command = program;
|
|
let pendingValue = false;
|
|
for (let index = 0; index < optionIndex; index += 1) {
|
|
const arg = remainingArgs[index];
|
|
if (!arg || arg === FLAG_TERMINATOR) {
|
|
return "root";
|
|
}
|
|
if (pendingValue) {
|
|
pendingValue = false;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("-")) {
|
|
const option = findCommandOption(command, arg);
|
|
if (!option && index === optionIndex - 1 && !arg.includes("=")) {
|
|
// Unknown option surfaces may allow arbitrary flags; keep the value-safe behavior there.
|
|
return "value";
|
|
}
|
|
pendingValue = shouldOptionConsumeFollowingToken(option, arg, remainingArgs[index + 1]);
|
|
continue;
|
|
}
|
|
command = findSubcommand(command, arg) ?? command;
|
|
}
|
|
if (pendingValue) {
|
|
return "value";
|
|
}
|
|
|
|
const arg = remainingArgs[optionIndex];
|
|
return command !== program && arg !== undefined && findCommandOption(command, arg) !== undefined
|
|
? "command"
|
|
: "root";
|
|
}
|
|
|
|
function normalizeRootNoColorArgvForProgram(argv: string[], program: CommanderCommand): string[] {
|
|
return normalizeRootNoColorArgv(argv, {
|
|
shouldPreserveNoColor: ({ remainingArgs, noColorIndex }) =>
|
|
resolveRootOptionRole(program, remainingArgs, noColorIndex) === "value",
|
|
});
|
|
}
|
|
|
|
function normalizeRootLogLevelArgvForProgram(argv: string[], program: CommanderCommand): string[] {
|
|
return normalizeRootLogLevelArgv(argv, {
|
|
shouldPreserveLogLevel: ({ remainingArgs, logLevelIndex }) =>
|
|
resolveRootOptionRole(program, remainingArgs, logLevelIndex) !== "root",
|
|
});
|
|
}
|
|
|
|
async function ensureCliEnvProxyDispatcher(): Promise<void> {
|
|
try {
|
|
const { hasEnvHttpProxyAgentConfigured } = await import("../infra/net/proxy-env.js");
|
|
if (!hasEnvHttpProxyAgentConfigured()) {
|
|
return;
|
|
}
|
|
const { ensureGlobalUndiciEnvProxyDispatcher } =
|
|
await import("../infra/net/undici-global-dispatcher.js");
|
|
ensureGlobalUndiciEnvProxyDispatcher();
|
|
} catch {
|
|
// Best-effort proxy bootstrap; CLI startup should continue without it.
|
|
}
|
|
}
|
|
|
|
function isDebugProxyCaptureEnvEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
return (
|
|
isTruthyEnvValue(env.OPENCLAW_DEBUG_PROXY_ENABLED) ||
|
|
isTruthyEnvValue(env.OPENCLAW_DEBUG_PROXY_REQUIRE)
|
|
);
|
|
}
|
|
|
|
function shouldBootstrapCliProxyBeforeFastPath(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
if (isDebugProxyCaptureEnvEnabled(env)) {
|
|
return true;
|
|
}
|
|
return CLI_PROXY_ENV_KEYS.some((key) => {
|
|
const value = env[key];
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
});
|
|
}
|
|
|
|
function isKnownBuiltInCommandRoot(primary: string): boolean {
|
|
return (
|
|
getCoreCliCommandNamesCore().includes(primary) ||
|
|
getSubCliEntriesCore().some((entry) => entry.name === primary)
|
|
);
|
|
}
|
|
|
|
function resolvesMachineOutput(
|
|
descriptor: {
|
|
machineOutput?: (params: { argv: readonly string[]; stdoutIsTTY: boolean }) => boolean;
|
|
},
|
|
argv: readonly string[],
|
|
): boolean {
|
|
return descriptor.machineOutput?.({ argv, stdoutIsTTY: isMachineOutputStdoutTTY() }) ?? false;
|
|
}
|
|
|
|
function resolveBuiltInMachineOutput(argv: string[]): boolean {
|
|
const { primary } = resolveCliArgvInvocation(argv);
|
|
if (!primary) {
|
|
return false;
|
|
}
|
|
const descriptor = [...getCoreCliCommandDescriptors(), ...getSubCliEntriesCore()].find(
|
|
(entry) => entry.name === primary,
|
|
);
|
|
return descriptor ? resolvesMachineOutput(descriptor, argv) : false;
|
|
}
|
|
|
|
async function resolvePluginMachineOutput(params: {
|
|
argv: string[];
|
|
config: OpenClawConfig;
|
|
session?: PluginCliLoadSession;
|
|
}): Promise<boolean> {
|
|
const { primary } = resolveCliArgvInvocation(params.argv);
|
|
if (!primary || isKnownBuiltInCommandRoot(primary)) {
|
|
return false;
|
|
}
|
|
const { loadPluginCliDescriptors } = await loadCliRegistryLoaderModule();
|
|
const descriptors = await loadPluginCliDescriptors({
|
|
cfg: params.config,
|
|
env: process.env,
|
|
primaryCommand: primary,
|
|
session: params.session,
|
|
});
|
|
const descriptor = descriptors.find((entry) => entry.name === primary);
|
|
const resolveOutput = () => (descriptor ? resolvesMachineOutput(descriptor, params.argv) : false);
|
|
return params.session ? params.session.withCache(resolveOutput) : resolveOutput();
|
|
}
|
|
|
|
async function isPluginCliRoot(params: {
|
|
primary: string;
|
|
config: OpenClawConfig;
|
|
session?: PluginCliLoadSession;
|
|
}): Promise<boolean | null> {
|
|
try {
|
|
const { resolvePluginCliRootOwnerIds } = await loadCliRegistryLoaderModule();
|
|
const ownerIds = await resolvePluginCliRootOwnerIds({
|
|
cfg: params.config,
|
|
env: process.env,
|
|
primaryCommand: params.primary,
|
|
session: params.session,
|
|
});
|
|
return ownerIds === null ? null : ownerIds.length > 0;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function createAllowlistAgnosticCliLookupConfig(config: OpenClawConfig): OpenClawConfig {
|
|
if (!Array.isArray(config.plugins?.allow) || config.plugins.allow.length === 0) {
|
|
return config;
|
|
}
|
|
return {
|
|
...config,
|
|
plugins: {
|
|
...config.plugins,
|
|
allow: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
async function resolveCliCommandSurfaceOwner(params: {
|
|
primary: string;
|
|
config: OpenClawConfig;
|
|
}): Promise<string | undefined> {
|
|
const { resolveManifestCliCommandSurfaceOwner } = await loadManifestCommandAliasesRuntimeModule();
|
|
const manifestOwner = resolveManifestCliCommandSurfaceOwner({
|
|
command: params.primary,
|
|
config: params.config,
|
|
env: process.env,
|
|
});
|
|
if (manifestOwner) {
|
|
return manifestOwner;
|
|
}
|
|
try {
|
|
const { resolvePluginCliRootOwnerIds } = await loadCliRegistryLoaderModule();
|
|
return (
|
|
await resolvePluginCliRootOwnerIds({
|
|
cfg: createAllowlistAgnosticCliLookupConfig(params.config),
|
|
env: process.env,
|
|
primaryCommand: params.primary,
|
|
})
|
|
)?.[0];
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function resolveUnownedCliPrimaryCandidate(argv: string[]): string | null {
|
|
const invocation = resolveCliArgvInvocation(rewriteUpdateFlagArgv(argv));
|
|
const { primary } = invocation;
|
|
if (
|
|
!primary ||
|
|
primary === "help" ||
|
|
isReservedNonPluginCommandRoot(primary) ||
|
|
isKnownBuiltInCommandRoot(primary)
|
|
) {
|
|
return null;
|
|
}
|
|
return primary;
|
|
}
|
|
|
|
async function resolveUnownedCliPrimary(params: {
|
|
argv: string[];
|
|
config: OpenClawConfig;
|
|
session?: PluginCliLoadSession;
|
|
}): Promise<string | null> {
|
|
const primary = resolveUnownedCliPrimaryCandidate(params.argv);
|
|
if (!primary) {
|
|
return null;
|
|
}
|
|
const pluginRoot = await isPluginCliRoot({
|
|
primary,
|
|
config: params.config,
|
|
session: params.session,
|
|
});
|
|
if (pluginRoot !== false) {
|
|
return null;
|
|
}
|
|
return primary;
|
|
}
|
|
|
|
async function resolveUnownedCliPrimaryError(params: {
|
|
argv: string[];
|
|
primary: string;
|
|
config: OpenClawConfig;
|
|
}): Promise<Error> {
|
|
const { resolveManifestCommandAliasOwner, resolveManifestToolOwner } =
|
|
await loadManifestCommandAliasesRuntimeModule();
|
|
const cliCommandSurfaceOwner = await resolveCliCommandSurfaceOwner(params);
|
|
const pluginPolicyMessage = resolveMissingPluginCommandMessage(params.primary, params.config, {
|
|
resolveCommandAliasOwner: resolveManifestCommandAliasOwner,
|
|
resolveToolOwner: resolveManifestToolOwner,
|
|
resolveCliCommandSurfaceOwner: () => cliCommandSurfaceOwner,
|
|
});
|
|
if (pluginPolicyMessage) {
|
|
return await createExpectedPluginPolicyError(pluginPolicyMessage);
|
|
}
|
|
const sanitizedPrimary = sanitizeTerminalText(params.primary);
|
|
const displayPrimary =
|
|
sanitizedPrimary.length <= UNKNOWN_COMMAND_DISPLAY_LIMIT
|
|
? sanitizedPrimary
|
|
: `${truncateUtf16Safe(sanitizedPrimary, UNKNOWN_COMMAND_DISPLAY_LIMIT - 1)}…`;
|
|
const { createCliUnknownCommandError } = await import("./program/error-output.js");
|
|
return createCliUnknownCommandError(displayPrimary, {
|
|
argv: params.argv,
|
|
...(displayPrimary === params.primary ? {} : { commandNames: [] }),
|
|
});
|
|
}
|
|
|
|
async function createExpectedPluginPolicyError(message: string): Promise<Error> {
|
|
const { ExpectedCliError } = await import("./failure-output.js");
|
|
return new ExpectedCliError({ message, humanOutput: message, machineOutput: message });
|
|
}
|
|
|
|
async function bootstrapCliProxyCaptureAndDispatcher(
|
|
startupTrace: ReturnType<typeof createGatewayDispatchStartupTrace>,
|
|
options: { ensureDispatcher?: boolean } = {},
|
|
): Promise<void> {
|
|
// Capture init, exit finalize, and coverage warnings all no-op unless the
|
|
// debug-proxy env requests capture; importing their sqlite-store graph anyway
|
|
// costs ~100 MB RSS on metadata-only commands such as `plugins list --json`.
|
|
if (isDebugProxyCaptureEnvEnabled()) {
|
|
const [
|
|
{ initializeDebugProxyCapture, finalizeDebugProxyCapture },
|
|
{ maybeWarnAboutDebugProxyCoverage },
|
|
] = await startupTrace.measure("proxy-imports", () =>
|
|
Promise.all([import("../proxy-capture/runtime.js"), import("../proxy-capture/coverage.js")]),
|
|
);
|
|
initializeDebugProxyCapture("cli");
|
|
process.once("exit", () => {
|
|
finalizeDebugProxyCapture();
|
|
});
|
|
maybeWarnAboutDebugProxyCoverage(undefined, (message) => console.warn(message));
|
|
}
|
|
if (options.ensureDispatcher !== false) {
|
|
await startupTrace.measure("proxy-dispatcher", () => ensureCliEnvProxyDispatcher());
|
|
}
|
|
}
|
|
|
|
export async function runCli(
|
|
argv: string[] = process.argv,
|
|
options: {
|
|
additionalStartupTrace?: ReturnType<typeof createGatewayDispatchStartupTrace>;
|
|
retainConsoleRoutingUntilProcessExit?: boolean;
|
|
runtimeRecoveryEnv?: NodeJS.ProcessEnv;
|
|
} = {},
|
|
) {
|
|
const runtimeRecoveryEnv = options.runtimeRecoveryEnv ?? { ...process.env };
|
|
const originalArgv = normalizeWindowsArgv(argv);
|
|
const builtInMachineOutput = resolveBuiltInMachineOutput(originalArgv);
|
|
return await withConsoleLogsRoutedToStderrForJson(
|
|
originalArgv,
|
|
() => {
|
|
const run = async (harnessCleanup?: CliHarnessCleanup) => {
|
|
try {
|
|
return await runCliWithPreparedOutputMode(originalArgv, {
|
|
...options,
|
|
runtimeRecoveryEnv,
|
|
builtInMachineOutput,
|
|
harnessCleanup,
|
|
});
|
|
} catch (error) {
|
|
// Selection and Commander preactions run before the Gateway action's failure boundary.
|
|
if (
|
|
isGatewayRunInvocationArgv(originalArgv) &&
|
|
!resolveCliArgvInvocation(originalArgv).hasHelpOrVersion
|
|
) {
|
|
const { handleGatewayStartupMaintenance } =
|
|
await import("./gateway-cli/startup-maintenance.js");
|
|
if (await handleGatewayStartupMaintenance(error)) {
|
|
return;
|
|
}
|
|
}
|
|
throw error;
|
|
} finally {
|
|
const resources = harnessCleanup?.pluginResources;
|
|
if (resources) {
|
|
await runCliDisposer("plugin-registration-resources", async () => {
|
|
try {
|
|
await resources.release();
|
|
} catch (error) {
|
|
console.error(`Plugin CLI resource disposal failed: ${String(error)}`);
|
|
throw error;
|
|
}
|
|
});
|
|
pauseNonTtyStdinForCliExit();
|
|
}
|
|
}
|
|
};
|
|
// Nested registrars and late actions share this lightweight owner, even when no
|
|
// top-level plugin preparation is needed. Gateway retains its boot/process owner.
|
|
const gatewayRun = isGatewayRunInvocationArgv(originalArgv);
|
|
return withCliCommandCleanup(gatewayRun, (cleanup) =>
|
|
gatewayRun ? run() : withPluginCache(createPluginCache(), () => run(cleanup)),
|
|
);
|
|
},
|
|
{
|
|
machineOutput: builtInMachineOutput,
|
|
restoreChanges: true,
|
|
retainRoutingUntilProcessExit: options.retainConsoleRoutingUntilProcessExit,
|
|
},
|
|
);
|
|
}
|
|
|
|
async function runCliWithPreparedOutputMode(
|
|
originalArgv: string[],
|
|
options: {
|
|
additionalStartupTrace?: ReturnType<typeof createGatewayDispatchStartupTrace>;
|
|
builtInMachineOutput: boolean;
|
|
harnessCleanup?: CliHarnessCleanup;
|
|
runtimeRecoveryEnv: NodeJS.ProcessEnv;
|
|
},
|
|
) {
|
|
const startupTrace = createGatewayDispatchStartupTrace(originalArgv, "cli.main");
|
|
const earlyProfile = parseCliProfileArgs(originalArgv);
|
|
if (earlyProfile.ok && earlyProfile.profile) {
|
|
applyCliProfileEnv({ profile: earlyProfile.profile });
|
|
}
|
|
const originalInvocation = resolveCliArgvInvocation(originalArgv);
|
|
let consoleCaptureInstalled = false;
|
|
const installConsoleCapture = async () => {
|
|
if (consoleCaptureInstalled) {
|
|
return;
|
|
}
|
|
const { enableConsoleCapture } = await loadLoggingModule();
|
|
enableConsoleCapture();
|
|
consoleCaptureInstalled = true;
|
|
};
|
|
const configureStartupTraces = async () => {
|
|
await configureGatewayStartupTraceConsoleFormatting(startupTrace);
|
|
if (options.additionalStartupTrace) {
|
|
await configureGatewayStartupTraceConsoleFormatting(options.additionalStartupTrace);
|
|
}
|
|
};
|
|
const parsedContainer = parseCliContainerArgs(originalArgv);
|
|
if (!parsedContainer.ok) {
|
|
await installConsoleCapture();
|
|
await configureStartupTraces();
|
|
throw new Error(parsedContainer.error);
|
|
}
|
|
const parsedProfile = parseCliProfileArgs(parsedContainer.argv);
|
|
const containerTargetName =
|
|
parsedContainer.container ?? normalizeOptionalString(process.env.OPENCLAW_CONTAINER) ?? null;
|
|
const hasPreHelpValidationError =
|
|
!parsedProfile.ok || (containerTargetName !== null && parsedProfile.profile !== null);
|
|
// Console formatting is a process-wide invariant. Install capture before
|
|
// container dispatch or validation can bypass the pure help/version path.
|
|
if (
|
|
!originalInvocation.hasHelpOrVersion ||
|
|
containerTargetName !== null ||
|
|
hasPreHelpValidationError
|
|
) {
|
|
await installConsoleCapture();
|
|
}
|
|
if (!parsedProfile.ok) {
|
|
await configureStartupTraces();
|
|
throw new Error(parsedProfile.error);
|
|
}
|
|
if (parsedProfile.profile) {
|
|
applyCliProfileEnv({ profile: parsedProfile.profile });
|
|
}
|
|
if (containerTargetName && parsedProfile.profile) {
|
|
await configureStartupTraces();
|
|
throw new Error("--container cannot be combined with --profile/--dev");
|
|
}
|
|
|
|
let containerTarget: ReturnType<typeof maybeRunCliInContainer>;
|
|
try {
|
|
containerTarget = maybeRunCliInContainer(originalArgv);
|
|
} catch (error) {
|
|
await configureStartupTraces();
|
|
throw error;
|
|
}
|
|
if (containerTarget.handled) {
|
|
await configureStartupTraces();
|
|
if (containerTarget.exitCode !== 0) {
|
|
process.exitCode = containerTarget.exitCode;
|
|
}
|
|
return;
|
|
}
|
|
const normalizedArgv = rewriteUpdateFlagArgv(
|
|
normalizeRootHelpTargetArgv(normalizeRootNoColorArgv(parsedProfile.argv)),
|
|
);
|
|
const normalizedInvocation = resolveCliArgvInvocation(normalizedArgv);
|
|
const isHelpOrVersionInvocation = normalizedInvocation.hasHelpOrVersion;
|
|
const isGatewayRunInvocation = isGatewayRunInvocationArgv(normalizedArgv);
|
|
const isDatabaseInvocation = normalizedInvocation.commandPath[0] === "database";
|
|
// Gateway pre-bootstrap owns state/config dotenv selection. This phase only
|
|
// needs the workspace file, so avoid importing the loader when it is absent.
|
|
const loadGlobalEnv = !isGatewayRunInvocation;
|
|
startupTrace.mark("argv");
|
|
|
|
// Enforce the minimum supported runtime before gateway selection can read or recover config.
|
|
const { assertSupportedRuntime, isCurrentRuntimeSupported } =
|
|
await import("../infra/runtime-guard.js");
|
|
await assertSupportedRuntime(
|
|
undefined,
|
|
undefined,
|
|
normalizedArgv,
|
|
true,
|
|
options.runtimeRecoveryEnv,
|
|
);
|
|
|
|
if (await tryRunGatewayServiceUpdateCapabilityProbe(normalizedArgv)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!isHelpOrVersionInvocation &&
|
|
!isDatabaseInvocation &&
|
|
!isAgentExecInvocation(normalizedInvocation.commandPath) &&
|
|
shouldLoadCliDotEnv(loadGlobalEnv)
|
|
) {
|
|
await startupTrace.measure("dotenv", async () => {
|
|
if (isRemoteAgentDispatchInvocation(normalizedArgv, normalizedInvocation.primary)) {
|
|
const { loadGatewayDispatchCliDotEnv } = await import("./gateway-dispatch-dotenv.js");
|
|
await loadGatewayDispatchCliDotEnv({ quiet: true });
|
|
} else {
|
|
const { loadCliDotEnv } = await import("./dotenv.js");
|
|
loadCliDotEnv({ loadGlobalEnv, quiet: true });
|
|
}
|
|
});
|
|
}
|
|
if (
|
|
!isHelpOrVersionInvocation &&
|
|
normalizedInvocation.primary === "doctor" &&
|
|
process.env.OPENCLAW_UPDATE_IN_PROGRESS === "1"
|
|
) {
|
|
// Debug capture can migrate shared state before Commander reaches Doctor.
|
|
// Resolve the update guard after selectors settle, before any bootstrap writer.
|
|
const [{ guardUpdateDoctorSchemaUpgrade }, { defaultRuntime }] = await Promise.all([
|
|
import("../commands/doctor-update-schema-guard.js"),
|
|
import("../runtime.js"),
|
|
]);
|
|
await guardUpdateDoctorSchemaUpgrade({
|
|
runtime: defaultRuntime,
|
|
json: options.builtInMachineOutput,
|
|
});
|
|
}
|
|
await configureStartupTraces();
|
|
if (!isHelpOrVersionInvocation && isGatewayRunInvocation) {
|
|
await startupTrace.measure("gateway-run-select-environment", async () => {
|
|
const [{ selectGatewayRunEnvironment }, { defaultRuntime }] = await Promise.all([
|
|
import("./gateway-cli/pre-bootstrap.js"),
|
|
import("../runtime.js"),
|
|
]);
|
|
const opts = resolveGatewayRunPreBootstrapOptions(normalizedArgv) ?? {};
|
|
await selectGatewayRunEnvironment({ opts, runtime: defaultRuntime });
|
|
});
|
|
}
|
|
normalizeEnv();
|
|
if (shouldEnsureCliPath(normalizedArgv)) {
|
|
const { ensureOpenClawCliOnPath } = await import("../infra/path-env.js");
|
|
ensureOpenClawCliOnPath();
|
|
}
|
|
// Cheap import gate only. Session-ref owns the authoritative URL/options parse.
|
|
const mayContainBareSessionUrl = normalizedArgv.slice(2).some((arg) => arg.includes("://"));
|
|
const bareSessionInvocation =
|
|
!isHelpOrVersionInvocation && mayContainBareSessionUrl
|
|
? (await import("./session-ref.js")).parseBareSessionInvocation(normalizedArgv)
|
|
: null;
|
|
|
|
// Activate operator-managed proxy routing for network-capable commands.
|
|
// Local Gateway/control-plane commands keep direct loopback access while
|
|
// runtime, provider, plugin, update, and manifest/metadata-owned plugin commands route egress.
|
|
let proxyHandle: ProxyHandle | null = null;
|
|
let proxyStopPromise: Promise<void> | undefined;
|
|
let onSigterm: (() => void) | null = null;
|
|
let onSigint: (() => void) | null = null;
|
|
let onExit: (() => void) | null = null;
|
|
let unregisterProxySignalExitBarrier: (() => void) | null = null;
|
|
let bestEffortConfigPromise: Promise<OpenClawConfig> | null = null;
|
|
let pluginCliSession: PluginCliLoadSession | undefined;
|
|
const getPluginCliSession = async () => {
|
|
pluginCliSession ??= (await loadCliRegistryLoaderModule()).createPluginCliLoadSession(
|
|
getPluginCache(),
|
|
);
|
|
return pluginCliSession;
|
|
};
|
|
const isolateProxyConfigEnv = isGatewayRunInvocation;
|
|
const bestEffortConfigStartupPolicy = resolveCliStartupPolicyForArgv({
|
|
argv: normalizedArgv,
|
|
commandPath: normalizedInvocation.commandPath,
|
|
jsonOutputMode: options.builtInMachineOutput || hasJsonOutputFlag(normalizedArgv),
|
|
env: process.env,
|
|
});
|
|
const useSourceOnlyBestEffortConfig =
|
|
!(await isCurrentRuntimeSupported()) ||
|
|
normalizedInvocation.primary === "update" ||
|
|
normalizedInvocation.primary === "doctor";
|
|
const readBestEffortCliConfig = async (): Promise<OpenClawConfig> => {
|
|
if (!bestEffortConfigPromise) {
|
|
bestEffortConfigPromise = import("../config/io.js").then(async (configIo) => {
|
|
if (useSourceOnlyBestEffortConfig) {
|
|
return configIo.readSourceConfigBestEffort();
|
|
}
|
|
const readOptions: Parameters<typeof configIo.readBestEffortConfig>[0] = {
|
|
// Routing must not create state before Doctor decides whether migrations are needed.
|
|
observe: false,
|
|
...(isolateProxyConfigEnv ? { isolateEnv: true } : {}),
|
|
...(bestEffortConfigStartupPolicy.validateConfigOnly || isGatewayRunInvocation
|
|
? { pluginValidation: "core-only" }
|
|
: { skipPluginValidation: true }),
|
|
};
|
|
if (!resolveUnownedCliPrimaryCandidate(normalizedArgv)) {
|
|
return configIo.readBestEffortConfig(readOptions);
|
|
}
|
|
const session = await getPluginCliSession();
|
|
const snapshot = await session.readConfig(() =>
|
|
configIo.readBestEffortConfigSnapshot(readOptions),
|
|
);
|
|
if (snapshot.configDiagnostics) {
|
|
const { path: configPath, issues } = snapshot.configDiagnostics;
|
|
throw createInvalidConfigError(configPath, formatInvalidConfigDetails(issues));
|
|
}
|
|
return snapshot.config;
|
|
});
|
|
}
|
|
return await bestEffortConfigPromise;
|
|
};
|
|
const uninstallProxySignalHandlers = () => {
|
|
if (onSigterm) {
|
|
process.off("SIGTERM", onSigterm);
|
|
onSigterm = null;
|
|
}
|
|
if (onSigint) {
|
|
process.off("SIGINT", onSigint);
|
|
onSigint = null;
|
|
}
|
|
if (onExit) {
|
|
process.off("exit", onExit);
|
|
onExit = null;
|
|
}
|
|
};
|
|
const stopStartedProxy = () => {
|
|
if (proxyStopPromise) {
|
|
return proxyStopPromise;
|
|
}
|
|
unregisterProxySignalExitBarrier?.();
|
|
unregisterProxySignalExitBarrier = null;
|
|
uninstallProxySignalHandlers();
|
|
const handle = proxyHandle;
|
|
proxyHandle = null;
|
|
const stop = async () => {
|
|
if (handle) {
|
|
const { stopProxy } = await loadProxyLifecycleModule();
|
|
await stopProxy(handle);
|
|
}
|
|
};
|
|
const resources = options.harnessCleanup?.pluginResources;
|
|
proxyStopPromise = Promise.resolve().then(() =>
|
|
resources ? resources.runCleanup(stop) : stop(),
|
|
);
|
|
return proxyStopPromise;
|
|
};
|
|
const killStartedProxy = () => {
|
|
const handle = proxyHandle;
|
|
proxyHandle = null;
|
|
handle?.kill("SIGTERM");
|
|
};
|
|
const installProxySignalHandlers = () => {
|
|
if (!proxyHandle || onSigterm || onSigint || onExit) {
|
|
return;
|
|
}
|
|
unregisterProxySignalExitBarrier = registerSignalExitBarrier(stopStartedProxy);
|
|
const shutdown = (exitCode: number) => {
|
|
void waitForSignalExitBarriers().finally(() => {
|
|
process.exit(exitCode);
|
|
});
|
|
};
|
|
onSigterm = () => shutdown(143);
|
|
onSigint = () => shutdown(130);
|
|
onExit = () => killStartedProxy();
|
|
process.once("SIGTERM", onSigterm);
|
|
process.once("SIGINT", onSigint);
|
|
process.once("exit", onExit);
|
|
};
|
|
const replaceStartedProxy = async (config: OpenClawConfig["proxy"]) => {
|
|
await stopStartedProxy();
|
|
const { startProxy } = await loadProxyLifecycleModule();
|
|
proxyHandle = await startProxy(config);
|
|
proxyStopPromise = undefined;
|
|
installProxySignalHandlers();
|
|
};
|
|
let uninstallGatewayRunRuntimeHooks: (() => void) | null = null;
|
|
let unhandledRejectionHandlerInstalled = false;
|
|
|
|
try {
|
|
const startupTraces = [startupTrace, options.additionalStartupTrace].filter(
|
|
(trace): trace is ReturnType<typeof createGatewayDispatchStartupTrace> => Boolean(trace),
|
|
);
|
|
if (
|
|
!isDatabaseInvocation &&
|
|
(await Promise.all(startupTraces.map((trace) => trace.requiresDiagnosticsConfig()))).some(
|
|
Boolean,
|
|
)
|
|
) {
|
|
const config = await withConsoleLogsRoutedToStderr(readBestEffortCliConfig);
|
|
await Promise.all(startupTraces.map((trace) => trace.configureDiagnosticsTimeline(config)));
|
|
}
|
|
if (
|
|
!isHelpOrVersionInvocation &&
|
|
!bareSessionInvocation &&
|
|
normalizedInvocation.primary &&
|
|
!isKnownBuiltInCommandRoot(normalizedInvocation.primary)
|
|
) {
|
|
const config = await withConsoleLogsRoutedToStderr(readBestEffortCliConfig);
|
|
if (
|
|
await withConsoleLogsRoutedToStderr(() =>
|
|
resolvePluginMachineOutput({ argv: normalizedArgv, config, session: pluginCliSession }),
|
|
)
|
|
) {
|
|
const { routeLogsToStderr } = await loadLoggingModule();
|
|
routeLogsToStderr();
|
|
}
|
|
}
|
|
if (!isHelpOrVersionInvocation && shouldStartProxyForCli(normalizedArgv)) {
|
|
const config = await withConsoleLogsRoutedToStderr(readBestEffortCliConfig);
|
|
if (!bareSessionInvocation) {
|
|
const unownedPrimary = await resolveUnownedCliPrimary({
|
|
argv: normalizedArgv,
|
|
config,
|
|
session: pluginCliSession,
|
|
});
|
|
if (unownedPrimary) {
|
|
throw await resolveUnownedCliPrimaryError({
|
|
argv: normalizedArgv,
|
|
primary: unownedPrimary,
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
await replaceStartedProxy(config?.proxy ?? undefined);
|
|
}
|
|
|
|
if (!isHelpOrVersionInvocation && isGatewayRunInvocation) {
|
|
const { installGatewayRunRuntimeHooks } = await import("./gateway-cli/runtime-hooks.js");
|
|
uninstallGatewayRunRuntimeHooks = installGatewayRunRuntimeHooks({
|
|
releaseManagedProxy: stopStartedProxy,
|
|
refreshManagedProxy: replaceStartedProxy,
|
|
});
|
|
}
|
|
|
|
if (shouldUseRootHelpFastPath(normalizedArgv)) {
|
|
const { loadRootHelpRenderOptionsForConfigSensitivePlugins } =
|
|
await loadRootHelpLiveConfigModule();
|
|
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(
|
|
process.env,
|
|
);
|
|
if (!liveRootHelpOptions) {
|
|
const { outputPrecomputedRootHelpText } = await loadRootHelpMetadataModule();
|
|
if (outputPrecomputedRootHelpText()) {
|
|
return;
|
|
}
|
|
}
|
|
const { outputRootHelp } = await import("./program/root-help.js");
|
|
await outputRootHelp(liveRootHelpOptions ?? undefined);
|
|
return;
|
|
}
|
|
|
|
if (await tryOutputPrecomputedCommandHelp(normalizedArgv)) {
|
|
return;
|
|
}
|
|
|
|
if (shouldUseSetupOnboardConfigureHelpFastPath(normalizedArgv)) {
|
|
const { tryOutputSetupOnboardConfigureHelp } =
|
|
await import("./setup-onboard-configure-help-fast-path.js");
|
|
if (await tryOutputSetupOnboardConfigureHelp(normalizedArgv)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Genuine help fast paths have returned. Any remaining help/version-shaped
|
|
// invocation can still fail validation and must honor the console style.
|
|
await installConsoleCapture();
|
|
|
|
if (bareSessionInvocation) {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
console.error(
|
|
"OpenClaw TUI needs an interactive TTY. Use `openclaw agent --local ...` for automation.",
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const { runTuiCliAction } = await import("./tui-cli.js");
|
|
await runTuiCliAction(bareSessionInvocation.target, bareSessionInvocation.options);
|
|
return;
|
|
}
|
|
|
|
// Reject unowned command roots before help/version routing, so that
|
|
// `openclaw <typo> --help` surfaces the same Unknown command error as
|
|
// `openclaw <typo>` instead of silently showing generic top-level help.
|
|
// Runs after legitimate precomputed help fast paths so known help commands
|
|
// still dispatch normally. See #81077.
|
|
{
|
|
const unownedPrimaryCandidate = resolveUnownedCliPrimaryCandidate(normalizedArgv);
|
|
if (unownedPrimaryCandidate) {
|
|
const config = await readBestEffortCliConfig();
|
|
const unownedPrimary = await resolveUnownedCliPrimary({
|
|
argv: normalizedArgv,
|
|
config,
|
|
session: pluginCliSession,
|
|
});
|
|
if (unownedPrimary) {
|
|
throw await resolveUnownedCliPrimaryError({
|
|
argv: normalizedArgv,
|
|
primary: unownedPrimary,
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const shouldRunBareRootCommand = shouldHandleBareRoot(normalizedArgv);
|
|
if (shouldRunBareRootCommand) {
|
|
await ensureCliEnvProxyDispatcher();
|
|
}
|
|
const bareRootLaunchTarget = shouldRunBareRootCommand
|
|
? await resolveBareRootLaunchTarget(normalizedArgv)
|
|
: null;
|
|
|
|
if (bareRootLaunchTarget) {
|
|
if (bareRootLaunchTarget.kind === "remote-gateway-inference") {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
console.error(
|
|
"Remote Gateway inference setup needs an interactive TTY. Re-run `openclaw` in a terminal connected to this Gateway.",
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const { runRemoteGatewayInferenceOnboarding } =
|
|
await import("../commands/onboard-remote-gateway.js");
|
|
await runRemoteGatewayInferenceOnboarding(bareRootLaunchTarget.target);
|
|
return;
|
|
}
|
|
if (bareRootLaunchTarget.kind === "onboarding") {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
console.error(
|
|
bareRootLaunchTarget.classic
|
|
? "OpenClaw config is invalid. Run `openclaw doctor --fix` before onboarding."
|
|
: "Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const { setupWizardCommand } = await import("../commands/onboard.js");
|
|
await setupWizardCommand(bareRootLaunchTarget.classic ? { classic: true } : {});
|
|
return;
|
|
}
|
|
if (bareRootLaunchTarget.kind === "tui") {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
console.error(
|
|
"OpenClaw TUI needs an interactive TTY. Use `openclaw agent --local ...` for automation.",
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const { runTui } = await import("../tui/tui.js");
|
|
// This TUI now shares the CLI process, so keep its final exit fallback armed
|
|
// in case imported runtime handles survive the normal teardown.
|
|
await runTui({
|
|
...(bareRootLaunchTarget.local
|
|
? { deliver: false, local: true }
|
|
: {
|
|
deliver: false,
|
|
config: bareRootLaunchTarget.config,
|
|
boundGateway: {
|
|
url: bareRootLaunchTarget.gatewayUrl,
|
|
...(bareRootLaunchTarget.token ? { token: bareRootLaunchTarget.token } : {}),
|
|
...(bareRootLaunchTarget.password
|
|
? { password: bareRootLaunchTarget.password }
|
|
: {}),
|
|
...(bareRootLaunchTarget.tlsFingerprint
|
|
? { tlsFingerprint: bareRootLaunchTarget.tlsFingerprint }
|
|
: {}),
|
|
},
|
|
}),
|
|
forceProcessExitOnReturn: true,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const shouldUseCliEnvProxy =
|
|
!isHelpOrVersionInvocation && shouldStartProxyForCli(normalizedArgv);
|
|
const bootstrapProxyBeforeFastPath =
|
|
shouldUseCliEnvProxy && shouldBootstrapCliProxyBeforeFastPath();
|
|
// Gateway execution can return before full CLI bootstrap, so install the
|
|
// shared process policy before the fast path can start asynchronous work.
|
|
if (isGatewayRunFastPathArgv(normalizedArgv)) {
|
|
const { installUnhandledRejectionHandler } = await startupTrace.measure(
|
|
"unhandled-rejection-handler-import",
|
|
() => import("../infra/unhandled-rejections.js"),
|
|
);
|
|
installUnhandledRejectionHandler();
|
|
unhandledRejectionHandlerInstalled = true;
|
|
}
|
|
if (
|
|
!bootstrapProxyBeforeFastPath &&
|
|
(await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!isHelpOrVersionInvocation && !isDatabaseInvocation) {
|
|
await bootstrapCliProxyCaptureAndDispatcher(startupTrace, {
|
|
ensureDispatcher: shouldUseCliEnvProxy,
|
|
});
|
|
}
|
|
|
|
if (
|
|
bootstrapProxyBeforeFastPath &&
|
|
(await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!isHelpOrVersionInvocation) {
|
|
const route = await startupTrace.measure("route-import", () => import("./route.js"));
|
|
const routed = await startupTrace.measure(
|
|
"route",
|
|
() =>
|
|
options.builtInMachineOutput
|
|
? route.tryRouteCli(normalizedArgv, { machineOutput: true })
|
|
: route.tryRouteCli(normalizedArgv),
|
|
{ timeline: false },
|
|
);
|
|
if (routed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let parseArgv = normalizeGeneratedHelpCommandArgv(normalizedArgv);
|
|
const suppressStartupProgress = options.builtInMachineOutput || hasJsonOutputFlag(parseArgv);
|
|
const { createCliProgress } = await loadProgressModule();
|
|
const startupProgress = createCliProgress({
|
|
label: "Loading OpenClaw CLI…",
|
|
indeterminate: true,
|
|
delayMs: 0,
|
|
...(suppressStartupProgress ? { enabled: false } : {}),
|
|
});
|
|
let startupProgressStopped = false;
|
|
const stopStartupProgress = () => {
|
|
if (startupProgressStopped) {
|
|
return;
|
|
}
|
|
startupProgressStopped = true;
|
|
startupProgress.done();
|
|
};
|
|
|
|
try {
|
|
const [
|
|
{ buildProgram },
|
|
{ formatUncaughtError },
|
|
{ formatCliFailureLines, formatCliJsonFailure },
|
|
{ runFatalErrorHooks },
|
|
{
|
|
installUnhandledRejectionHandler,
|
|
isBenignUncaughtExceptionError,
|
|
isUncaughtExceptionHandled,
|
|
},
|
|
{ defaultRuntime, restoreRuntimeTerminalState },
|
|
] = await startupTrace.measure("core-imports", () =>
|
|
Promise.all([
|
|
import("./program.js"),
|
|
import("../infra/errors.js"),
|
|
import("./failure-output.js"),
|
|
import("../infra/fatal-error-hooks.js"),
|
|
import("../infra/unhandled-rejections.js"),
|
|
import("../runtime.js"),
|
|
]),
|
|
);
|
|
const program = await startupTrace.measure("build-program", () => buildProgram());
|
|
await options.harnessCleanup?.pluginResources?.waitForRegistrations();
|
|
|
|
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
|
// These log the error and exit gracefully instead of crashing without trace.
|
|
if (!unhandledRejectionHandlerInstalled) {
|
|
installUnhandledRejectionHandler();
|
|
}
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
if (isUncaughtExceptionHandled(error)) {
|
|
return;
|
|
}
|
|
if (isBenignUncaughtExceptionError(error)) {
|
|
console.warn(
|
|
"[openclaw] Non-fatal uncaught exception (continuing):",
|
|
formatUncaughtError(error),
|
|
);
|
|
return;
|
|
}
|
|
if (isJsonOutputModeActive(normalizedArgv)) {
|
|
defaultRuntime.writeJson(formatCliJsonFailure(error));
|
|
}
|
|
for (const line of formatCliFailureLines({
|
|
title: "OpenClaw hit an unexpected runtime error.",
|
|
error,
|
|
argv: normalizedArgv,
|
|
})) {
|
|
console.error(line);
|
|
}
|
|
for (const message of runFatalErrorHooks({ reason: "uncaught_exception", error })) {
|
|
console.error("[openclaw]", message);
|
|
}
|
|
restoreRuntimeTerminalState("uncaught exception", { resumeStdinIfPaused: false });
|
|
process.exit(1);
|
|
});
|
|
|
|
const invocation = resolveCliArgvInvocation(parseArgv);
|
|
// Register the primary command (builtin or subcli) so help and command parsing
|
|
// are correct even with lazy command registration.
|
|
const { primary } = invocation;
|
|
if (primary) {
|
|
await startupTrace.measure("register-primary", async () => {
|
|
const { getProgramContext } = await import("./program/program-context.js");
|
|
const ctx = getProgramContext(program);
|
|
if (ctx) {
|
|
const { registerCoreCliByName } = await import("./program/command-registry.js");
|
|
await registerCoreCliByName(program, ctx, primary);
|
|
}
|
|
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
|
await registerSubCliByName(program, primary, parseArgv);
|
|
});
|
|
}
|
|
|
|
const hasBuiltinPrimary =
|
|
primary !== null &&
|
|
program.commands.some(
|
|
(command) => command.name() === primary || command.aliases().includes(primary),
|
|
);
|
|
const shouldSkipPluginRegistration = shouldSkipPluginCommandRegistration({
|
|
argv: parseArgv,
|
|
primary,
|
|
hasBuiltinPrimary,
|
|
});
|
|
if (!shouldSkipPluginRegistration) {
|
|
const config = await startupTrace.measure("register-plugin-commands", async () => {
|
|
const { registerPluginCliCommandsFromValidatedConfig } =
|
|
await import("../plugins/cli.js");
|
|
const startupPolicy = resolveCliStartupPolicyForArgv({
|
|
argv: parseArgv,
|
|
commandPath: invocation.commandPath,
|
|
jsonOutputMode: suppressStartupProgress,
|
|
});
|
|
return await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
|
|
mode: "lazy",
|
|
primary,
|
|
skipPluginValidation: startupPolicy.skipConfigGuard,
|
|
session: await getPluginCliSession(),
|
|
});
|
|
});
|
|
if (
|
|
primary &&
|
|
!program.commands.some(
|
|
(command) => command.name() === primary || command.aliases().includes(primary),
|
|
)
|
|
) {
|
|
const { resolveManifestCommandAliasOwner, resolveManifestToolOwner } =
|
|
await loadManifestCommandAliasesRuntimeModule();
|
|
const cliCommandSurfaceOwner = await resolveCliCommandSurfaceOwner({
|
|
primary,
|
|
config,
|
|
});
|
|
const missingPluginCommandMessage = resolveMissingPluginCommandMessage(primary, config, {
|
|
resolveCommandAliasOwner: resolveManifestCommandAliasOwner,
|
|
resolveToolOwner: resolveManifestToolOwner,
|
|
resolveCliCommandSurfaceOwner: () => cliCommandSurfaceOwner,
|
|
});
|
|
if (missingPluginCommandMessage) {
|
|
throw await createExpectedPluginPolicyError(missingPluginCommandMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
parseArgv = normalizeRootLogLevelArgvForProgram(
|
|
normalizeRootNoColorArgvForProgram(parseArgv, program),
|
|
program,
|
|
);
|
|
stopStartupProgress();
|
|
|
|
let completedHelpOrVersion = false;
|
|
try {
|
|
const resources = options.harnessCleanup?.pluginResources;
|
|
await resources?.waitForRegistrations();
|
|
pluginCliSession?.close();
|
|
// The invocation's cache scope survives closed preparation through action completion.
|
|
const parse = () =>
|
|
startupTrace.measure("parse", () => program.parseAsync(parseArgv), {
|
|
timeline: false,
|
|
});
|
|
await (resources ? resources.run(parse) : parse());
|
|
await resources?.waitForRegistrations();
|
|
completedHelpOrVersion = isHelpOrVersionInvocation;
|
|
} catch (error) {
|
|
if (!isCommanderParseExit(error)) {
|
|
throw error;
|
|
}
|
|
if (isJsonOutputModeActive(parseArgv) && error.exitCode !== 0) {
|
|
throw error;
|
|
}
|
|
process.exitCode = error.exitCode;
|
|
completedHelpOrVersion = isHelpOrVersionInvocation && error.exitCode === 0;
|
|
}
|
|
if (completedHelpOrVersion) {
|
|
// Lazy command-group registrars can import native/runtime resources solely to
|
|
// render complete help. Request an exit now; the top-level finally flushes it
|
|
// after shared async teardown completes.
|
|
requestExitAfterOneShotOutput();
|
|
}
|
|
} finally {
|
|
stopStartupProgress();
|
|
}
|
|
} finally {
|
|
pluginCliSession?.close();
|
|
uninstallGatewayRunRuntimeHooks?.();
|
|
const resources = options.harnessCleanup?.pluginResources;
|
|
await runCliDisposer("managed-proxy", stopStartedProxy, resources?.runCleanup);
|
|
await closeCliResources(options.harnessCleanup);
|
|
if (!resources) {
|
|
pauseNonTtyStdinForCliExit();
|
|
}
|
|
}
|
|
}
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|