Entrée et démarrage
Responsabilités
Tout ce qui se passe entre le moment où l'utilisateur tape la commande openclaw et celui où la première vraie sous-commande commence à s'exécuter relève de l'« entrée (entry) »: vérification de la version Node, choix du chemin de compile cache, raccourcis (fast path) pour help/version, transfert de signaux, et enfin passage de contrôle à dist/entry.js. Ce dernier reprend la main pour normaliser l'environnement, parser les options profile/container, décider d'un respawn, puis appeler runMainOrRootHelp qui transmet argv au répartiteur CLI.
Si l'entrée est scindée en deux couches, c'est volontaire: openclaw.mjs est un ESM Node pur, sans aucune dépendance TypeScript, exécutable isolément, qui traite « ce qui doit être décidé avant que le compilé TS ne soit chargé »; src/entry.ts est l'entrée du compilé TS et suppose que dist/ existe déjà, ce qui autorise des dynamic imports et du tracing plus sophistiqués.
Motivation de conception
Pourquoi séparer l'entrée en deux couches et bourrer la première de raccourcis (fast path)? La cause racine est la latence de démarrage à froid. OpenClaw est un CLI; la tolérance de l'utilisateur pour openclaw --version est de l'ordre de quelques centaines de millisecondes, mais charger Commander + plugins + configuration complète prend plusieurs centaines de ms, voire plus d'une seconde. Si chaque --version devait parcourir toute la chaîne de démarrage, l'expérience serait exécrable.
La stratégie de la couche d'entrée est: ne charger que ce qui est strictement nécessaire. Le numéro de version est lu directement depuis package.json; le texte d'aide vient d'un cli-startup-metadata.json précompilé; ce n'est que lorsqu'une vraie commande doit s'exécuter que l'on dynamic import dist/entry.js. Résultat, openclaw --version s'affiche en quelques dizaines de ms.
La seconde couche entry.ts a aussi ses propres raccourcis: elle tente d'abord tryHandleRootHelpFastPath puis tryHandlePrecomputedCommandHelpFastPath, et ne dynamic import ./cli/run-main.js que si les deux échouent. Chaque maillon de la chaîne d'import est await, donc quand un fast path hit, le code suivant n'est jamais chargé.
L'autre motivation est l'isolation du compile cache. Sous Node 22+, module.enableCompileCache() accélère significativement le démarrage ESM, mais un checkout du code source et certaines versions de Node 24 sur Windows ont un bug de deadlock. La couche d'entrée contient une logique de respawn: détecter si le process courant est un checkout source (isSourceCheckoutLauncher) ou une version Node 24 concernée, et si oui respawn un sous-processus qui désactive le compile cache en positionnant NODE_DISABLE_COMPILE_CACHE=1 dans env. Ce check doit être fait avant l'entrée TS, sinon le cache est déjà pollué.
Fichiers clés
openclaw.mjs garde-fou de version:L11-L53—MIN_NODE_MAJOR=22/MIN_NODE_MINOR=19, en dessous duquel onprocess.exit(1).openclaw.mjs respawn compile cache:L207-L264—respawnWithoutCompileCacheIfNeeded+respawnWithPackagedCompileCacheIfNeededdécident de relancer un sous-processus.openclaw.mjs fast path help:L717-L763—tryOutputBareRootHelpettryOutputPrecomputedCommandHelpcrachent le texte d'aide précompilé.openclaw.mjs repli vers entry.js:L765-L780— fast path tous manqués ⇒ dynamic import./dist/entry.js.entry.ts garde-fou isMainModule:L56-L79— évite qu'un bundler réimportantentry.jscomme dépendance partagée ne lance deux fois la passerelle.entry.ts plan de respawn:L90-L103—buildCliRespawnPlan+runCliRespawnPlantraitent les scénarios nécessitant un changement de runtime.entry.ts fast path version + appel runMainOrRootHelp:L130-L131—tryHandleRootVersionFastPathmanqué ⇒ on envoie au répartiteur principal.entry.ts runMainOrRootHelp:L271-L295— fast path help + dynamic import run-main.entry.ts help commande précompilé:L208-L269—tryHandlePrecomputedCommandHelpFastPathgèrebrowser/secrets/nodes+ help sous-commandes.
Flux de données
Le garde-fou de version en haut de openclaw.mjs (openclaw.mjs:L38):
const ensureSupportedNodeVersion = () => {
if (isSupportedNodeVersion(parseNodeVersion(process.versions.node))) {
return;
}
process.stderr.write(
`openclaw: Node.js v${MIN_NODE_VERSION}+ is required (current: v${process.versions.node}).\n` +
"If you use nvm, run:\n" +
` nvm install ${MIN_NODE_MAJOR}\n` +
` nvm use ${MIN_NODE_MAJOR}\n` +
` nvm alias default ${MIN_NODE_MAJOR}\n`,
);
process.exit(1);
};MIN_NODE_MAJOR=22 et MIN_NODE_MINOR=19 sont des seuils durs; en dessous on sort. Après le garde-fou vient la décision de respawn du compile cache (openclaw.mjs:L207):
const respawnWithoutCompileCacheIfNeeded = () => {
const needsDisabledCompileCacheRespawn =
isSourceCheckoutLauncher() || shouldSkipCompileCacheForWindowsNode24();
if (!needsDisabledCompileCacheRespawn) {
return false;
}
if (process.env[COMPILE_CACHE_DISABLED_RESPAWNED_ENV] === "1") {
return false;
}
// ...
return runRespawnedChild(
process.execPath,
[...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
env,
);
};Notez la garde d'auto-bouclage COMPILE_CACHE_DISABLED_RESPAWNED_ENV: le sous-processus respawné repasse cette logique, mais comme l'env vaut déjà "1", on respawn au plus une fois, jamais de récursion infinie.
Repli de la première couche vers dist/entry.js (openclaw.mjs:L765): si tous les fast paths échouent, on tryImport("./dist/entry.js"), puis à défaut .mjs, puis seulement si les deux échouent on appelle buildMissingEntryErrorMessage pour afficher « arborescence source non construite, lancez pnpm install && pnpm build ». tryImport n'avale que les erreurs directes de type module not found; les autres sont propagées — ainsi si dist/entry.js existe mais qu'un import interne échoue, l'erreur n'est pas masquée en feintant un « missing dist ».
Le cœur de la répartition de la seconde couche entry.ts (entry.ts:L130):
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}runMainOrRootHelp tente elle-même d'abord les fast paths help (entry.ts:L271):
async function runMainOrRootHelp(argv: string[]): Promise<void> {
if (await tryHandleRootHelpFastPath(argv)) {
return;
}
if (await tryHandlePrecomputedCommandHelpFastPath(argv)) {
return;
}
try {
const { runCli } = await gatewayEntryStartupTrace.measure(
"run-main-import",
() => import("./cli/run-main.js"),
);
await runCli(argv);
} catch (error) {
const { formatCliFailureLines } = await import("./cli/failure-output.js");
for (const line of formatCliFailureLines({
title: "Could not start the CLI.",
error,
argv,
})) {
console.error(line);
}
process.exit(1);
}
}Notez gatewayEntryStartupTrace.measure("run-main-import", ...) — ce dynamic import de run-main.js est chronométré explicitement car c'est l'étape la plus lourde du démarrage à froid (plusieurs centaines de ms). Quand runCli jette, on ne fait pas juste console.error(error.stack); on passe par formatCliFailureLines pour afficher un titre user-friendly + trace, puis process.exit(1).
En haut de entry.ts il y a aussi un garde-fou isMainModule crucial (entry.ts:L56):
if (
!isMainModule({
currentFile: fileURLToPath(import.meta.url),
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
})
) {
// Imported as a dependency — skip all entry-point side effects.
} else {
// ... logique complète de démarrage
}Le commentaire l'explique: un bundler peut traiter entry.js comme dépendance partagée importée par dist/index.js; sans ce garde-fou on lancerait deux passerelles, la seconde échouant à prendre le port et faisant crasher le tout.
Limites et modes d'échec
- Transfert de signaux lors du respawn:
runRespawnedChild(openclaw.mjs:L102) attache des listenersSIGTERM/SIGINT/SIGHUP/SIGQUITau sous-processus, qui forward le signal reçu puis, en filet de sécurité 1 s plus tard,process.exit. Cela empêche un sous-processus qui ignore le signal de bloquer le launcher indéfiniment. Les signaux Windows sont différents (SIGTERM/SIGINT/SIGBREAK). - Pollution du chemin compile cache: si la variable d'env
NODE_COMPILE_CACHEpointe vers un répertoire non inscriptible,module.enableCompileCacheéchoue silencieusement (openclaw.mjs:L267), letry/catchavale l'erreur — c'est intentionnel: un échec de cache ne doit pas bloquer le démarrage. - Détection de checkout source:
isSourceCheckoutLauncher()vérifie la présence de.gitousrc/entry.ts; dans une arborescence source, le compile cache est forcément désactivé (le code source peut changer à tout moment, les compilations cachées deviendraient périmées). - Message d'erreur missing dist:
buildMissingEntryErrorMessage(openclaw.mjs:L354) détecte sisrc/entry.tsexiste; si oui, il suggère « arborescence source non construite, lancezpnpm install && pnpm build» au lieu d'un simple « file not found » — cette distinction épargne des minutes de débogage à l'utilisateur. - Double démarrage via isMainModule: le commentaire du garde-fou en haut de
entry.ts(entry.ts:L56) explique que sans lui, un bundler traiterait entry.js comme dépendance partagée,runCliserait appelé deux fois, et la seconde instance échouerait à prendre le lock/port de la passerelle. - Protection contre la boucle de respawn:
COMPILE_CACHE_DISABLED_RESPAWNED_ENVetOPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNEDgardent les deux types de respawn, garantissant au plus un respawn.
Résumé
La couche d'entrée traite « ce qui peut être décidé avant le chargement TS »: garde-fou de version, décision sur le compile cache, fast path help/version, transfert de signaux. openclaw.mjs est un script Node pur, sans aucune dépendance au compilé; entry.ts est l'entrée du compilé TS qui ajoute plan de respawn, parsing profile, repli fast path help, puis only then transmet argv à runCli qui entre vraiment dans la répartition CLI. La page suivante explique comment la chaîne de répartition route argv vers le bon subcli, gère le hot path de la passerelle et fait l'enregistrement différé (lazy-register) des commandes; la séquence de démarrage de la passerelle elle-même se trouve dans Cœur de passerelle.
Pour comparer avec la documentation officielle: OpenClaw docs · README.