// Global Commander pre-action hook: startup presentation, config guard, logging, and plugin preflight. import type { Command } from "commander"; import type { ConfigFileSnapshot } from "../../config/types.js"; import { setVerbose } from "../../globals.js"; import type { LogLevel } from "../../logging/levels.js"; import { resolvePluginInstallInvalidConfigPolicy } from "../../plugins/install-config.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveCliArgvInvocation } from "../argv-invocation.js"; import { getVerboseFlag, isHelpOrVersionInvocation } from "../argv.js"; import { CLI_NAME } from "../cli-name.js"; import { applyCliExecutionStartupPresentation, ensureCliExecutionBootstrap, } from "../command-execution-startup.js"; import { inheritOptionFromParent } from "../command-options.js"; import { resolveCliCommandPathPolicy } from "../command-path-policy.js"; import { resolveCliStartupPolicy } from "../command-startup-policy.js"; import { applyResolvedCommandOutputMode } from "../json-output-mode.js"; import { isModelsPlainMachineOutput } from "../models-output-mode.js"; import { resolvePluginInstallPreactionRequest } from "../plugin-install-config-policy.js"; import { getCommanderCommandPath, hasCommanderOptionToken } from "./commander-parse-facts.js"; import { isCommandJsonOutputMode } from "./json-mode.js"; import { isParentDefaultHelpAction } from "./parent-default-help.js"; const HELP_OR_VERSION_FLAGS = new Set(["-h", "--help", "-V", "--version"]); function setProcessTitleForCommand(actionCommand: Command) { let current: Command = actionCommand; while (current.parent && current.parent.parent) { current = current.parent; } const name = current.name(); if (!name || name === CLI_NAME) { return; } process.title = `${CLI_NAME}-${name}`; } function shouldAllowInvalidConfigForAction(actionCommand: Command, commandPath: string[]): boolean { return ( commandPath[0] === "update" || resolvePluginInstallInvalidConfigPolicy( resolvePluginInstallPreactionRequest({ actionCommand, commandPath, argv: process.argv, }), ) === "allow-plugin-recovery" ); } function getCliLogLevel(actionCommand: Command): LogLevel | undefined { if (actionCommand.getOptionValueSourceWithGlobals("logLevel") !== "cli") { return undefined; } const logLevel = actionCommand.optsWithGlobals<{ logLevel?: unknown }>().logLevel; return typeof logLevel === "string" ? (logLevel as LogLevel) : undefined; } function getStateMigrationAgentId(actionCommand: Command): string | undefined { if (!actionCommand.options.some((option) => option.attributeName() === "agent")) { return undefined; } const value = actionCommand.getOptionValueSource("agent") === "cli" ? actionCommand.getOptionValue("agent") : inheritOptionFromParent(actionCommand, "agent", "cli"); return typeof value === "string" && value.trim() ? value.trim() : undefined; } function isBareParentDefaultHelpInvocation(actionCommand: Command, argv: string[]): boolean { if (!isParentDefaultHelpAction(actionCommand)) { return false; } const { commandPath } = resolveCliArgvInvocation(argv); const [primary, extra] = commandPath; if (extra !== undefined || !primary) { return false; } return primary === actionCommand.name() || actionCommand.aliases().includes(primary); } function isGuidedConfigAction(actionCommand: Command): boolean { return actionCommand.name() === "config" && !actionCommand.parent?.parent; } function isGuidedConfigCommandPath(commandPath: string[]): boolean { const [primary, secondary, extra] = commandPath; if (primary !== "config" || extra !== undefined) { return false; } return ( secondary !== "get" && secondary !== "set" && secondary !== "patch" && secondary !== "unset" && secondary !== "file" && secondary !== "schema" && secondary !== "validate" ); } function isGatewayRunAction(actionCommand: Command): boolean { if (actionCommand.name() === "gateway") { return actionCommand.parent?.parent === null; } return ( actionCommand.name() === "run" && actionCommand.parent?.name() === "gateway" && actionCommand.parent.parent?.parent === null ); } async function runStateStoreGuard(commandPath: string[]): Promise { if (resolveCliCommandPathPolicy(commandPath).stateStoreGuard !== "run") { return; } let outcome: import("../state-dir-gateway-check.js").CliGatewayStateDirOutcome; try { const { checkCliGatewayStateDir } = await import("../state-dir-gateway-check.js"); outcome = await checkCliGatewayStateDir({ command: `openclaw ${commandPath.join(" ")}` }); } catch (error) { const { formatErrorMessage } = await import("../../infra/errors.js"); const { logDebug } = await import("../../logger.js"); logDebug(`state-store guard unavailable: ${formatErrorMessage(error)}`); return; } if (outcome.kind === "warn") { defaultRuntime.log(outcome.message); } else if (outcome.kind === "refuse") { throw new Error(outcome.message); } } /** Register global pre-action bootstrap hooks for every non-help command invocation. */ export function registerPreActionHooks(program: Command, programVersion: string) { program.hook("preAction", async (_thisCommand, actionCommand) => { setProcessTitleForCommand(actionCommand); const argv = process.argv; const helpOrVersionWasOptionValue = hasCommanderOptionToken( actionCommand, argv, HELP_OR_VERSION_FLAGS, "value", ); if ( (isHelpOrVersionInvocation(argv) && !helpOrVersionWasOptionValue) || isBareParentDefaultHelpInvocation(actionCommand, argv) ) { return; } const commandPath = getCommanderCommandPath(actionCommand); const nativeUpdateCapabilityProbe = commandPath.length === 2 && (commandPath[0] === "gateway" || commandPath[0] === "daemon") && ["install", "restart", "stop"].includes(commandPath[1] ?? "") && actionCommand.getOptionValue("updateExecutor") === "check"; const jsonOutputMode = nativeUpdateCapabilityProbe || isCommandJsonOutputMode(actionCommand, argv); const machineOutputMode = jsonOutputMode || isModelsPlainMachineOutput(argv, actionCommand); applyResolvedCommandOutputMode(jsonOutputMode, machineOutputMode); const startupPolicy = resolveCliStartupPolicy({ argv, commandPath, jsonOutputMode, machineOutputMode, env: process.env, }); await applyCliExecutionStartupPresentation({ startupPolicy, version: programVersion, }); const verbose = getVerboseFlag(argv, { includeDebug: true }); setVerbose(verbose); const cliLogLevel = getCliLogLevel(actionCommand); if (cliLogLevel) { process.env.OPENCLAW_LOG_LEVEL = cliLogLevel; } if (!verbose) { process.env.NODE_NO_WARNINGS ??= "1"; } // Capability discovery precedes staged-update admission and must not migrate live state. if ( nativeUpdateCapabilityProbe || isGuidedConfigAction(actionCommand) || isGuidedConfigCommandPath(commandPath) ) { return; } await runStateStoreGuard(commandPath); if (startupPolicy.skipConfigGuard) { // Config validation and plugin activation are independent startup policies. // A cold config read must not suppress a plugin runtime explicitly required by the command. await ensureCliExecutionBootstrap({ runtime: defaultRuntime, commandPath, startupPolicy, skipConfigGuard: true, }); return; } let beforeStateMigrations: ((snapshot?: ConfigFileSnapshot) => Promise) | undefined; let skipPristineStartupStateMigrations = false; let skipPristineCoreStateMigrations = false; let allowInvalid = shouldAllowInvalidConfigForAction(actionCommand, commandPath); if (isGatewayRunAction(actionCommand)) { const { prepareGatewayRunBootstrap, recheckGatewayRunBootstrap, wasPreparedGatewayRunCoreStatePristine, wasPreparedGatewayRunStatePristine, } = await import("../gateway-cli/pre-bootstrap.js"); const { resolveGatewayRunOptions } = await import("../gateway-cli/run-options.js"); const resolvedOptions = resolveGatewayRunOptions(actionCommand.opts(), actionCommand); allowInvalid ||= resolvedOptions.allowUnconfigured === true; const opts = resolvedOptions; const shouldBootstrap = await prepareGatewayRunBootstrap({ opts, runtime: defaultRuntime }); if (!shouldBootstrap) { return; } skipPristineStartupStateMigrations = wasPreparedGatewayRunStatePristine(); skipPristineCoreStateMigrations = wasPreparedGatewayRunCoreStatePristine(); beforeStateMigrations = (snapshot) => recheckGatewayRunBootstrap({ opts, runtime: defaultRuntime, ...(snapshot ? { snapshot } : {}), }); } const stateMigrationAgentId = getStateMigrationAgentId(actionCommand); if (stateMigrationAgentId) { const existingGuard = beforeStateMigrations; beforeStateMigrations = async (snapshot) => { if (snapshot) { const { isValidAgentId, normalizeAgentId } = await import("@openclaw/normalization-core/agent-id"); if (isValidAgentId(stateMigrationAgentId)) { const [{ listAgentIds }, { retainLegacyDefaultAgentId }] = await Promise.all([ import("../../agents/agent-scope-config.js"), import("../../config/legacy.default-agent-owner.js"), ]); const agentId = normalizeAgentId(stateMigrationAgentId); if (listAgentIds(snapshot.sourceConfig).includes(agentId)) { retainLegacyDefaultAgentId(snapshot.sourceConfig, agentId); } } } return (await existingGuard?.(snapshot)) ?? true; }; } await ensureCliExecutionBootstrap({ runtime: defaultRuntime, commandPath, startupPolicy, allowInvalid, ...(beforeStateMigrations ? { beforeStateMigrations } : {}), ...(skipPristineStartupStateMigrations ? { skipPristineStartupStateMigrations: true } : {}), ...(skipPristineCoreStateMigrations ? { skipPristineCoreStateMigrations: true } : {}), }); if (beforeStateMigrations && isGatewayRunAction(actionCommand)) { const { reloadTrustedGatewayRunEnvironment } = await import("../gateway-cli/pre-bootstrap.js"); await reloadTrustedGatewayRunEnvironment({ runtime: defaultRuntime }); } }); }