@nodefony/devkit 10.0.0-beta.2 → 10.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +151 -5
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  4. package/dist/index.js +24 -11
  5. package/dist/nodefony/config/config.js +16 -137
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/controllers/DevkitController.js +7 -14
  8. package/dist/nodefony/controllers/McpController.js +27 -163
  9. package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
  10. package/dist/nodefony/mcp/observe.js +235 -0
  11. package/dist/nodefony/mcp/profileReport.js +242 -0
  12. package/dist/nodefony/mcp/profileRoute.js +521 -0
  13. package/dist/nodefony/mcp/shotClient.js +84 -0
  14. package/dist/nodefony/mcp/shutdown.js +44 -0
  15. package/dist/nodefony/mcp/tools.js +696 -0
  16. package/dist/nodefony/service/DevkitService.js +93 -29
  17. package/dist/nodefony/src/card.js +1 -2
  18. package/dist/nodefony/src/errors/DevkitError.js +1 -2
  19. package/dist/nodefony/src/profileSession.js +14 -0
  20. package/dist/types/index.d.ts +15 -1
  21. package/dist/types/nodefony/config/config.d.ts +1 -1
  22. package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
  23. package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
  24. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
  25. package/dist/types/nodefony/mcp/observe.d.ts +83 -0
  26. package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
  27. package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
  28. package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
  29. package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
  30. package/dist/types/nodefony/mcp/tools.d.ts +109 -0
  31. package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
  32. package/dist/types/nodefony/src/profileSession.d.ts +12 -0
  33. package/docs/index.md +217 -20
  34. package/package.json +17 -12
  35. package/skills/nodefony-add-crud/SKILL.md +5 -0
  36. package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
  37. package/skills/nodefony-add-service/SKILL.md +14 -3
  38. package/skills/nodefony-authentication/SKILL.md +234 -0
  39. package/skills/nodefony-browser/SKILL.md +65 -21
  40. package/skills/nodefony-browser/references/socket.md +13 -4
  41. package/skills/nodefony-browser/references/sondes.md +13 -0
  42. package/skills/nodefony-browser/scripts/audit.mjs +59 -10
  43. package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
  44. package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
  45. package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
  46. package/skills/nodefony-browser/scripts/socket.mjs +70 -4
  47. package/skills/nodefony-browser/scripts/watch.mjs +101 -0
  48. package/skills/nodefony-dev/SKILL.md +78 -82
  49. package/skills/nodefony-dev/scripts/docs.mjs +24 -8
  50. package/skills/nodefony-devops/SKILL.md +98 -74
  51. package/skills/nodefony-devops/references/compose.md +8 -0
  52. package/skills/nodefony-devops/references/dimensionnement.md +122 -0
  53. package/skills/nodefony-devops/references/frontal.md +282 -48
  54. package/skills/nodefony-devops/references/kubernetes.md +41 -14
  55. package/skills/nodefony-devops/references/secrets.md +30 -5
  56. package/skills/nodefony-devops/references/variables.md +66 -31
  57. package/skills/nodefony-migrate-schema/SKILL.md +5 -2
  58. package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
  59. package/skills/nodefony-protect-route/SKILL.md +124 -21
  60. package/skills/nodefony-security-review/SKILL.md +127 -0
  61. package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
  62. package/skills/nodefony-troubleshoot/SKILL.md +225 -0
  63. package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
  64. package/skills/nodefony-upgrade/SKILL.md +130 -0
  65. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -14,7 +14,7 @@ description: >
14
14
  est-il fini", "je ne trouve rien sur ce sujet", "ce n'est pas documenté", "je touche au
15
15
  frontend", "mon composant charge des données", "mon écran n'affiche rien".
16
16
  metadata:
17
- version: 1.2.0
17
+ version: 1.3.0
18
18
  ---
19
19
 
20
20
  # nodefony-dev — développer dans cette application sans rien inventer
@@ -45,15 +45,19 @@ de son geste, et ceux-là ne se devinent pas — ils se paient.
45
45
  | -------------------------------------------------------------------- | ------------------------------- |
46
46
  | Une ressource : table, validation, service, REST et WebSocket | `nodefony-add-crud` |
47
47
  | Une logique métier réutilisable, hors de tout controller | `nodefony-add-service` |
