Files
openclaw/src/cli/run-main.ts
2026-03-24 10:17:17 -04:00

199 lines
6.4 KiB
TypeScript

import process from "node:process";
import { fileURLToPath } from "node:url";
import { normalizeEnv } from "../infra/env.js";
import { formatUncaughtError } from "../infra/errors.js";
import { isMainModule } from "../infra/is-main.js";
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
import { enableConsoleCapture } from "../logging.js";
import {
getCommandPathWithRootOptions,
getPrimaryCommand,
hasHelpOrVersion,
isRootHelpInvocation,
} from "./argv.js";
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
import { loadCliDotEnv } from "./dotenv.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
import { tryRouteCli } from "./route.js";
import { normalizeWindowsArgv } from "./windows-argv.js";
async function closeCliMemoryManagers(): Promise<void> {
try {
const { closeAllMemorySearchManagers } = await import("../memory/search-manager.js");
await closeAllMemorySearchManagers();
} catch {
// Best-effort teardown for short-lived CLI processes.
}
}
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
const index = argv.indexOf("--update");
if (index === -1) {
return argv;
}
const next = [...argv];
next.splice(index, 1, "update");
return next;
}
export function shouldRegisterPrimarySubcommand(argv: string[]): boolean {
return !hasHelpOrVersion(argv);
}
export function shouldSkipPluginCommandRegistration(params: {
argv: string[];
primary: string | null;
hasBuiltinPrimary: boolean;
}): boolean {
if (params.hasBuiltinPrimary) {
return true;
}
if (!params.primary) {
return hasHelpOrVersion(params.argv);
}
return false;
}
export function shouldEnsureCliPath(argv: string[]): boolean {
if (hasHelpOrVersion(argv)) {
return false;
}
const [primary, secondary] = getCommandPathWithRootOptions(argv, 2);
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 shouldUseRootHelpFastPath(argv: string[]): boolean {
return isRootHelpInvocation(argv);
}
export async function runCli(argv: string[] = process.argv) {
const originalArgv = normalizeWindowsArgv(argv);
const parsedContainer = parseCliContainerArgs(originalArgv);
if (!parsedContainer.ok) {
throw new Error(parsedContainer.error);
}
const parsedProfile = parseCliProfileArgs(parsedContainer.argv);
if (!parsedProfile.ok) {
throw new Error(parsedProfile.error);
}
if (parsedProfile.profile) {
applyCliProfileEnv({ profile: parsedProfile.profile });
}
const containerTargetName =
parsedContainer.container ?? process.env.OPENCLAW_CONTAINER?.trim() ?? null;
if (
containerTargetName &&
(parsedProfile.profile ||
process.env.OPENCLAW_PROFILE?.trim() ||
process.env.OPENCLAW_GATEWAY_PORT?.trim() ||
process.env.OPENCLAW_GATEWAY_URL?.trim() ||
process.env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
process.env.OPENCLAW_GATEWAY_PASSWORD?.trim())
) {
throw new Error(
"--container cannot be combined with --profile/--dev or gateway override env vars",
);
}
const containerTarget = maybeRunCliInContainer(originalArgv);
if (containerTarget.handled) {
if (containerTarget.exitCode !== 0) {
process.exitCode = containerTarget.exitCode;
}
return;
}
let normalizedArgv = parsedProfile.argv;
loadCliDotEnv({ quiet: true });
normalizeEnv();
if (shouldEnsureCliPath(normalizedArgv)) {
ensureOpenClawCliOnPath();
}
// Enforce the minimum supported runtime before doing any work.
assertSupportedRuntime();
try {
if (shouldUseRootHelpFastPath(normalizedArgv)) {
const { outputRootHelp } = await import("./program/root-help.js");
outputRootHelp();
return;
}
if (await tryRouteCli(normalizedArgv)) {
return;
}
// Capture all console output into structured logs while keeping stdout/stderr behavior.
enableConsoleCapture();
const { buildProgram } = await import("./program.js");
const program = buildProgram();
const { installUnhandledRejectionHandler } = await import("../infra/unhandled-rejections.js");
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
// These log the error and exit gracefully instead of crashing without trace.
installUnhandledRejectionHandler();
process.on("uncaughtException", (error) => {
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
process.exit(1);
});
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
// Register the primary command (builtin or subcli) so help and command parsing
// are correct even with lazy command registration.
const primary = getPrimaryCommand(parseArgv);
if (primary) {
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, parseArgv);
}
const { registerSubCliByName } = await import("./program/register.subclis.js");
await registerSubCliByName(program, primary);
}
const hasBuiltinPrimary =
primary !== null && program.commands.some((command) => command.name() === primary);
const shouldSkipPluginRegistration = shouldSkipPluginCommandRegistration({
argv: parseArgv,
primary,
hasBuiltinPrimary,
});
if (!shouldSkipPluginRegistration) {
// Register plugin CLI commands before parsing
const { registerPluginCliCommands } = await import("../plugins/cli.js");
const { loadValidatedConfigForPluginRegistration } =
await import("./program/register.subclis.js");
const config = await loadValidatedConfigForPluginRegistration();
if (config) {
registerPluginCliCommands(program, config);
}
}
await program.parseAsync(parseArgv);
} finally {
await closeCliMemoryManagers();
}
}
export function isCliMainModule(): boolean {
return isMainModule({ currentFile: fileURLToPath(import.meta.url) });
}