Skip to content

Répartition des commandes CLI

源码版本v2026.6.11

Responsabilités

runCli(argv) est le point où OpenClaw commence réellement à répartir les commandes. Recevant argv depuis l'entrée, il doit: parser les options profile/container, charger le .env, décider de lancer un proxy, faire un repli sur le fast path help, tenter la route rapide (route-first), puis seulement tomber sur le programme Commander complet — enregistrement dynamique des commandes primaires (primary command), enregistrement des commandes de plugin, et program.parseAsync(argv) pour que Commander prenne le relais.

La répartition (dispatch) dans OpenClaw n'est pas un acte unique, mais un entonnoir à trois étages:

  1. Fast path: --help/--version/browser --help/secrets --help/nodes --help et autres requêtes de pur help lisent un texte précompilé sans charger Commander.
  2. Route-first: tryRouteCli route argv directement vers l'implémentation via une table routedCommands pré-parsée, court-circuitant l'enregistrement Commander complet.
  3. Commander complet: buildProgram() construit le programme racine Commander, enregistre en lazy les primary command et les commandes de plugin, puis program.parseAsync.

Chaque étage return si hit, sinon on descend. Ainsi openclaw gateway run (tryRunGatewayRunFastPath:L143) saute l'essentiel des enregistrements, tandis que openclaw plugins install foo (chemin froid) tombe dans Commander complet.

Motivation de conception

Pourquoi un entonnoir à trois étages plutôt qu'un simple program.parseAsync(argv)? Parce qu'avant parseAsync, il faut buildProgram, qui appelle registerProgramCommands et enregistre toutes les commandes cœur et subclis. OpenClaw a 40+ subclis (gateway/agent/cron/models/devices/plugins/channels/security/secrets/skills/...), chacun avec son sous-arbre de commandes; tout enregistrer eagerly coûterait plusieurs centaines de ms de démarrage à froid.

Plusieurs choix de conception sont clés:

  • Enregistrement primary-only: le premier token non-option dans argv est la commande primaire (gateway dans openclaw gateway run). Si shouldRegisterPrimaryCommandOnly(argv) est vrai, on n'enregistre que celle-là, tous les autres subclis sont ignorés.
  • Lazy import: chaque registreur de subcli est () => import("../xxx-cli.js"), chargé dynamiquement uniquement si hit.
  • Hot path de la passerelle: openclaw gateway run est la commande la plus fréquente; tryRunGatewayRunFastPath dédié contourne buildProgram et construit un programme Commander minimaliste.
  • Route-first: certaines commandes n'ont même pas besoin du parseur Commander; elles sont dispatchées directement depuis une table argv pré-parsée.

Le coût: plus de chemins de code, plus de complexité à comprendre. Le gain: le démarrage à froid de openclaw gateway run passe de la seconde à quelques centaines de ms.

Fichiers clés

Flux de données

En haut de runCli on normalise argv et on charge l'environnement (run-main.ts:L610):

typescript
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;
  }

Notez maybeRunCliInContainer — si argv spécifie --container <name>, ce code remplace ce processus par une exécution dans le conteneur et return; la logique de répartition réelle n'est jamais atteinte.

Le hot path passerelle est le nœud clé de l'entonnoir (run-main.ts:L905):

typescript
if (!bootstrapProxyBeforeFastPath &&
    (await tryRunGatewayRunFastPath(normalizedArgv, startupTrace))) {
  return;
}
// bootstrap proxy puis on retente le hot path
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 est joué avant le bootstrap du proxy, car gateway run gère lui-même le proxy (via installGatewayRunRuntimeHooks). Si le hot path miss et que le proxy est requis, on le boot puis on rejoue le hot path. Si toujours miss, on tente tryRouteCli. tryRunGatewayRunFastPath fait en interne un Promise.all de 8 dynamic imports (run-main.ts:L159), puis construit un programme Commander minimaliste ne contenant que la commande gateway run. La séquence de démarrage complète de gateway run se trouve dans Cœur de passerelle.

