Despacho de comandos CLI
Responsabilidad
runCli(argv) es donde el comando de OpenClaw realmente empieza a despacharse. Tras recibir argv de la entrada, debe: parsear las opciones de profile/container, cargar .env, decidir si arranca el proxy, recorrer el fast path de help como red de seguridad, probar la ruta rápida route-first y, solo entonces, caer al programa Commander completo — registrar de forma dinámica el comando primario (primary command), registrar los comandos de plugin y, por último, llamar a program.parseAsync(argv) para que Commander reparta el trabajo.
Despachar (dispatch) en OpenClaw no es una sola acción, sino un embudo de tres capas:
- Fast path: peticiones puras de help como
--help/--version/browser --help/secrets --help/nodes --helpleen texto precompilado de vuelta sin cargar Commander. - Route-first:
tryRouteClienruta argv directamente a la implementación del comando usando una tablaroutedCommandspre-parseada, eludiendo el registro completo de Commander. - Commander completo:
buildProgram()construye el programa raíz de Commander, registra perezosamente los comandos primarios y los de plugin según se necesiten, y, por último,program.parseAsync.
Cada capa que acierta hace return; si no acierta, desciende a la siguiente. Así, las rutas calientes como openclaw gateway run (tryRunGatewayRunFastPath:L143) pueden saltarse la mayoría del coste de registro, mientras que las rutas frías como openclaw plugins install foo recorren Commander completo.
Motivación de diseño
¿Por qué un embudo de tres capas en lugar de un program.parseAsync(argv) directo? Porque antes de parseAsync, Commander exige buildProgram, que a su vez llama a registerProgramCommands para registrar todos los comandos núcleo y subclis. OpenClaw tiene más de 40 subclis (gateway/agent/cron/models/devices/plugins/channels/security/secrets/skills/...), y cada subcli tiene su propio árbol de subcomandos; registrar todo de forma eager costaría cientos de milisegundos de arranque en frío.
Hay varias decisiones clave:
- Registro primary-only: el primer token no-opcional de argv es el primary command (
gatewayenopenclaw gateway run). CuandoshouldRegisterPrimaryCommandOnly(argv)es verdadero, solo se registra ese; el resto de subclis se saltan. - Lazy import: el registrador de cada subcli es
() => import("../xxx-cli.js")con carga dinámica; solo se ejecuta cuando de verdad se necesita. - Hot path del gateway:
openclaw gateway runes lo más usado y tiene untryRunGatewayRunFastPathdedicado que omitebuildProgramy construye un programa Commander mínimo. - Route-first: algunos comandos ni siquiera necesitan la capacidad de parseo de Commander y se despachan directamente usando una tabla argv pre-parseada.
El coste son múltiples rutas de código y una curva de aprendizaje mayor; el beneficio es reducir el arranque en frío de openclaw gateway run de segundos a centenas de milisegundos.
Archivos clave
runCli parte superior:L610-L667— normalización de argv, parseo container/profile, carga de .env.runMain help fast path:L760-L820— cinco categorías de fast path de help: root/browser/secrets/nodes/subcommand.gateway hot path + tryRouteCli:L905-L928— sitryRunGatewayRunFastPathacierta, retorna; si no,tryRouteCli.tryRunGatewayRunFastPath:L143-L238— construye un Commander mínimo con solo el comandogateway run.primary-only + registro de comandos de plugin:L1005-L1070—registerCoreCliByName+registerSubCliByName+ registro perezoso de comandos de plugin.coreEntrySpecs:L50-L143— registro de comandos núcleo (crestodian/setup/onboard/configure/config/backup/migrate/maintenance/message/mcp/transcripts/agent/agents/status-health-sessions).entrySpecs registro de subclis:L92-L278— registro de subclis (acp/gateway/daemon/logs/system/models/devices/node/sandbox/tui/cron/dns/docs/qa/proxy/hooks/webhooks/qr/clawbot/pairing/plugins/channels/directory/security/secrets/skills/update).gateway run fallback:L45-L66—shouldRegisterGatewayRunOnly+registerGatewayRunOnlytercera capa del hot path.tryRouteCli:L82-L115— ruta rápida route-first.createParsedRoute:L26-L59— compila entradas del catálogo en unRouteSpec.
Flujo de datos
La parte superior de runCli normaliza argv y carga el entorno (run-main.ts:L610):
export async function runCli(argv: string[] = process.argv) {
const originalArgv = normalizeWindowsArgv(argv);
const startupTrace = createGatewayStartupTrace(originalArgv, "cli.main");
const parsedContainer = parseCliContainerArgs(originalArgv);
if (!parsedContainer.ok) {
throw new Error(parsedContainer.error);
}
// ...
const containerTarget = maybeRunCliInContainer(originalArgv);
if (containerTarget.handled) {
if (containerTarget.exitCode !== 0) {
process.exitCode = containerTarget.exitCode;
}
return;
}Nótese maybeRunCliInContainer: si argv especifica --container <name>, aquí se sustituye este proceso por una ejecución dentro del contenedor y se retorna; la lógica de despacho real no continúa.
El hot path del gateway es el nodo clave del embudo de despacho (run-main.ts:L905):
if (!bootstrapProxyBeforeFastPath &&
(await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))) {
return;
}
// tras arrancar el proxy, se prueba el hot path de nuevo
if (bootstrapProxyBeforeFastPath &&
(await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))) {
return;
}
const { tryRouteCli } = await startupTrace.measure("route-import", () => import("./route.js"));
if (await startupTrace.measure("route", () => tryRouteCli(normalizedArgv))) {
return;
}tryRunGatewayRunFastPath se ejecuta antes de arrancar el proxy, porque gateway run gestiona su propio proxy (vía installGatewayRunRuntimeHooks). Si el hot path no acierta y hace falta proxy, se arranca primero y luego se vuelve a probar el hot path. Solo si tampoco acierta se va a tryRouteCli. tryRunGatewayRunFastPath usa internamente Promise.all para dynamic import en paralelo 8 módulos (run-main.ts:L159), y entonces construye un programa Commander mínimo con solo el comando gateway run. La secuencia de arranque completa de gateway run se ve en Núcleo del gateway.
La ruta rápida route-first (route.ts:L82): si OPENCLAW_DISABLE_ROUTE_FIRST no está definido y argv no contiene help/version, se usa resolveCliArgvInvocation para parsear el commandPath y se llama a findRoutedCommand (routes.ts:L8), que hace un escaneo lineal de la tabla routedCommands buscando el primer matches(path) con canRun(argv) que pase; si acierta, prepareRoutedCommand (carga plugins según la estrategia route.loadPlugins — text-only significa que solo las llamadas no --json hacen pre-carga) y luego route.run(argv), eludiendo Commander completo.
La ruta de Commander completo hace registro primary-only (run-main.ts:L1009):
const { primary } = invocation;
if (primary && shouldRegisterPrimaryCommandOnly(parseArgv)) {
await startupTrace.measure("register-primary", async () => {
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, parseArgv);
});
}El registro de subclis es declarativo (register.subclis-core.ts:L92): defineImportedProgramCommandGroupSpecs trata el caso simple de «módulo que exporta un nombre de función fijo» (declarativo puro: commandNames + loadModule: () => import(...) + exportName); los casos que requieren parámetros extra (por ejemplo plugins necesita colgar comandos de plugin antes/después) usan un objeto inline con envoltorio registerSubCliWithPluginCommands, donde pluginCliPosition: "before" | "after" controla si los comandos de plugin se cuelgan antes o después del subcli.
registerSubCliByName tiene además una especialidad para gateway (register.subclis-core.ts:L45) — incluso al caer al camino completo de Commander, openclaw gateway run sigue usando el registro minimalista de «colgar solo el subcomando run»; removeCommandByName quita el comando gateway que pudiera haberse registrado antes y lo vuelve a colgar. Es la tercera red de seguridad del hot path.
Límites y fallos
- Intercepción unowned primary:
openclaw <typo>tras el fast path de help y antes de route hace una comprobación de unowned primary (run-main.ts:L826) que lanzaUnknown commanden lugar de mostrar silenciosamente el help de nivel superior. Esto se diseñó para corregir #81077 — de lo contrarioopenclaw gatway --helpaparentaría éxito. - Diagnóstico de comando de plugin ausente: tras el registro primary-only, si el primary no es ni comando builtin ni está en la tabla de subclis (
run-main.ts:L1043), se llama aresolveMissingPluginCommandMessageFromPolicy, que sugiere «quizá quieras instalar el plugin X» en vez de un help vacío. - Orden de arranque del proxy:
tryRunGatewayRunFastPathse ejecuta antes de arrancar el proxy (run-main.ts:L905), porque gateway run gestiona su propio proxy. Otros comandos arrancan primero el proxy y luego prueban — si el orden se invierte, el proceso gateway podría ser sobornado por un proxy externo. - Salida del parseo de Commander: cuando
program.parseAsynclanza errorescommander.*(run-main.ts:L324),isCommanderParseExitlos reconoce y solo fijaprocess.exitCodesin re-lanzar — el diseñoexitOverridede Commander trata help/version como exits. - Limpieza de recursos: el bloque
finallyderunCli(run-main.ts:L1089) llama acloseCliMemoryManagers,disposeCliAgentHarnessesypauseNonTtyStdinForCliExit— antes de salir de un proceso CLI efímero hay que limpiar el runtime en memoria y los harnesses de agent, o los subprocesos quedarán colgados. - Tres redes de seguridad para gateway run:
shouldRegisterGatewayRunOnly(register.subclis-core.ts:L45) es la tercera capa del hot path — incluso si los fast path anteriores fallan y se llega al Commander completo,gateway runsolo cuelga el subcomando run sin cargar el árbol completo de comandos de gateway.
Resumen
El despacho CLI es un embudo de tres capas más una red de seguridad: fast path de help lee texto precompilado; route-first recorre una tabla de rutas pre-parseada; Commander completo hace registro primary-only. gateway run, por ser la ruta caliente, tiene optimización dedicada en tres puntos (tryRunGatewayRunFastPath, shouldRegisterGatewayRunOnly, registerGatewayRunOnly). Una vez que el comando realmente arranca, cómo se ejecuta el bucle principal del agent se ve en Bucle principal del agent; la secuencia de arranque del gateway se ve en Núcleo del gateway; cómo la capa de entrada pasa argv a runCli se ve en Entrada y arranque.
Referencias oficiales: Documentación de OpenClaw · README.