mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-13 10:11:20 +00:00
274 lines
9.7 KiB
TypeScript
274 lines
9.7 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
import { CommanderError } from "commander";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { resolveStateDir } from "../config/paths.js";
|
|
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 { resolveManifestCommandAliasOwner } from "../plugins/manifest-command-aliases.runtime.js";
|
|
import { hasMemoryRuntime } from "../plugins/memory-state.js";
|
|
import {
|
|
normalizeLowercaseStringOrEmpty,
|
|
normalizeOptionalLowercaseString,
|
|
normalizeOptionalString,
|
|
} from "../shared/string-coerce.js";
|
|
import { resolveCliArgvInvocation } from "./argv-invocation.js";
|
|
import {
|
|
shouldRegisterPrimaryCommandOnly,
|
|
shouldSkipPluginCommandRegistration,
|
|
} from "./command-registration-policy.js";
|
|
import { shouldEnsureCliPathForCommandPath } from "./command-startup-policy.js";
|
|
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
|
|
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
|
|
import { tryRouteCli } from "./route.js";
|
|
import { normalizeWindowsArgv } from "./windows-argv.js";
|
|
|
|
async function closeCliMemoryManagers(): Promise<void> {
|
|
if (!hasMemoryRuntime()) {
|
|
return;
|
|
}
|
|
try {
|
|
const { closeActiveMemorySearchManagers } = await import("../plugins/memory-runtime.js");
|
|
await closeActiveMemorySearchManagers();
|
|
} 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 shouldEnsureCliPath(argv: string[]): boolean {
|
|
const invocation = resolveCliArgvInvocation(argv);
|
|
if (invocation.hasHelpOrVersion) {
|
|
return false;
|
|
}
|
|
return shouldEnsureCliPathForCommandPath(invocation.commandPath);
|
|
}
|
|
|
|
export function shouldUseRootHelpFastPath(argv: string[]): boolean {
|
|
return resolveCliArgvInvocation(argv).isRootHelpInvocation;
|
|
}
|
|
|
|
export function resolveMissingPluginCommandMessage(
|
|
pluginId: string,
|
|
config?: OpenClawConfig,
|
|
): string | null {
|
|
const normalizedPluginId = normalizeLowercaseStringOrEmpty(pluginId);
|
|
if (!normalizedPluginId) {
|
|
return null;
|
|
}
|
|
const allow =
|
|
Array.isArray(config?.plugins?.allow) && config.plugins.allow.length > 0
|
|
? config.plugins.allow
|
|
.filter((entry): entry is string => typeof entry === "string")
|
|
.map((entry) => normalizeOptionalLowercaseString(entry))
|
|
.filter(Boolean)
|
|
: [];
|
|
const commandAlias = resolveManifestCommandAliasOwner({
|
|
command: normalizedPluginId,
|
|
config,
|
|
});
|
|
const parentPluginId = commandAlias?.pluginId;
|
|
if (parentPluginId) {
|
|
if (allow.length > 0 && !allow.includes(parentPluginId)) {
|
|
return (
|
|
`"${normalizedPluginId}" is not a plugin; it is a command provided by the ` +
|
|
`"${parentPluginId}" plugin. Add "${parentPluginId}" to \`plugins.allow\` ` +
|
|
`instead of "${normalizedPluginId}".`
|
|
);
|
|
}
|
|
if (config?.plugins?.entries?.[parentPluginId]?.enabled === false) {
|
|
return (
|
|
`The \`openclaw ${normalizedPluginId}\` command is unavailable because ` +
|
|
`\`plugins.entries.${parentPluginId}.enabled=false\`. Re-enable that entry if you want ` +
|
|
"the bundled plugin command surface."
|
|
);
|
|
}
|
|
if (commandAlias.kind === "runtime-slash") {
|
|
const cliHint = commandAlias.cliCommand
|
|
? `Use \`openclaw ${commandAlias.cliCommand}\` for related CLI operations, or `
|
|
: "Use ";
|
|
return (
|
|
`"${normalizedPluginId}" is a runtime slash command (/${normalizedPluginId}), not a CLI command. ` +
|
|
`It is provided by the "${parentPluginId}" plugin. ` +
|
|
`${cliHint}\`/${normalizedPluginId}\` in a chat session.`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (allow.length > 0 && !allow.includes(normalizedPluginId)) {
|
|
return (
|
|
`The \`openclaw ${normalizedPluginId}\` command is unavailable because ` +
|
|
`\`plugins.allow\` excludes "${normalizedPluginId}". Add "${normalizedPluginId}" to ` +
|
|
`\`plugins.allow\` if you want that bundled plugin CLI surface.`
|
|
);
|
|
}
|
|
if (config?.plugins?.entries?.[normalizedPluginId]?.enabled === false) {
|
|
return (
|
|
`The \`openclaw ${normalizedPluginId}\` command is unavailable because ` +
|
|
`\`plugins.entries.${normalizedPluginId}.enabled=false\`. Re-enable that entry if you want ` +
|
|
"the bundled plugin CLI surface."
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function shouldLoadCliDotEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
if (existsSync(path.join(process.cwd(), ".env"))) {
|
|
return true;
|
|
}
|
|
return existsSync(path.join(resolveStateDir(env), ".env"));
|
|
}
|
|
|
|
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 ?? normalizeOptionalString(process.env.OPENCLAW_CONTAINER) ?? null;
|
|
if (containerTargetName && parsedProfile.profile) {
|
|
throw new Error("--container cannot be combined with --profile/--dev");
|
|
}
|
|
|
|
const containerTarget = maybeRunCliInContainer(originalArgv);
|
|
if (containerTarget.handled) {
|
|
if (containerTarget.exitCode !== 0) {
|
|
process.exitCode = containerTarget.exitCode;
|
|
}
|
|
return;
|
|
}
|
|
let normalizedArgv = parsedProfile.argv;
|
|
|
|
if (shouldLoadCliDotEnv()) {
|
|
const { loadCliDotEnv } = await import("./dotenv.js");
|
|
loadCliDotEnv({ quiet: true });
|
|
}
|
|
normalizeEnv();
|
|
if (shouldEnsureCliPath(normalizedArgv)) {
|
|
ensureOpenClawCliOnPath();
|
|
}
|
|
|
|
// Enforce the minimum supported runtime before doing any work.
|
|
assertSupportedRuntime();
|
|
|
|
try {
|
|
if (shouldUseRootHelpFastPath(normalizedArgv)) {
|
|
const { outputPrecomputedRootHelpText } = await import("./root-help-metadata.js");
|
|
if (!outputPrecomputedRootHelpText()) {
|
|
const { outputRootHelp } = await import("./program/root-help.js");
|
|
await outputRootHelp();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (await tryRouteCli(normalizedArgv)) {
|
|
return;
|
|
}
|
|
|
|
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
|
enableConsoleCapture();
|
|
|
|
const [{ buildProgram }, { installUnhandledRejectionHandler }, { restoreTerminalState }] =
|
|
await Promise.all([
|
|
import("./program.js"),
|
|
import("../infra/unhandled-rejections.js"),
|
|
import("../terminal/restore.js"),
|
|
]);
|
|
const program = buildProgram();
|
|
|
|
// 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));
|
|
restoreTerminalState("uncaught exception", { resumeStdinIfPaused: false });
|
|
process.exit(1);
|
|
});
|
|
|
|
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
|
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 && shouldRegisterPrimaryCommandOnly(parseArgv)) {
|
|
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 { registerPluginCliCommandsFromValidatedConfig } = await import("../plugins/cli.js");
|
|
const config = await registerPluginCliCommandsFromValidatedConfig(
|
|
program,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
mode: "lazy",
|
|
primary,
|
|
},
|
|
);
|
|
if (config) {
|
|
if (primary && !program.commands.some((command) => command.name() === primary)) {
|
|
const missingPluginCommandMessage = resolveMissingPluginCommandMessage(primary, config);
|
|
if (missingPluginCommandMessage) {
|
|
throw new Error(missingPluginCommandMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
await program.parseAsync(parseArgv);
|
|
} catch (error) {
|
|
if (!(error instanceof CommanderError)) {
|
|
throw error;
|
|
}
|
|
process.exitCode = error.exitCode;
|
|
}
|
|
} finally {
|
|
await closeCliMemoryManagers();
|
|
}
|
|
}
|
|
|
|
export function isCliMainModule(): boolean {
|
|
return isMainModule({ currentFile: fileURLToPath(import.meta.url) });
|
|
}
|