48
- | Un flux temps réel, un canal, un abonnement client | `nodefony-add-realtime-channel` |
49
- | Réserver une route, un rôle, une zone, ouvrir à un partenaire | `nodefony-protect-route` |
48
+ | Un flux temps réel (WebSocket ou SSE), un canal, un abonnement | `nodefony-add-realtime-channel` |
49
+ | Se connecter : /login, second facteur, retour après connexion, jeton | `nodefony-authentication` |
50
+ | Réserver une route ou une page à des rôles, ouvrir à un partenaire | `nodefony-protect-route` |
50
51
  | Changer une entité déjà en base, ou déployer un schéma changé | `nodefony-migrate-schema` |
51
52
  | Conclure quoi que ce soit d'un ÉCRAN — affichage, contraste, console | `nodefony-browser` |
53
+ | Quelque chose qui marchait ne marche plus : démarrage, 404, journal | `nodefony-troubleshoot` |
54
+ | Relire la sécurité de ton diff AVANT de commiter | `nodefony-security-review` |
55
+ | Monter Nodefony de version, ou une montée qui a laissé des rouges | `nodefony-upgrade` |
52
56
 
53
57
  Aucune ligne ne correspond ? Alors seulement, continue ici. Et si ta tâche est EXACTEMENT celle
54
58
  d'un spécialiste, va droit à lui : il n'y a pas à passer par cette page d'abord.
55
59
 
56
- `ls .agents/skills/` liste ceux que TON projet a reçus ; `npx nodefony ai:sync` les remet à jour
60
+ `ls .agents/skills/` liste ceux que TON projet a reçus ; `npx nodefony upgrade` les remet à jour
57
61
  après une montée de version. Ce sont des **pointeurs** : le contenu vit dans `node_modules` et
58
62
  suit la version installée — les éditer ne servirait à rien.
59
63
 
@@ -124,6 +128,11 @@ Elle répond **toujours** — application pas encore construite, terminal sans a
124
128
  d'environnement : elle ne lit que des fichiers. Elle le DIT alors (« modules installés », pas
125
129
  « chargés ») et renvoie à `inspect modules` pour ce qui est vraiment monté.
126
130
 
131
+ **`rg` ou outil MCP ?** Question étroite, format connu (un motif, un fichier) → `rg`, bien moins
132
+ cher. Question ouverte sur l'application qui TOURNE (démarrage sain ? quelle route casse ?) →
133
+ `nodefony_check` (= `nodefony doctor`), `nodefony_observe` : un appel au lieu de plusieurs `rg`,
134
+ et des faits qu'aucun fichier ne porte.
135
+
127
136
  ```bash
128
137
  npx nodefony inspect routes --json # routes réelles (chemin MONTÉ, méthodes, controller)
129
138
  npx nodefony inspect services --json # services enregistrés, et le module qui les porte
@@ -152,7 +161,8 @@ npx nodefony symbols --module @nodefony/http # toute la surface exportée d'un
152
161
  ```
153
162
 
154
163
  Le graphe symbolique de tout le framework est livré avec le paquet `nodefony` : la réponse ne
155
- dépend ni d'un serveur, ni d'un build, ni de ta connexion. Va y chercher un symbole AVANT d'ouvrir
164
+ dépend ni d'un serveur, ni d'un build, ni de ta connexion. Celui de TON code (`.ai/symbols.json`),
165
+ `npm run dev` le tient à jour — ou `npx nodefony symbols --generate` ; les deux sont lus ensemble. Va y chercher un symbole AVANT d'ouvrir
156
166
  un `.d.ts` — et avant, surtout, d'inventer une signature.
157
167
 
158
168
  **Si la commande te résiste, répare l'APPEL — ne te rabats pas sur les sources.** C'est le réflexe
@@ -165,31 +175,42 @@ tube, puis remets ton filtre.
165
175
  (`npx nodefony ai:mcp` écrit `.mcp.json`) — mêmes réponses, et tes propres modules peuvent publier
166
176
  les leurs. La porte est une ROUTE : elle n'existe que serveur démarré, et un client qui la trouve
167
177
  éteinte la marque en échec pour toute sa session — démarre l'application D'ABORD, ta session