Le fast path route-first (route.ts:L82), si OPENCLAW_DISABLE_ROUTE_FIRST n'est pas posé et qu'argv ne contient pas help/version, utilise resolveCliArgvInvocation pour extraire commandPath, appelle findRoutedCommand (routes.ts:L8) qui scanne linéairement routedCommands pour le premier matches(path) avec canRun(argv) OK, puis prepareRoutedCommand (qui charge les plugins selon la stratégie route.loadPluginstext-only signifie que seuls les appels non --json pré-chargent) et enfin route.run(argv), en court-circuitant Commander complet.

Le chemin Commander complet fait l'enregistrement primary-only (run-main.ts:L1009):

typescript
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);
  });
}

Le registre des subclis est déclaratif (register.subclis-core.ts:L92): defineImportedProgramCommandGroupSpecs traite le cas simple « module qui exporte un nom de fonction fixe » (purement déclaratif — commandNames + loadModule: () => import(...) + exportName); les cas nécessitant des paramètres supplémentaires (par exemple plugins qui attache des commandes de plugin avant/après) utilisent un objet inline qui enveloppe via registerSubCliWithPluginCommands, contrôlé par pluginCliPosition: "before" | "after".

registerSubCliByName a aussi un cas spécial gateway (register.subclis-core.ts:L45) — même en repli sur Commander complet, openclaw gateway run passe par un enregistrement minimaliste « monter uniquement run », via removeCommandByName qui démonte un éventuel gateway précédent avant de le remonter. C'est le troisième étage de repli du hot path.

Limites et modes d'échec

  • Interception unowned primary: openclaw <typo> passe un check unowned primary après le fast path help et avant la route (run-main.ts:L826), lève Unknown command plutôt que d'afficher silencieusement l'aide racine. C'est le fix de #81077 — sinon openclaw gatway --help feindrait le succès.
  • Diagnostic commande de plugin manquante: après l'enregistrement primary-only, si primary n'est ni commande builtin ni dans la table subclis (run-main.ts:L1043), on appelle resolveMissingPluginCommandMessageFromPolicy pour suggérer « peut-être installer le plugin X » plutôt qu'un help vide.
  • Ordre de bootstrap du proxy: tryRunGatewayRunFastPath est joué avant le bootstrap du proxy (run-main.ts:L905), car gateway run gère lui-même le proxy. Les autres commandes boot le proxy d'abord — un ordre inverse laisserait le processus gateway se faire voler le proxy par une autre commande.
  • Sortie du parseur Commander: quand program.parseAsync jette une erreur commander.* (run-main.ts:L324), isCommanderParseExit la reconnaît et se contente de poser process.exitCode sans rethrow — le design exitOverride de Commander traite help/version comme des exits.
  • Nettoyage de ressources: le bloc finally de runCli (run-main.ts:L1089) appelle closeCliMemoryManagers, disposeCliAgentHarnesses, pauseNonTtyStdinForCliExit — un processus CLI court doit nettoyer son runtime mémoire et ses agent harness avant de sortir, sinon les sous-processus fuient.
  • Trois étages de repli pour gateway run: shouldRegisterGatewayRunOnly (register.subclis-core.ts:L45) est le troisième étage du hot path — même si les fast paths précédents miss et que l'on tombe dans Commander complet, gateway run ne monte toujours que la sous-commande run, sans charger tout l'arbre gateway.

Résumé

La répartition CLI est un entonnoir à trois étages avec un repli: fast path help lit du texte précompilé, route-first passe par une table de routes pré-parsée, Commander complet fait l'enregistrement primary-only. gateway run étant un hot path, il a des optimisations dédiées à trois endroits (tryRunGatewayRunFastPath, shouldRegisterGatewayRunOnly, registerGatewayRunOnly). Une fois la commande en exécution, voir Boucle principale de l'agent pour la boucle; Cœur de passerelle pour la séquence de démarrage de la passerelle; Entrée et démarrage pour la manière dont l'entrée transmet argv à runCli.

Pour comparer avec la documentation officielle: OpenClaw docs · README.