CLI — piloter le framework en ligne de commande
stablenodefony (cœur)mis à jour 2026-07-20
Une application Nodefony se pilote par une seule porte : le binaire
nodefony. Il démarre le serveur en développement, le construit, le lance en production, ouvre un cluster, échafaude un projet ou un module, et accueille les commandes que tes propres modules ajoutent. Sous le capot, chaque commande est une classe qui s’accroche au cycle de vie du kernel et décide jusqu’où le booter — de « rien du tout » (un simplestatus) à « serveurs prêts » (development).
📍 Documentation › Cœur — @nodefony/core › CLI
🧠 Le modèle mental — un binaire, un kernel, des commandes#
Trois idées suffisent à comprendre toute la CLI.
Le binaire délègue à l’application. Installé globalement, nodefony sert surtout à créer un
projet hors de tout dépôt. Mais dans une application, c’est la version des node_modules du projet
qui fait autorité — elle seule connaît ses modules et ses commandes. Le lanceur remonte donc au projet
(findProjectRoot) et, s’il y trouve un autre paquet nodefony que lui-même, lui passe la main dans
le même processus (bin/nodefony.ts). C’est le pattern du wrapper de projet (gradlew, mvnw) :
le projet gagne.
Où tu tapes nodefony |
CLI qui s’exécute |
|---|---|
hors de tout projet (create app) |
celui que tu as lancé — le global |
| dans un projet aux dépendances installées | celui du projet (node_modules/nodefony) — délégation |
dans un projet sans node_modules |
le global, faute d’autre |
Pour voir la décision, pose NF_CLI_DEBUG=1 : le lanceur écrit [nodefony] cli → <chemin> sur
la sortie d’erreur. Quand global et projet n’ont pas la même version, une ligne le dit d’office
(versionMismatchNotice, resolveLocalCli.ts#versionMismatchNotice) — jamais sous --json. Et create app, qui
tourne par le global, demande au registre si une version plus récente du même canal est publiée
(startFreshnessCheck, cliFreshness.ts#startFreshnessCheck) : il l’annonce avec le geste npm i -g nodefony@<tag>,
se tait hors ligne, en CI ou sous NF_NO_UPDATE_CHECK=1.
Une commande choisit son point d’arrêt. Chaque commande déclare un kernelEvent : la phase du
boot à laquelle elle s’exécute et où le démarrage s’arrête. Une commande qui n’a besoin de rien
(status, stop, create) ne boote aucun kernel — c’est un fast-path standalone. Une commande
qui introspecte la config booste jusqu’à onReady. Une commande serveur (development, production)
va jusqu’à onPostReady, où les serveurs écoutent, puis reste en vie.
Deux familles de commandes. Les intégrées (development, build, create…) sont posées par
le cœur au démarrage (CliKernel.registerBuiltinCommands(), CliKernel.ts#CliKernel.registerBuiltinCommands). Les commandes de
module (http:network, security:user:add…) sont ajoutées par chaque module dans son constructeur —
elles suivent le namespace <module>:<action> et empruntent exactement le même chemin.
🚀 Démarrage rapide#
1. Piloter une application#
Toutes les commandes s’exécutent depuis la racine du projet (celle qui porte nodefony.config.ts) :
npx nodefony development # serveur de dev : Vite/HMR + redémarrage auto (alias : dev)
npx nodefony debug # idem, débogueur ouvert dans le serveur (127.0.0.1:9229)
npx nodefony build # construit tous les paquets (alias : compile)
npx nodefony production -w 4 # runtime prod, 4 workers, au premier plan (cloud-native)
npx nodefony status # les process en cours (dev/prod/cluster), sans rien démarrer
npx nodefony stop # arrêt propre de tout runtime lancé depuis ce projet
npx nodefony --help # la liste complète, commandes de module incluses
Un serveur qui écoute n’est pas un serveur qui sert. status porte une ligne
disponibilité distincte des ports : un composant peut retenir la mise en service — un schéma de
base en retard, un cache froid — pendant que les ports répondent parfaitement. Il NOMME alors ce
qui retient, et pourquoi :
disponibilité : ⚠ RETENUE — drizzle:default (2 migrations en attente) ; /readyz rend 503
Le détail vient du serveur lui-même, par un fichier local qu’il tient à jour — jamais de la sonde
publique, qui ne donne les noms qu’à un appelant authentifié. Quand ce détail manque ou ne
concorde pas avec ce que le runtime vient de répondre, status affiche le compte sans nommer :
un nom faux ferait chercher une cause déjà levée.
Le port servi n’est pas toujours le port demandé — et status le dit. En développement, un
port déjà pris fait glisser l’écoute sur le suivant (servers.portPolicy: "auto"). Le démarrage
annonce alors l’adresse RÉELLE, et status porte l’écart tant qu’il dure :
✓ serveur prêt en 3456ms — http://127.0.0.1:5153 · https://127.0.0.1:5154
⚠ PORTS DÉCALÉS — 5151, 5152 déjà pris par un autre processus → cette app sert
http://127.0.0.1:5153 · https://127.0.0.1:5154. Viser CETTE adresse (nodefony status)
Sans cette annonce, on interroge le port de sa configuration, l’application VOISINE répond — même sonde de vie, même racine, et un refus d’authentification pour un compte qu’elle ne connaît pas — et l’on cherche une panne chez soi pendant qu’on parle à quelqu’un d’autre.
Le verdict sort aussi par le code de retour. status rend 0 quand un processus de CE projet
tourne, et 69 (EX_UNAVAILABLE) quand il n’y en a aucun — même si un port de la configuration
répond, puisque ce serait celui d’une autre application. C’est ce qui rend un démarrage en
arrière-plan constatable sans lire son journal :
npx nodefony status || npx nodefony dev # démarrer seulement si rien ne tourne ici
Plusieurs applications sur le même poste. status et stop sont scopés au projet du
répertoire courant : ils ne comptent ni n’arrêtent jamais le runtime du voisin. Quand un autre
projet tourne, status le NOMME dans une table (nom du package.json, ports tenus, racine),
et ce nom est exactement ce que stop accepte — pas besoin de cd :
npx nodefony stop mon-autre-app # par nom déclaré, ou par nom de dossier
npx nodefony stop /chemin/complet # par chemin, quand deux projets sont homonymes
npx nodefony stop --all # tous les runtimes Nodefony du poste (explicite)
Une cible qui ne désigne pas exactement UN projet est refusée (code de sortie 1), avec la
liste pour corriger : arrêter un serveur est irréversible, et deviner « le plus proche » ferait
tomber le mauvais.
2. Ajouter sa propre commande#
Une commande maison vit dans un module : on l’ajoute dans son constructeur avec addCommand. Le nom
suit la convention <module>:<action>, et kernelEvent dit à quelle phase elle s’exécute.
import { Module, Command } from "nodefony";
import type { Kernel, CliKernel } from "nodefony";
class GreetCommand extends Command {
constructor(cli: CliKernel) {
// Le kernel sera prêt (onReady) avant que generate() ne tourne.
super("app:greet", "Saluer depuis la ligne de commande", cli, {
kernelEvent: "onReady",
// Écrits sous toute erreur d'usage et sous --help : la bonne forme, prête à copier.
examples: ["nodefony app:greet Ada"],
});
this.addArgument("<name>", "qui saluer");
}
override async generate(name: string): Promise<this> {
// `log` est hérité de Service : ni import, ni logger à injecter.
this.log(`bonjour ${name}`, "INFO");
return this;
}
}
class GreetModule extends Module {
constructor(kernel: Kernel) {
super("greet", kernel, import.meta.url, {});
// Posé au constructeur du module (exécuté quand le manifeste le charge).
this.addCommand(GreetCommand);
}
}
export default GreetModule;
Une fois le module déclaré dans le manifeste modules, la commande apparaît dans nodefony --help et
s’invoque npx nodefony app:greet Ada.
📖 Lexique#
| Terme | En clair |
|---|---|
Cli |
La façade au-dessus de Commander : enregistre les commandes, parse argv, porte les helpers d’affichage. Utilisable sans kernel (standalone). |
CliKernel |
Un Cli relié au kernel : c’est lui qui boote l’application pour les commandes qui en ont besoin, et qui classe intégrées vs modules. |
Command |
La classe de base d’une commande. On surcharge generate() (l’action) et, au besoin, les hooks de cycle de vie. |
kernelEvent |
La phase du boot où la commande s’exécute — et le point où le démarrage s’arrête (onStart → onRegister → onBoot → onReady → onPostReady). |
| standalone (0 boot) | Une commande qui ne démarre aucun kernel : status, stop, create, completion, --version. Réponse immédiate. |
| runtime / long-running | Une commande serveur (development, production, cluster) qui, une fois la phase atteinte, reste en vie au lieu de rendre la main. |
| scaffold | La génération de code à partir de gabarits (create app/module/entity…) — un moteur pur alimenté par une spec déclarative. |
| dispatch différé | Les commandes de module ne sont connues qu’après leur enregistrement ; le kernel diffère le parse jusque-là, puis exécute la bonne. |
🗂️ Les commandes intégrées#
Vingt-sept commandes posées par le cœur (CliKernel.registerBuiltinCommands(), CliKernel.ts#CliKernel.registerBuiltinCommands),
plus l’alias start. La colonne arrêt indique jusqu’où le boot va — 0 boot = fast-path standalone.
| Commande | Alias | Ce qu’elle fait | Arrêt | Classe |
|---|---|---|---|---|
development |
dev |
Serveur de dev : Vite/HMR + redémarrage auto (--detach/--wait/--health/--log) |
onPostReady |
DevCommand.ts#Dev |
debug |
— | development + débogueur ouvert DANS le serveur, rouvert à chaque rechargement (--inspect=<port>, --inspect-brk) |
onPostReady |
DebugCommand.ts#Debug |
production |
prod |
Runtime prod au premier plan ; topologie via -w, --workers |
onPostReady |
ProdCommand.ts#Prod |
cluster |
— | Cluster de N workers (cgroup-aware, respawn) — -w, --workers |
onPostReady |
ClusterCommand.ts#Cluster |
inspect |
— | L’état RÉEL de l’app : routes · modules · services · config · stores · entities · graph, --json — sans ouvrir de port |
onPostReady |
InspectCommand.ts#Inspect.constructor |
build |
compile |
Construit tous les paquets (délègue à turbo run build) — -f/--force |
onRegister |
BuildCommand.ts#Build |
install |
— | install sur tous les modules — -f/--force |
onRegister |
InstallCommand.ts#Install |
outdated |
— | outdated sur tous les modules |
onRegister |
OutdatedCommand.ts |
upgrade |
— | Après une montée de version : remet le bloc framework d’AGENTS.md, les pointeurs de skills et les copies livrées au niveau des paquets — --check (sort en 1 si en retard), --dry-run, --json (0 boot) |
0 boot |
UpgradeCommand.ts#Upgrade |
start |
— | Alias de prod (il n’y a plus de commande propre) |
onStart |
ProdCommand.ts#Prod.constructor |
doctor |
— | Diagnostic STATIQUE : paquets importés non déclarés, câblage (entité / controller / service jamais enregistrés, nom réservé, brique manquante), segment :id qui répondra 404 — --json, --cwd ; remonte à la racine de l’app, donc lançable depuis n’importe quel sous-dossier (0 boot) |
0 boot |
DoctorCommand.ts#Check |
env |
— | Le .env lu, valeurs effectives et provenance de chacune (0 boot) |
0 boot |
EnvCommand.ts#Env |
status |
— | Introspecte les process dev/prod/cluster (0 boot) | 0 boot |
StatusCommand.ts#Status |
stop |
— | Arrête proprement les runtimes du projet (0 boot) — --all |
0 boot |
StopCommand.ts#Stop |
completion |
— | Script de complétion shell (0 boot) | 0 boot |
CompletionCommand.ts#Completion |
create |
— | Échafaude projet / module / entité (0 boot) | 0 boot |
CreateCommand.ts#options |
card |
devkit:card |
Carte de visite de l’app : identité, modules installés, où aller, quoi lancer — --json, --cwd (0 boot) |
0 boot |
CardCommand.ts |
symbols |
— | Signature et TSDoc d’un symbole du framework, depuis le graphe publié — --module, --json (0 boot) |
0 boot |
SymbolsCommand.ts |
man |
— | Ouvre la page de manuel du paquet du projet (man nodefony ne la trouve qu’en installation globale) — --path (0 boot) |
0 boot |
ManCommand.ts#Man |
ai:sync |
— | Pose dans .agents/skills/ les pointeurs vers les skills livrés par les paquets installés — --dry-run, --json, --cwd (0 boot) |
0 boot |
cli/aiSync.ts |
scaffold:sync |
— | Pose les fichiers que livrent les paquets installés (nodefony.contribute) ; --update rafraîchit les copies restées intactes — --dry-run, --json, --cwd (0 boot) |
0 boot |
cli/contributions.ts |
ai:mcp |
— | Déclare le serveur MCP de l’app à ton agent (0 boot) | 0 boot |
AiMcpCommand.ts |
see |
— | Ouvre une page dans un navigateur piloté et la mesure (0 boot) | 0 boot |
SeeCommand.ts |
image:check |
— | Refuse une image de conteneur qui embarque un secret (0 boot) | 0 boot |
ImageCommand.ts |
git:hooks |
— | Pose les hooks git : contrôles au commit et au push (0 boot) | 0 boot |
GitHooksCommand.ts |
licenses |
— | Les licences des dépendances redistribuées | onRegister |
LicensesCommand.ts |
scripts |
— | Ce que font les scripts npm du projet (descriptions déclarées dans nodefony.scripts) |
onRegister |
ScriptsCommand.ts |
menu |
— | Menu interactif : les commandes utiles ici | onStart |
MenuCommand.ts |
status et stop sont détournées vers leur exécution réelle avant tout boot
(CliKernel.ts#CliKernel.start, via isStandaloneDevCommand) — leur generate() n’est qu’un filet.
doctor et env prennent le même raccourci, et pour une raison qui se retient
(CliKernel.ts#cliOptions et CliKernel.ts#CliKernel.start) : on lance ces deux commandes précisément quand l’application ne
démarre plus. Les faire booter les rendrait muettes au seul moment où elles servent — et le
rapport se noierait sous le journal du Kernel. doctor prend donc ce fast-path : sans lui, il
partirait en dispatch différé, c’est-à-dire en boot.
Deux verbes à retenir, et leur frontière :
doctorest statique — il ne lit que des fichiers, donc il fonctionne sur une application cassée.inspectest runtime — il boote sans serveur et rend ce que l’application est VRAIMENT, pas ce que son code laisse croire.
ai:sync pose des POINTEURS, pas des copies. Les skills d’agent vivent dans les paquets
(node_modules/@nodefony/*/skills/, et les modules locaux de l’app) ; la commande écrit dans
.agents/skills/ un fichier court qui les DÉSIGNE. Le contenu suit donc npm update sans qu’aucun
fichier du projet ne soit réécrit — une copie, elle, mentirait six mois plus tard sans casser le
build. Le dossier visé est celui que tous les clients conformes lisent, pas celui d’un seul.
nodefony create app fait ce geste à la création, juste avant le premier commit : les pointeurs
sont faits pour être versionnés, l’équipe et l’intégration continue disposent des mêmes skills.
Aucun postinstall ne l’appelle — --ignore-scripts est courant, les scripts d’installation sont
un vecteur d’attaque connu, et écrire dans un dossier versionné à chaque installation produirait
des différences surprises. Un pointeur identique n’est jamais réécrit (l’horodatage ne bouge pas) ;
un pointeur que plus aucun paquet ne livre est nommé, jamais supprimé — quelqu’un a pu en
écrire un à la main sous le même nom.
scaffold:sync pose des COPIES, et c’est voulu. Certains fichiers doivent exister EN VRAI dans
l’application : un thème que Keycloak monte depuis le compose (un lien vers node_modules se monte
mal sous Windows, et yarn moderne n’a pas de node_modules), un realm d’import que l’application
fera évoluer. Un paquet le déclare dans son package.json :
{ "nodefony": { "contribute": "./dist/nodefony/scaffold/contribute.js" } }
Ce module exporte contribute(context) (type TAppContributor). Le contexte
(IAppContributionContext) distingue deux sortes de fichiers :
| Geste | Nature | Fichier présent |
|---|---|---|
write(chemin, contenu) |
semence : générée POUR l’application (un realm), qui la fait ensuite évoluer | jamais remplacé |
copyTree(dossier, chemin), write(…, { tracked: true }) |
copie suivie : ce que le paquet livre tel quel (un thème) | rafraîchi par --update s’il est intact |
Une copie suivie est inscrite avec son empreinte sha256 dans scaffold.lock.json, à la racine
de l’application et versionné avec elle. C’est la preuve qui manquait pour suivre les versions du
paquet sans écraser une retouche :
- empreinte du fichier = celle de la livraison précédente → intact : sans
--update, il est signalé « en retard » ; avec, il est remplacé par la version du paquet ; - empreinte différente → retouché, ou posé avant le registre : gardé et signalé, jamais écrasé (pour reprendre la version du paquet, supprimer le fichier puis relancer) ;
- fichier identique à la livraison → inscrit, même s’il précède le registre.
Les fins de ligne d’un texte sont ramenées à \n avant l’empreinte : un dépôt extrait sous Windows
avec core.autocrlf ne fait pas passer chaque fichier intact pour retouché. Le registre n’est
réécrit que s’il change, et une contribution en échec garde son entrée.
Le cœur ne connaît aucun contributeur : c’est ce qui permet à @nodefony/security de livrer son
décor Keycloak sans que le cœur en sache rien. create app joue les contributions après
l’installation, avant le premier commit (le registre entre donc dans ce commit) ; la commande les
rejoue après un --no-install, et --update après un npm update. Elle sort en 1 si une
contribution échoue. Le même geste existe dans le plan d’administration, en développement
seulement : GET /nodefony/kernel/api/scaffold (le plan de --update, sans écrire) et
POST /nodefony/kernel/api/scaffold/sync ({ "update": true }), superviseur suspendu le temps
de l’écriture.
Les commandes de module#
Chaque module ajoute ses commandes dans son constructeur (Module.addCommand(), Module.ts#Module.addCommand),
sous le namespace <module>:<action>. Elles apparaissent dans --help comme les intégrées :
| Commande | Ce qu’elle fait | Classe |
|---|---|---|
http:network |
Inventaire des interfaces réseau (-j pour du JSON) |
networkCommand.ts#Network |
http:certificates |
Génère les certificats TLS de dev (-f/--force) |
certificatesCommand.ts#Certificates |
proxy:generate |
Émet une config nginx/haproxy pour l’app | proxyGenerateCommand.ts#ProxyGenerate |
frontend:build |
Build de production des bundles Vite (-f/--force) |
frontend-build.ts#FrontendBuild |
security:secrets |
Vérifie / écrit les secrets de sécurité (-w/--write) |
security-secrets.ts#SecuritySecrets.constructor |
security:user:add |
Crée un utilisateur (-p, -r roles, -a admin) |
security-user-add.ts#SecurityUserAdd |
Une commande introuvable rend le code
EX_USAGE(64) — jamais un repli silencieux sur le serveur (CliKernel.ts#CliKernel.dispatchModuleCommand).
🏗️ Échafauder — create#
create prend un type en argument — sept (app | module | controller | service | front | entity | command, CREATE_TYPES, create.ts#CREATE_TYPES) — et route vers un moteur de scaffold unique
(runCreateCommand(), create.ts#runCreateCommand) :
nodefony create app mon-app --preset complete --frontend react # nouveau projet
nodefony create module blog --controller rest # module dans une app
nodefony create entity Article title:string body:text # entité + service + controller + tests
nodefony create service Billing # service @injectable + son interface
nodefony create command import --phase onReady # commande CLI <module>:import
serviceetcommandexistent parce qu’une mesure les a réclamés, pas par symétrie. Sans générateur de service, un agent produit une classe à méthodesstatic: elle compile, elle marche, et elle reste invisible au conteneur. Sans générateur de commande, il n’a aucun modèle et invente. Un type de scaffold manquant ne se voit pas — il se paie en code inventé.
Le moteur est pur et piloté par une spec déclarative 100 % JSON (getScaffoldSpec(), spec.ts#getScaffoldSpec),
partagée par trois fronts : le CLI rapide (flags), le CLI interactif (readline), et un futur formulaire
Studio. Ajouter une question = une entrée dans la spec, aucun front à toucher.
Le détail de chaque scaffold (options, gabarits, presets) vit dans les skills dédiés : create app
(preset complete/minimal, front React/Vue/Angular/Svelte), create module, create entity. En résumé —
create app et create entity exécutent ce qu’ils génèrent au boot suivant (la table naît en
CREATE TABLE IF NOT EXISTS). Ajouter ensuite un champ qui accepte le vide le pose au boot
suivant ; un champ obligatoire ne l’est jamais — il faudrait inventer la valeur des lignes déjà
présentes. La production, elle, se migre : nodefony orm:migrate.
Trois comportements de create entity qui surprennent si on ne les connaît pas :
- Une colonne de relation (
ref:) naît indexée. C’est par elle que passent le chargement d’association (?include=) et les jointures que vous écrirez ensuite. En revanche la clé étrangère n’est pas créée : une jointure n’en a jamais eu besoin (elle compare deux colonnes), et une contrainte se déclare dans leCREATE TABLE, donc n’atteindrait jamais une base déjà créée. L’intégrité référentielle relève des migrations. - Les noms d’entités du framework sont refusés (
User,session,access_token,audit_event…). Le registre est plat : une entité homonyme prendrait la place de celle d’un module, et l’application ne démarrerait plus — avec un message parlant d’une colonne inconnue, jamais du doublon. Le refus arrive avant la moindre écriture et propose les voies possibles. drizzle-ormest ajouté aupackage.jsons’il manque : le code produit l’importe directement, ce n’est donc pas une dépendance du seul module ORM. Unnpm installest requis après coup, et la commande le dit.
🧩 Ajouter sa propre commande#
Le squelette est en Démarrage rapide ; voici les leviers.
generate() est l’action. On la surcharge (command/Command.ts#Command.generate) ; elle reçoit les arguments
positionnels déclarés par addArgument(), et l’instance Commander en dernier paramètre. Les hooks de
cycle de vie (onKernelStart(), onKernelReady()…) sont câblés à la demande par setEvents()
(command/Command.ts#Command.setEvents), idempotent.
kernelEvent = jusqu’où booter. C’est le choix structurant :
onPostReady— les serveurs écoutent. Pour les runtimes (dev/prod/cluster).onReady— tout est booté sauf les serveurs. Pour introspecter la config/les services sans écouter.onBoot— les services du kernel existent (ex. certificats).onRegister(défaut) — les modules sont enregistrés (ex. build, install).onStart— rien n’est chargé : pour le vrai standalone.
examples = la bonne forme, montrée au bon moment. Des invocations complètes, telles qu’on les
tape (command/Command.ts#OptionsCommandInterface). Sous une erreur d’usage — option inconnue,
argument manquant —, la CLI écrit l’usage, les options et les deux premiers exemples, puis sort en
64 ; --help les montre tous. Une liste calculée (lue dans une page, un catalogue) se passe en
fonction : le constructeur de chaque commande tourne à chaque lancement de la CLI, alors que les
exemples ne servent que sous un refus ou un --help.
Enregistrer. Un module appelle this.addCommand(Ctor) dans son constructeur (Module.ts#Module.addCommand) —
il exige que kernel.cli existe, sinon il lève Kernel not ready (Module.ts#Module.loadService). Hors module, un
outil autonome construit un Cli et appelle cli.addCommand(Ctor) (Cli.ts#Cli.addCommand). Dans les deux cas,
addCommand instancie la commande et l’enregistre sous le nom porté par son constructeur.
⚙️ La complétion shell#
nodefony completion <bash|zsh|fish> imprime un script à sourcer (renderCompletionScript(),
completion.ts#renderCompletionScript ; shells supportés COMPLETION_SHELLS, completion.ts#COMPLETION_SHELLS) :
source <(nodefony completion zsh) # essai immédiat (zsh)
nodefony completion install zsh # installation gérée (bloc idempotent dans le rc)
Au TAB, le script appelle nodefony __complete (fast-path 0 boot, sort toujours OK). Les
suggestions viennent d’un manifeste en cache écrit au boot de dev (commandes de module comprises,
CliKernel.writeCompletionManifest(), CliKernel.ts#CliKernel.writeCompletionManifest) ; hors projet, le repli est la liste des
intégrées en mémoire.
🩺 Codes de sortie#
La CLI suit la convention BSD sysexits (SysExit, sysexits.ts#SysExit) — utile pour scripter et lire
un échec en CI :
| Code | Nom | Quand |
|---|---|---|
| 0 | OK |
Succès. |
| 64 | USAGE |
Mauvais usage : commande/argument inconnu. |
| 65 | DATAERR |
Donnée d’entrée invalide. |
| 66 | NOINPUT |
Entrée attendue absente. |
| 69 | UNAVAILABLE |
Un service requis est indisponible. |
| 70 | SOFTWARE |
Erreur interne pendant l’exécution. |
| 73 | CANTCREAT |
Impossible de créer un fichier de sortie (scaffold). |
| 78 | CONFIG |
Configuration invalide. |
⚠️ Pièges (symptôme → cause → correction)#
| Symptôme | Cause | Correction |
|---|---|---|
| Un « projet fantôme » démarre (1 module, 0 warning) | La CLI résout l’application depuis process.cwd() |
Lancer depuis la racine du projet (celle du nodefony.config.ts). |
Kernel not ready sur module.addCommand(...) |
addCommand appelé alors que kernel.cli n’existe pas encore |
L’appeler dans le constructeur du module, pas plus tôt. |
| La commande n’apparaît pas / n’est pas reconnue | addCommand après le parse, ou module absent du manifeste modules |
Ordre add → parse ; déclarer le module dans modules. |
generate() n’est jamais appelé |
Le kernelEvent déclaré n’est pas atteint (mauvaise phase) |
Choisir la phase réelle (voir § « jusqu’où booter »). |
| Commande inconnue → un serveur démarre | (n’arrive pas) le dispatch rend EX_USAGE 64 |
Vérifier le nom exact ; --help liste tout. |
create … refuse de s’exécuter hors d’un projet |
create module/controller/entity sont in-project |
Les lancer dans une app ; seul create app s’utilise hors projet. |
🧪 Tests#
Le CLI est couvert par une suite dédiée du cœur, sans serveur pour la plupart :
- Unitaires — la façade
Cli(Cli.test.ts), leCliKernelet son registre (CliKernel.test.ts), la classification intégrée/module sans boot (CliKernelDispatch.test.ts), la classe de baseCommand(command/Command.test.ts), les commandes runtime (KernelCommands.test.ts), le scaffold (create.test.ts,entityFields.test.ts,scaffoldDestination.test.ts), la complétion (completion.test.ts) et la délégation du binaire projet/global (resolveLocalCli.test.ts). - Intégration / bout en bout — le binaire réel
node bin/nodefony <commande>(CliIntegration.test.ts) :--help/--versionsans condition, et les boots serveur derrièreNF_RUN_CLI_BOOT=1.
Le décompte exact (cas comptés, par fichier) est rendu dans la carte de tests de cette page — jamais
figé dans le texte, où il vieillirait. Pour le relancer : cd src/nodefony && npm run test (les boots
réels : npm run test:boot).
🔗 Pour aller plus loin#
- ⬆️ Retour au hub : @nodefony/core — vue d’ensemble · Toute la documentation
- 🔄 Kernel & Module — le cycle de vie que les commandes accrochent (
onRegister→onPostReady) - 🚦 Cycle de boot du kernel — l’ordre d’allumage détaillé
- 🏗️ Scaffolds : les skills
create app,create module,create entity(options et gabarits) - 📖 Lexique général — le vocabulaire transverse du framework