168
- ENSUITE. Tout le reste (déclarer un outil métier, le réserver à des scopes, l'autorisation OAuth) :
178
+ ENSUITE. Aucun serveur ne peut tourner (CI, agent dans un runner) ? Ton client lance lui-même
179
+ `npx nodefony ai:stdio` et lui parle sur l'entrée et la sortie standard : mêmes outils, mais le
180
+ code du démarrage, sans rechargement à chaud ni trafic réel à mesurer. Tout le reste (déclarer un outil métier, le réserver à des scopes, l'autorisation OAuth) :
169
181
  `node_modules/@nodefony/devkit/docs/index.md`.
170
182
 
183
+ **Ta tâche est d'OFFRIR les outils de l'application à un agent** (« expose ceci par MCP ») ? Ce
184
+ n'est pas la porte ci-dessus, qui sert l'outillage en développement : c'est une porte de
185
+ l'APPLICATION, et elle se génère — n'écris pas le protocole à la main :
186
+
187
+ ```bash
188
+ npx nodefony create controller mcp --kind mcp # porte locale : la machine seulement
189
+ npx nodefony create controller mcp --kind mcp --expose oauth # production : jeton exigé (401 sans)
190
+ npx nodefony create controller mcp --kind mcp --expose mixte # outils publics sans jeton, les autres réservés à un scope
191
+ ```
192
+
193
+ Elle naît avec deux outils d'exemple (`nodefony/mcp/mcp-tools.ts`), sa définition
194
+ (`defineMcpGate`, `nodefony/mcp/mcp-gate.ts`) et son test. Ajoute tes outils à la LISTE de la
195
+ porte : elle publie ce qu'elle choisit, rien d'autre. Mode d'emploi :
196
+ `node_modules/@nodefony/ai/docs/mcp.md` § « Monter une porte ».
197
+
171
198
  ### L'environnement : ne devine JAMAIS, demande
172
199
 
173
200
  ```bash
174
- npx nodefony env # cascade des .env, valeur EFFECTIVE de chaque variable, sa PROVENANCE
201
+ npx nodefony env # valeur EFFECTIVE de chaque variable, sa PROVENANCE
175
202
  ```
176
203
 
177
204
  **Encadre toute modification de configuration par cette commande** : une fois AVANT, pour savoir ce
178
205
  qui s'applique et d'où ça vient ; une fois APRÈS, pour prouver que ta valeur est celle qui gagne.
179
- Lire les `.env` toi-même donne des contenus ; la précédence est un mécanisme — tu ne peux que la
180
- supposer, et une supposition fausse ne se voit qu'en production. La commande ne boote rien, donc
206
+ Lire `.env` toi-même donne un contenu ; ce que l'application voit dépend aussi du shell et de
207
+ `env.ts` — tu ne peux que le supposer, et une supposition fausse ne se voit qu'en production. La commande ne boote rien, donc
181
208
  elle répond aussi quand l'application ne démarre plus.
182
209
 
183
- **Précédence, du plus FORT au plus faible** — le premier qui pose une valeur gagne, les suivants
184
- sont ignorés en silence :
185
-
186
- ```
187
- process.env > .env.<déploiement>.local > .env.<mode>.local > .env.local
188
- > .env.<déploiement> > .env.<mode> > .env
189
- ```
190
-
191
- `<mode>` = `NODE_ENV` · `<déploiement>` = `APP_ENV` (plus spécifique, donc plus fort). Les `*.local`
192
- ne sont jamais committés : les secrets y vont, et nulle part ailleurs.
210
+ **Deux fichiers** : `.env` = valeurs du POSTE (secrets de dev compris), **jamais commité** ;
211
+ `.env.example` = notice commitée, **jamais chargée**. `process.env` > `.env`, rien d'autre. La
212
+ production n'a **aucun** fichier (orchestrateur ; réglages non secrets dans `nodefony.config.ts`).
213
+ Un module n'a pas de `.env`. N'écris JAMAIS `.env.local` ni `.env.<mode>` : refusés au démarrage (78).
193
214
 
194
215
  | Forme | Ce que c'est | Où c'est déclaré |
195
216
  | ------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ |
@@ -261,6 +282,13 @@ une table à part — et ce nom est ce que `stop` accepte. Donc « aucune instan
261
282
  REFUSÉE, avec un code de sortie non nul et rien d'arrêté — **lis ce code**, un refus ressemble
262
283
  sinon à un succès.
263
284
 
285
+ **Le plein écran n'est pas pour toi — et il est le DÉFAUT sous macOS et Linux.** Il fige la barre
286
+ d'état en bas, capte le clavier et la souris (molette, PgUp/PgDn, Ctrl+C devient une touche) :
287
+ c'est un écran pour un humain. Il s'active dès que le terminal répond à une sonde, et un
288
+ pseudo-terminal (tmux, un outil qui t'en fournit un) y répond. Nodefony ne devine jamais qu'un
289
+ agent est aux commandes : si tu lances `development` toi-même, ajoute **toujours** `--no-ui`
290
+ (ou pose `NF_DEV_UI=0`), ou `--output plain` pour une sortie sans séquences d'écran.
291
+
264
292
  **Pour faire tourner une suite contre un serveur, prends `--no-watch`.** Le mode développement
265
293
  relance le serveur dès qu'un fichier bouge : pendant un run, le redémarrage coupe les connexions
266
294
  sous les tests, et le rouge qui en sort accuse le code alors que le fautif est le décor.
@@ -283,8 +311,9 @@ routes auth` ne rend que les routes dont le chemin, le nom, le contrôleur, l'ac
283
311
  4. **Édite le moins possible**, et regroupe : toutes les modifications serveur d'une même
284
312
  fonctionnalité, PUIS un seul cycle de reconstruction. Le frontend passe en HMR, zéro
285
313
  redémarrage.
286
- 5. **Prouve.** `npm run verify` — une seule commande : types, style, tests, câblage, dans cet
287
- ordre. Elle s'arrête au premier rouge, et **ce rouge est ta tâche suivante**.
314
+ 5. **Prouve.** `npm run verify` — une seule commande : types, style, mise en forme,
315
+ `.env.example`, tests, build, câblage, dans cet ordre. Elle s'arrête au premier rouge, et
316
+ **ce rouge est ta tâche suivante**.
288
317
 
289
318
  **Le point d'arrêt qu'on rate** : `npm test` seul ne prouve pas que ça compile — vitest
290
319
  n'inspecte aucun type. Une application peut être verte et ne pas compiler.
@@ -304,50 +333,19 @@ ici — ils changent tous les trimestres ; raisonne en poids.
304
333
 
305
334
  ## 7. Les pièges du serveur
306
335
 
307
- Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers sont les plus fréquents.
308
-
309
- - **Ta route répond 404 alors qu'elle existe dans les sources.** Le runtime charge `dist/`, pas
310
- le source : `npm run build`. C'est la cause n°1.
311
- - **Une classe que rien ne déclare compile, passe ses tests, et casse au démarrage suivant** —
312
- entité hors `@entities([…])`, controller hors `@controllers([…])`. Table jamais créée, route
313
- en 404. C'est le mode d'échec de la COPIE : on recopie le voisin au lieu d'appeler le
314
- générateur, qui, lui, déclare. `npx nodefony doctor` la NOMME.
315
- - **Une clé de configuration mal orthographiée est retirée en silence.** `satisfies` sur le
316
- fragment n'est pas décoratif : sans lui, la faute compile, puis la validation écarte la clé et
317
- le module démarre sur son défaut. Personne ne le voit.
336
+ Ceux qu'on pose en ÉCRIVANT. Ceux qu'on rencontre quand quelque chose qui marchait ne marche plus
337
+ — démarrage qui échoue, route en 404, refus inattendu, journal en erreur, tests rouges d'un coup —
338
+ ont leur skill, qui part du symptôme : **`nodefony-troubleshoot`**.
339
+
340
+ - **Une clé de configuration mal orthographiée ARRÊTE le démarrage** (code 78), en la nommant.
341
+ `satisfies` sur un fragment extrait n'est pas décoratif pour autant : il te la signale à la
342
+ compilation, au lieu du prochain démarrage.
318
343
  - **La config d'un module est FIGÉE après le démarrage** : l'écrire lève `TypeError`. Une variante
319
344
  par requête = `overlayConfig()` (clés de la liste blanche du module), lue par `useConfig()`.
320
345
  - **Tu lis une liste sans la BORNER.** Un `find` sans limite matérialise la table entière —
321
346
  indolore sur les quelques lignes du poste de développement, fatal sur les dizaines de milliers
322
347
  de la production. Le service d'une entité hérite `findPage({ limit: 25 })`.
323
348
 
324
- - **Des dizaines de tests d'intégration rouges d'un coup** (`ECONNREFUSED`) : ils FRAPPENT un
325
- serveur, ils ne le lancent pas — il est éteint. `npx nodefony status` d'abord ; en e2e, laisse
326
- la commande gérer le cycle.
327
- - **Ta route NEUVE répond 404 et le `dist/` est à jour** : elle n'est pas montée où tu crois. Le
328
- chemin réel est le PRÉFIXE de son controller suivi du `path` de la route — une action
329
- `path: "/widget"` dans un controller `@controller("/api")` répond sur `/api/widget`.
330
- `npx nodefony inspect routes --json` donne le chemin MONTÉ.
331
- - **TOUT répond 404, même les routes du gabarit** : un AUTRE serveur tient les ports — ou LE TIEN
332
- a glissé, le port voulu étant pris. `npx nodefony status` montre les ports RÉELS, pas ceux que
333
- tu as configurés, et NOMME le projet voisin.
334
- - 🔴 **`nodefony <commande>` échoue là où `npm run dev` réussit, sur la MÊME application** : DEUX
335
- paquets `nodefony` tournent dans le processus — un binaire lié ailleurs (installation globale,
336
- `npm link`) exécute un noyau pendant que l'application importe le sien. Chaque copie a ses
337
- propres classes, son contexte de requête et ses registres d'injection : un module construit à la
338
- frontière perd son container SANS la moindre erreur — il figure dans la liste des modules et ne
339
- fait rien. ⚠️ Les deux copies peuvent être la MÊME version ; deux fichiers distincts suffisent,
340
- il n'y a aucune incohérence à repérer. `npm ls nodefony` liste les copies (« deduped » partout =
341
- une seule) ; `NF_CLI_DEBUG=1 nodefony --version` dit quel CLI s'exécute.
342
- - **`localhost` et `127.0.0.1` te jouent des tours** : ce sont deux ORIGINES distinctes — cookies,
343
- cache et passkeys ne les partagent pas. Une seule origine en développement, partout, URL ouverte
344
- comme callbacks.
345
-
346
- - **Les routes authentifiées plafonnent quand le reste tient la charge** : le stockage de session
347
- par défaut est SYNCHRONE — chaque reprise bloque la boucle d'événements. Compare une route
348
- anonyme et une route authentifiée AVANT d'accuser TLS ou le pare-feu ; passe le stockage sur
349
- redis pour la charge.
350
-
351
349
  ### Ce qui tue le processus, pas la requête
352
350
 
353
351
  - 🔴 **Une promesse lancée sans `await` porte TOUJOURS un `.catch()`.** Audit, courriel,
@@ -361,24 +359,6 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
361
359
  - **Un `PATCH` au corps vide se refuse en `400`.** Le laisser passer produit un `updateOne({})`,
362
360
  qui finit en `500 « No values to set »` — une erreur serveur pour une faute de client.
363
361
 
364
- ### Quand ça ne va pas, et que le message ne suffit pas
365
-
366
- - **Un test vert seul et rouge en suite accuse une RESSOURCE PARTAGÉE**, pas ton code : base ou
367
- index réutilisé, port, store jamais purgé, assertion `count === N` qui suppose une table
368
- vierge. Lance deux fichiers ENSEMBLE pour isoler la paire, puis cloisonne. **Ne sérialise
369
- jamais la suite** pour faire passer le rouge : tu masques la cause et tu paies la lenteur à
370
- chaque exécution.
371
- - **Une suite lancée contre un serveur en `production` reçoit `404` partout** : les modules
372
- `policy:"dev"` n'y sont pas. `NF_WITH_DEV_MODULES=1` déroge pour 30 minutes
373
- (`NF_WITH_DEV_MODULES_TTL_MIN`, 4 h au plus), et le `CRITIC « arrêt automatique … dérogation »`
374
- du journal est cette garde qui se referme — pas une panne.
375
- - **Ton application a démarré AMPUTÉE et tout a l'air sain** — base injoignable, module écarté
376
- par sa politique. Seul `npx nodefony doctor` le dit, en relisant `var/last-boot.json`.
377
- ⚠️ Ce bilan a un ÂGE : il décrit le **dernier démarrage**, et une commande console (`inspect`,
378
- une commande de module) ne l'écrase pas. C'est voulu — lis la date avant d'en conclure.
379
- - **L'application ne démarre plus et le superviseur avale la sortie** :
380
- `NF_DEV_CHILD=1 npx nodefony development` lance l'enfant seul et montre le crash brut.
381
-
382
362
  ## 8. Données et fichiers — les façades qu'on ne recompose pas
383
363
 
384
364
  - **Un cache ne remplace pas une base : ils se COMPLÈTENT.** Chaque adaptateur déclare les
@@ -386,6 +366,14 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
386
366
  huit, `redis` quatre (`session`, `tokens`, `passkeys`, `idempotency`) : un cache ne garde ni
387
367
  comptes ni journal d'audit. Ne promets jamais une parité qui n'existe pas — lis ce champ, et
388
368
  `npx nodefony inspect stores` dit où atterrit chaque donnée.
369
+ - **L'entrée d'une route se DÉCLARE — ne la valide pas à la main.** L'option `schema` d'un
370
+ décorateur de route (`@Post("/posts", { schema: { params, query, body, headers } })`, un schéma Zod
371
+ par partie) fait valider l'entrée AVANT l'action — **422** qui nomme chaque champ et sa partie, sur
372
+ HTTP comme par la socket (`api.request`) — et l'action la reçoit typée en UN argument,
373
+ `input: RouteInput<…>`. Une action à schéma ne prend **aucun** décorateur de paramètre (`@Body`,
374
+ `@Query`…) : le mélange est refusé au démarrage, qui nomme la route. L'identité se lit par
375
+ `this.user`. Un corps binaire arrive en octets (`Uint8Array`), un `text/*` en chaîne : un même
376
+ `z.instanceof(Uint8Array)` vaut pour les deux portes. Doc : `node_modules/@nodefony/framework/docs/decorateurs.md`.
389
377
  - **Les violations de contrainte sont DÉJÀ traduites en HTTP — ne les rattrape pas.** Un doublon
390
378
  sur une colonne unique ressort en **409**, une donnée qui viole le schéma Zod en **422**, chacun
391
379
  avec son corps JSON : le rendu d'erreur lit le code du pilote (`23505`, `ER_DUP_ENTRY`,
@@ -454,11 +442,16 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
454
442
  **couleur seule**, `rel="noopener noreferrer"` sur les liens externes.
455
443
 
456
444
  - **Une route d'API répond du HTML** : un repli SPA générique avale les routes voisines — le
457
- premier motif qui correspond gagne. Repli en préfixe LITTÉRAL ; `npx nodefony inspect routes
458
- --json` montre l'ordre réel.
445
+ premier motif qui correspond gagne. Déclare la route de la page monopage avec
446
+ `fallback: true` : elle ne sert alors que ce qu'aucune autre route ne sert, quel que soit
447
+ l'ordre. `npx nodefony inspect routes --json` montre l'ordre réel.
459
448
  - **Des utilisateurs « déconnectés au hasard »** : le traitement global « 401 = session expirée »
460
- frappe aussi les sondes d'authentification, où 401 est NORMAL — et détruit une session valide.
461
- Exempte les sondes du traitement global.
449
+ frappe aussi la connexion refusée (un mauvais mot de passe rend 401) — et détruit une session
450
+ valide. Exempte les routes d'authentification du traitement global. `me` ne renvoie jamais
451
+ 401 : il répond toujours 200, `{ user: null }` sans session.
452
+ - **La connexion ne s'écrit pas en `fetch`** : `/login` est servie ; la liaison du front
453
+ (`useNodefonyLogin`…) porte `me()` et `logout()` — suis l'adresse que rend `logout()`, sinon
454
+ la session Keycloak reste ouverte. Détail : skill `nodefony-authentication`.
462
455
  - **Ta page répond 200 et son script ne s'exécute pas** : la politique de contenu exige un `nonce`
463
456
  sur les scripts, et le navigateur refuse un `<script>` en ligne qui n'en porte pas (« Refused to
464
457
  execute inline script »). Un `curl` ne le voit JAMAIS, il ne lit que le corps. Signe le script
@@ -477,10 +470,13 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
477
470
  ## 10. Avant de dire « fait »
478
471
 
479
472
  ```bash
480
- npm run verify # types + style + tests + câblage
473
+ npm run verify # types, style, mise en forme, .env.example, tests, build, câblage
481
474
  npm run test:e2e # boot réel + HTTP/WS — le gate LENT, hors `verify`
482
475
  ```
483
476
 
477
+ Ton diff ajoute une route, une requête ou touche la sécurité ? Relis-le AVANT de commiter :
478
+ skill `nodefony-security-review` — un script y nomme, ligne par ligne, ce qu'un œil laisse passer.
479
+
484
480
  Puis, en une phrase : **nomme ce que tu n'as PAS lancé.** Un vert ne couvre que le diff qui l'a
485
481
  produit, et un banc sauté faute de son décor compte comme vert. Dire « e2e non lancé » coûte
486
482
  cinq mots ; le taire coûte la confiance dans tout le reste.
@@ -21,10 +21,23 @@
21
21
  * `@output` les pages classées, avec chemin, ligne et extrait — ou un refus qui DIT pourquoi
22
22
  */
23
23
 
24
- import { readFileSync, readdirSync, statSync } from "node:fs";
24
+ import { readFileSync, readdirSync, statSync, realpathSync } from "node:fs";
25
25
  import path from "node:path";
26
26
  import process from "node:process";
27
- import { pathToFileURL } from "node:url";
27
+ import { fileURLToPath } from "node:url";
28
+ import { findProjectRoot } from "nodefony/project-root";
29
+
30
+ /**
31
+ * Racine de l'APPLICATION vue depuis `cwd` — le dossier qui porte
32
+ * `nodefony.config.ts` —, ou `cwd` lui-même hors application. Un script lancé
33
+ * depuis `frontend/` ou `modules/blog/` travaille ainsi sur l'app entière.
34
+ * `nodefony/project-root` est le sous-chemin LÉGER du cœur (`node:` seul) :
35
+ * importer `nodefony` entier coûtait jusqu'à plusieurs secondes à froid.
36
+ *
37
+ * @param {string} cwd - le dossier de lancement.
38
+ * @returns {string} la racine retenue.
39
+ */
40
+ const appRoot = (cwd) => findProjectRoot(cwd) ?? cwd;
28
41
 
29
42
  /** Nombre de pages rendues par défaut — au-delà, personne ne lit. */
30
43
  const DEFAULT_LIMIT = 6;
@@ -55,14 +68,14 @@ const FLAGS = new Set([
55
68
  * @returns les options lues.
56
69
  * @throws Error quand un drapeau est inconnu ou qu'une valeur manque.
57
70
  */
58
- export function parseArgs(argv) {
71
+ export function parseArgs(argv, cwd = process.cwd()) {
59
72
  const opts = {
60
73
  terms: [],
61
74
  list: false,
62
75
  open: /** @type {string | null} */ (null),
63
76
  json: false,
64
77
  limit: DEFAULT_LIMIT,
65
- root: process.cwd(),
78
+ root: appRoot(cwd),
66
79
  help: false,
67
80
  };
68
81
  for (let i = 0; i < argv.length; i += 1) {
@@ -123,7 +136,7 @@ export function docDirectories(root) {
123
136
  };
124
137
  push(path.join(modules, "nodefony"));
125
138
  const scope = path.join(modules, "@nodefony");
126
- let entries = [];
139
+ let entries;
127
140
  try {
128
141
  entries = readdirSync(scope);
129
142
  } catch {
@@ -142,7 +155,7 @@ export function docDirectories(root) {
142
155
  function markdownFiles(dir) {
143
156
  const out = [];
144
157
  const walk = (current) => {
145
- let entries = [];
158
+ let entries;
146
159
  try {
147
160
  entries = readdirSync(current, { withFileTypes: true });
148
161
  } catch {
@@ -287,7 +300,7 @@ export function index(root) {
287
300
  const pages = [];
288
301
  for (const dir of docDirectories(root)) {
289
302
  for (const file of markdownFiles(dir)) {
290
- let source = "";
303
+ let source;
291
304
  try {
292
305
  source = readFileSync(file, "utf8");
293
306
  } catch {
@@ -538,7 +551,10 @@ export function main() {
538
551
  // confondrait deux scripts homonymes de deux skills.
539
552
  if (
540
553
  process.argv[1] !== undefined &&
541
- pathToFileURL(process.argv[1]).href === import.meta.url
554
+ // Chemins RÉELS des deux côtés : Node résout `import.meta.url` en suivant
555
+ // les liens, pas `argv[1]` — lancé par `/var/…` (macOS) ou un dossier lié,
556
+ // la comparaison brute échouait et le script ne faisait RIEN.
557
+ realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
542
558
  ) {
543
559
  process.exitCode = main();
544
560
  }