@nodefony/devkit 10.0.0-alpha.1

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 (67) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +318 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
  11. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  16. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  17. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  18. package/dist/index.js +47 -0
  19. package/dist/nodefony/command/CardCommand.js +70 -0
  20. package/dist/nodefony/config/config.js +200 -0
  21. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  22. package/dist/nodefony/controllers/DevkitController.js +60 -0
  23. package/dist/nodefony/controllers/McpController.js +223 -0
  24. package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
  25. package/dist/nodefony/interfaces/IDevkitService.js +1 -0
  26. package/dist/nodefony/interfaces/index.js +1 -0
  27. package/dist/nodefony/service/DevkitService.js +198 -0
  28. package/dist/nodefony/src/card.js +2 -0
  29. package/dist/nodefony/src/errors/DevkitError.js +21 -0
  30. package/dist/nodefony/src/mcp/guard.js +51 -0
  31. package/dist/nodefony/src/mcp/protocol.js +127 -0
  32. package/dist/nodefony/src/mcp/server.js +133 -0
  33. package/dist/nodefony/src/mcp/tools.js +163 -0
  34. package/dist/types/index.d.ts +52 -0
  35. package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
  36. package/dist/types/nodefony/config/config.d.ts +25 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  38. package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
  39. package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
  40. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
  41. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  43. package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
  44. package/dist/types/nodefony/src/card.d.ts +18 -0
  45. package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
  46. package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
  47. package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
  48. package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
  49. package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
  50. package/docs/index.md +358 -0
  51. package/package.json +77 -0
  52. package/skills/nodefony-add-crud/SKILL.md +199 -0
  53. package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
  54. package/skills/nodefony-add-service/SKILL.md +90 -0
  55. package/skills/nodefony-browser/SKILL.md +416 -0
  56. package/skills/nodefony-browser/references/socket.md +115 -0
  57. package/skills/nodefony-browser/references/sondes.md +175 -0
  58. package/skills/nodefony-browser/scripts/audit.mjs +169 -0
  59. package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
  60. package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
  61. package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
  62. package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
  63. package/skills/nodefony-browser/scripts/socket.mjs +354 -0
  64. package/skills/nodefony-browser/scripts/watch.mjs +125 -0
  65. package/skills/nodefony-migrate-schema/SKILL.md +359 -0
  66. package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
  67. package/skills/nodefony-protect-route/SKILL.md +195 -0
package/docs/index.md ADDED
@@ -0,0 +1,358 @@
1
+ ---
2
+ title: "@nodefony/devkit — l'outillage de développement d'une application"
3
+ navTitle: devkit
4
+ lang: fr
5
+ module: "@nodefony/devkit"
6
+ topic: overview
7
+ audience: [human, ai]
8
+ tags: [module, developpement, agent]
9
+ status: stable
10
+ updated: 2026-09-01
11
+ source: "src/packages/@nodefony/devkit/docs/index.md"
12
+ ---
13
+
14
+ # devkit
15
+
16
+ > L'outillage de développement d'une application : sa carte de visite, les
17
+ > skills qui disent à un agent comment faire les tâches courantes, et les portes
18
+ > qui mènent au reste.
19
+
20
+ Cette page est **surfacée dans Studio** (onglet Docs du module).
21
+
22
+ 📍 [Documentation](../../../../../docs/index.md) › **@nodefony/devkit**
23
+
24
+ ## Par où commencer
25
+
26
+ ```nodefony-cards
27
+ [
28
+ { "icon": "🪪", "title": "La carte de visite", "href": "#le-problème-quil-résout",
29
+ "desc": "`npx nodefony card` : qui répond ici, quels modules, où lire, quoi lancer. Servie par le cœur — elle marche même sans application construite." },
30
+ { "icon": "🔌", "title": "Le serveur MCP", "href": "#le-serveur-mcp--les-mêmes-réponses-en-outils",
31
+ "desc": "Les mêmes réponses, mais en outils qu'un agent appelle — dont l'autorisation OAuth 2.1 et vos propres outils." },
32
+ { "icon": "🎓", "title": "Les skills d'agent", "href": "#les-skills-dagent--répondre-à--comment-fait-on-ça-ici--",
33
+ "desc": "Ce qui répond à « comment fait-on ça, ici ? » sans que l'agent invente une convention." },
34
+ { "icon": "🚫", "title": "Ce qu'il ne fait pas", "href": "#ce-quil-ne-fait-pas",
35
+ "desc": "La frontière du module — et pourquoi il n'existe pas en production." }
36
+ ]
37
+ ```
38
+
39
+ ## Le problème qu'il résout
40
+
41
+ Une application Nodefony sait beaucoup de choses sur elle-même — ses modules, ses
42
+ routes, sa configuration, la documentation installée avec chaque paquet. Mais
43
+ elle ne le **dit** à personne. Celui qui arrive — un développeur qui reprend le
44
+ projet, un agent qui code — n'a d'autre choix que de deviner : lire les sources,
45
+ supposer une convention, inventer une route.
46
+
47
+ Le devkit répond à la question d'ouverture, et à elle seule : **qui répond ici,
48
+ et où faut-il aller ensuite ?**
49
+
50
+ ```bash
51
+ npx nodefony card # -j pour du JSON (| jq)
52
+ ```
53
+
54
+ La réponse tient en trois blocs : l'identité (nom, version, environnement, cœur),
55
+ les modules, puis **où lire** et **quoi lancer**.
56
+
57
+ Cette commande-là est servie par le **cœur**, pas par ce module : une carte de
58
+ visite qui exigerait une application déjà construite, ou une variable
59
+ d'environnement posée, serait fermée au moment exact où l'on en a besoin. Elle ne
60
+ lit que des fichiers — et quand rien n'a démarré, elle annonce des modules
61
+ **installés** plutôt que chargés, en renvoyant à `npx nodefony inspect modules`.
62
+
63
+ ## Pourquoi il n'existe pas en production
64
+
65
+ Ce que la carte expose — modules chargés, chemins de documentation, commandes —
66
+ aide pendant le développement. En production, c'est une description de votre
67
+ architecture offerte à qui la demande : une divulgation, pas une fonctionnalité.
68
+
69
+ D'où la double protection, et les deux moitiés comptent :
70
+
71
+ ```ts
72
+ // nodefony.config.ts — posé par `nodefony create app`
73
+ use("@nodefony/devkit", {}, { policy: "dev" }),
74
+ ```
75
+
76
+ - **`devDependencies`** : `npm ci --omit=dev` ne l'installe pas ;
77
+ - **`policy: "dev"`** : un déploiement qui installerait tout ne le charge pas
78
+ quand même. Un module non chargé n'est **même pas importé** — le coût en
79
+ production est nul, pas « faible ».
80
+
81
+ Corollaire à connaître : hors développement, c'est **la route** qui n'existe pas.
82
+ La commande, elle, répond — elle ne passe pas par ce module.
83
+
84
+ ## Quatre portes, une seule source
85
+
86
+ | Porte | Pour qui |
87
+ | ------------------------------- | ------------------------------------------------------------ |
88
+ | `npx nodefony card` | un agent, un humain au terminal — servie par le cœur |
89
+ | `GET /nodefony/devkit/api/card` | Studio, un script authentifié — modules réellement CHARGÉS |
90
+ | `POST /nodefony/mcp` | un agent qui appelle des **outils** (Model Context Protocol) |
91
+ | `buildCard()` (export du cœur) | une porte de plus, à écrire — rien à réimplémenter |
92
+
93
+ **Ajouter une porte n'ajoute jamais une vérité** : toutes lisent le même
94
+ service, qui dérive le même Kernel. La construction elle-même vit dans une
95
+ fonction pure (`buildCard`) qui reçoit son état au lieu de le lire — c'est ce qui
96
+ la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce soit.
97
+
98
+ > La route de la carte vit sous `/nodefony/<module>/api`, que le pare-feu d'une
99
+ > application réelle couvre : un agent qui code ne s'authentifie pas et n'a pas
100
+ > de navigateur. La porte qui compte pour lui est la commande — ou le serveur
101
+ > MCP ci-dessous.
102
+
103
+ ## Le serveur MCP — les mêmes réponses, en outils
104
+
105
+ ```bash
106
+ npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
107
+ ```
108
+
109
+ Quatre outils, qui sont les commandes que vous connaissez déjà :
110
+ `nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
111
+ `nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
112
+
113
+ **Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
114
+ transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
115
+ route de votre application. Elle vit tant qu'elle tourne, suit chaque
116
+ rechargement du serveur de développement, et n'a aucun cache à invalider.
117
+
118
+ **La déclaration reste dans VOTRE projet.** `ai:mcp` écrit `.mcp.json` à la
119
+ racine du projet — jamais dans une configuration globale. Ce n'est pas un détail
120
+ de rangement : l'URL porte un **port**, et deux applications Nodefony ouvertes en
121
+ même temps n'écoutent pas sur le même. Une déclaration globale en désignerait
122
+ forcément une, au hasard. (Pour Mistral Vibe, l'équivalent par projet est
123
+ `.vibe/config.toml`, section `[[mcp_servers]]`.)
124
+
125
+ **Cinq révisions servies** — `2026-07-28`, `2025-11-25`, `2025-06-18`,
126
+ `2025-03-26`, `2024-11-05` — et le serveur **répond celle que le client
127
+ demande**. Annoncer la plus récente à tout le monde rend la porte injoignable :
128
+ un SDK qui ne connaît pas encore cette révision raccroche, et le serveur le plus
129
+ conforme du monde ne parle alors à personne.
130
+
131
+ Trois choses à savoir avant de s'étonner d'un refus :
132
+
133
+ - une **adresse non locale** reçoit `403` (`mcp.allowRemote`) ;
134
+ - une **origine de navigateur** non déclarée reçoit `403` (`mcp.allowedOrigins`).
135
+ Un client MCP natif n'envoie pas d'`Origin` ; une page web en envoie toujours
136
+ un — c'est ce qui ferme le détournement DNS, seul vecteur réel contre un
137
+ serveur local ;
138
+ - le module étant `policy: "dev"`, **la route n'existe pas en production**.
139
+
140
+ Les outils intégrés sont en lecture seule, et `mcp.tools` est une allowlist.
141
+
142
+ ### Obtenir le porteur — `security:token`
143
+
144
+ Dès que la porte demande un jeton, il faut en produire un. L'application **le signe elle-même**, en
145
+ ligne de commande, sans qu'aucun serveur n'écoute :
146
+
147
+ ```bash
148
+ npx nodefony security:token --write # pose NF_MCP_TOKEN chez les agents présents
149
+ npx nodefony ai:mcp --auth # l'en-tête porte ${NF_MCP_TOKEN}, jamais le jeton
150
+ ```
151
+
152
+ `.mcp.json` ne contient donc **que le nom de la variable**. Le flux complet — audience à déclarer,
153
+ durée, rotation, et le refus qu'on rencontre en premier — vit dans
154
+ [obtenir un jeton](../../security/docs/obtenir-un-jeton.md).
155
+
156
+ ### Passer la porte sous autorisation OAuth 2.1
157
+
158
+ Le périmètre ci-dessus suffit à un poste de développement. Dès que la porte doit
159
+ répondre à quelqu'un d'autre, déclarez un serveur d'autorisation : elle devient
160
+ alors un **resource server** au sens de la spécification — elle publie ses
161
+ métadonnées ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)), valide
162
+ le porteur, refuse en `401`/`WWW-Authenticate`
163
+ ([RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)) et vérifie
164
+ l'audience ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).
165
+
166
+ ```ts
167
+ use("@nodefony/devkit", {
168
+ mcp: {
169
+ authorization: {
170
+ authorizationServers: ["https://auth.example"],
171
+ resource: "https://mon-app.example/nodefony/mcp",
172
+ },
173
+ },
174
+ });
175
+ ```
176
+
177
+ | Ce qui se passe alors | Où |
178
+ | ------------------------------------------------------------ | -------------------------------------------------------- |
179
+ | Le document de métadonnées est publié | `GET /.well-known/oauth-protected-resource/nodefony/mcp` |
180
+ | Une requête sans jeton — ou sans jeton LISIBLE — est refusée | `401` + `WWW-Authenticate: Bearer resource_metadata="…"` |
181
+ | Un jeton d'une autre audience n'ouvre rien | `401` — ou servi en **anonyme** si `anonymous: true` |
182
+ | Un outil déclarant des `scopes` devient atteignable | pour qui présente **tous** ces scopes |
183
+ | Les scopes publiés sont l'**union** de ceux des outils | `scopes_supported` du document, et `scope` du défi |
184
+
185
+ Le serveur d'**autorisation** n'est jamais à écrire : la spécification le place
186
+ « beyond the scope […] or a separate entity ». N'importe quel émetteur OAuth 2.1
187
+ convient.
188
+
189
+ > 🔴 **Ce module ne valide pas les jetons lui-même** — il est `policy: "dev"` et
190
+ > ne porte aucune cryptographie. Il cherche un service `accessTokenVerifier` dans le
191
+ > conteneur (contrat `IAccessTokenVerifier`, exporté par `nodefony`). Sans lui,
192
+ > une porte déclarée protégée répond `503` et le journal le dit en `CRITIC` :
193
+ > accepter des porteurs sans les lire serait pire que rester anonyme.
194
+
195
+ ### Ajouter VOS outils — ce que l'agent ne peut pas deviner
196
+
197
+ Les quatre outils ci-dessus décrivent le framework. Ils ne savent rien de votre
198
+ métier — et c'est précisément ce qu'un agent invente le plus mal. N'importe quel
199
+ module de votre application peut donc en publier :
200
+
201
+ ```ts
202
+ import { Module, mcpText, type IMcpTool } from "nodefony";
203
+
204
+ class Shop extends Module {
205
+ getMcpTools(): IMcpTool[] {
206
+ return [
207
+ {
208
+ name: "shop_stock",
209
+ // ⭐ La description est ce qui DÉCLENCHE l'outil. Dire ce qu'il rend
210
+ // ET quand s'en servir — un modèle n'appelle pas ce qu'il ne
211
+ // comprend pas.
212
+ description:
213
+ "Stock réel d'une référence produit. À utiliser avant de proposer " +
214
+ "une commande — la réponse vient de la base, pas d'un cache.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: { sku: { type: "string", description: "Référence" } },
218
+ required: ["sku"],
219
+ },
220
+ handler: async (args) => mcpText(await this.stock(String(args.sku))),
221
+ },
222
+ ];
223
+ }
224
+ }
225
+ ```
226
+
227
+ Trois choses à savoir :
228
+
229
+ - **rien ne s'enregistre au démarrage** : la liste est relue à chaque requête,
230
+ donc un module ajouté apparaît sans redémarrer quoi que ce soit ;
231
+ - **`mcp.tools` ne filtre que les outils intégrés** — le vôtre est publié dès
232
+ qu'il est déclaré, sans ligne de configuration supplémentaire ;
233
+ - un outil **écarté** (nom hors `[a-zA-Z0-9_-]{1,64}`, nom déjà pris, handler
234
+ absent, déclaration qui lève) le dit en `WARNING` dans les journaux du
235
+ serveur — il ne disparaît jamais en silence. Les outils intégrés gagnent
236
+ toute collision : personne ne peut répondre à la place de `nodefony_inspect`.
237
+
238
+ ### Réserver un outil à qui est autorisé
239
+
240
+ Un outil peut exiger des **scopes** (tous, pas au moins un) et/ou une identité
241
+ prouvée. Son handler reçoit alors l'appelant en second paramètre, pour borner ce
242
+ qu'il **rend** et pas seulement décider s'il répond :
243
+
244
+ ```ts
245
+ {
246
+ name: "shop_invoice",
247
+ description: "Facture d'une commande.",
248
+ inputSchema: { type: "object", properties: { id: { type: "string" } } },
249
+ scopes: ["shop:read", "shop:billing"], // ou : requiresAuth: true
250
+ handler: async (args, caller) =>
251
+ mcpText(await this.invoice(String(args.id), caller.subject)),
252
+ }
253
+ ```
254
+
255
+ La spec le prévoit explicitement : le jeu d'outils « MAY vary by the
256
+ authorization presented on the request — for example, returning only the tools
257
+ the caller's granted scopes permit », précisément parce que les identifiants
258
+ sont une **entrée de requête, pas un état de connexion**. C'est pourquoi la
259
+ liste est recollectée à chaque appel.
260
+
261
+ Le filtre s'applique **à la collecte**, donc en un seul point : un outil retenu
262
+ est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
263
+ « outil inconnu » plutôt qu'« interdit » — son existence même n'est pas révélée.
264
+
265
+ > 🔴 **Tant que la porte n'authentifie personne, un outil à scopes ne sortira
266
+ > jamais.** C'est le comportement voulu — fermé par défaut — mais il faut le
267
+ > savoir avant de chercher une panne : `caller` vaut `{ authenticated: false,
268
+ scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
269
+ > branché. Le jour où il le sera, ces déclarations prendront effet sans qu'une
270
+ > ligne d'outil change.
271
+
272
+ > ⚠️ Cette porte n'est pas authentifiée (voir l'écart ci-dessus) et le module est
273
+ > `policy: "dev"`. Avant d'exposer une donnée par un outil, se demander si elle
274
+ > supporterait d'être lue **sans identification**, par qui a accès à la machine.
275
+
276
+ ## Les skills d'agent — répondre à « comment fait-on ça, ici ? »
277
+
278
+ La carte dit **où aller**. Elle ne dit pas **comment faire**. Or c'est là qu'un
279
+ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
280
+ service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
281
+ dans le corps de l'action.
282
+
283
+ Le paquet livre donc cinq **skills** au format [Agent Skills](https://agentskills.io),
284
+ un par tâche où l'invention coûte cher :
285
+
286
+ | Skill | Le besoin qu'il couvre |
287
+ | ------------------------------- | ----------------------------------------------------------- |
288
+ | `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
289
+ | `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
290
+ | `nodefony-protect-route` | réserver une route à qui est habilité |
291
+ | `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
292
+ | `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
293
+
294
+ Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
295
+ vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
296
+ serait écrasé à la synchronisation suivante.
297
+
298
+ `nodefony create app` les met à disposition à la création ; après une montée de
299
+ version, `npx nodefony ai:sync` les remet à jour (`--dry-run` montre sans
300
+ écrire).
301
+
302
+ **Ce qui est écrit dans le projet est un pointeur, jamais une copie.** Le contenu
303
+ reste dans le paquet installé et suit `npm update` : une recette recopiée dans un
304
+ projet décrit, six mois plus tard, un framework qui a changé — et comme rien ne
305
+ casse, personne ne s'en aperçoit. C'est le même principe que le reste du devkit :
306
+ **rien de figé n'est copié chez l'utilisateur.**
307
+
308
+ Le dossier visé — `.agents/skills/` — est celui que tous les clients conformes
309
+ lisent, plutôt que le dossier propriétaire d'un seul d'entre eux. Ces fichiers
310
+ sont faits pour être commités : l'équipe entière et l'intégration continue
311
+ travaillent alors avec les mêmes recettes.
312
+
313
+ > Aucun `postinstall` ne les pose : `--ignore-scripts` est courant, les scripts
314
+ > d'installation sont un vecteur d'attaque connu de l'écosystème npm, et écrire
315
+ > dans un dossier versionné à chaque installation produirait des différences
316
+ > surprises.
317
+
318
+ ## Ce qu'il ne fait pas
319
+
320
+ Le scaffold (`nodefony create …`), le diagnostic (`nodefony doctor`) et
321
+ l'introspection (`nodefony inspect`) **ne sont pas ici** : ils vivent dans le
322
+ cœur, parce qu'ils doivent répondre sans qu'aucun module soit installé — et
323
+ surtout quand l'application est cassée. Un outil de diagnostic qui exige que
324
+ l'application démarre ne sert pas au moment où on en a besoin.
325
+
326
+ Même partage pour les skills, et il se retient en une phrase : **le VERBE vit
327
+ dans le cœur, le CONTENU dans ce paquet.** `ai:sync` doit répondre dans un
328
+ terminal qui n'a rien posé (portée par un module `policy: "dev"`, elle serait
329
+ absente sans `NODE_ENV`) ; les skills, eux, doivent se mettre à jour par npm.
330
+
331
+ ## Configuration
332
+
333
+ | Clé | Type | Défaut | Rôle |
334
+ | -------------------- | ---------- | -------------------------------------- | ---------------------------------------------- |
335
+ | `enabled` | `boolean` | `true` | Interrupteur du module |
336
+ | `mcp.enabled` | `boolean` | `true` | Répond-on aux requêtes MCP ? Coupé → `404` |
337
+ | `mcp.allowedOrigins` | `string[]` | `[]` | Origines de navigateur admises ; vide = aucune |
338
+ | `mcp.allowRemote` | `boolean` | `false` | Accepter un appel d'une adresse non locale |
339
+ | `mcp.tools` | `string[]` | `["inspect","check","symbols","card"]` | Allowlist des outils — lecture seule |
340
+
341
+ Les clés, leurs types et leurs défauts viennent du schéma Zod
342
+ (`nodefony/config/config.ts`) — **source unique** dont dérivent la
343
+ documentation, la validation au boot et le formulaire d'édition de Studio.
344
+
345
+ ## Pour aller plus loin
346
+
347
+ - ⬆️ **Retour au hub** : [documentation Nodefony](../../../../../docs/index.md) — et
348
+ [par où commencer](../../../../../docs/demarrer.md) si vous arrivez sur le framework.
349
+ - 🏗️ **Générer du code plutôt que l'écrire** :
350
+ [`nodefony create`](../../../../../docs/guides/generer-du-code.md) — voir ce qui va changer
351
+ avant que ça change, et l'appeler depuis un agent.
352
+ - 🧪 **Ce que vaut cet outillage, mesuré** :
353
+ [éprouver un framework avec un agent](../../../../../docs/guides/eprouver-loutillage-agent.md).
354
+ - 🖥️ **Les commandes du cœur** : [la CLI](../../../../nodefony/docs/cli.md) — `card` et `inspect`
355
+ y sont servies, pas ici.
356
+ - 📚 **La documentation installée avec les paquets** :
357
+ [`@nodefony/documentation`](../../documentation/docs/index.md).
358
+ - 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@nodefony/devkit",
3
+ "version": "10.0.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
6
+ "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "docs",
19
+ "skills"
20
+ ],
21
+ "scripts": {
22
+ "build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
23
+ "typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json",
24
+ "test": "vitest run",
25
+ "coverage": "vitest run --coverage"
26
+ },
27
+ "peerDependencies": {
28
+ "@nodefony/framework": "*",
29
+ "@nodefony/http": "*",
30
+ "nodefony": "*",
31
+ "zod": "^4.4.3",
32
+ "playwright": "^1.50.0",
33
+ "lighthouse": "^13.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@nodefony/framework": "*",
37
+ "@nodefony/http": "*",
38
+ "nodefony": "*"
39
+ },
40
+ "dependencies": {
41
+ "axe-core": "4.13.0",
42
+ "tslib": "2.8.1"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "playwright": {
46
+ "optional": true
47
+ },
48
+ "lighthouse": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/nodefony/nodefony-core.git",
55
+ "directory": "src/packages/@nodefony/devkit"
56
+ },
57
+ "homepage": "https://nodefony.github.io/nodefony-core/",
58
+ "bugs": {
59
+ "url": "https://github.com/nodefony/nodefony-core/issues"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "license": "CECILL-B",
65
+ "keywords": [
66
+ "nodefony",
67
+ "devkit",
68
+ "developer-tools",
69
+ "agent",
70
+ "scaffolding",
71
+ "typescript",
72
+ "esm"
73
+ ],
74
+ "engines": {
75
+ "node": ">=24.0.0"
76
+ }
77
+ }
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: nodefony-add-crud
3
+ description: >
4
+ Crée une ressource complète dans une application Nodefony — table, schémas de validation,
5
+ service CRUD, controller REST+WebSocket et tests — par le générateur `nodefony create entity`,
6
+ au lieu de l'écrire à la main. Porte la grammaire de champs (types, relations, index simples et
7
+ composites), les réglages pour épouser une table SQL existante, et les trois vérités qu'on
8
+ découvre autrement en production : la table naît au démarrage, un champ ajouté n'est rattrapé que
9
+ s'il accepte le vide, et la production s'applique par des migrations — dont le cycle complet vit
10
+ dans le skill `nodefony-migrate-schema`. À charger AVANT d'écrire une entité, un repository ou un
11
+ controller de ressource.
12
+ Déclencheurs : "ajoute une entité", "crée un CRUD", "nouvelle table", "modèle de données",
13
+ "ressource REST", "endpoint CRUD", "je veux stocker des articles/commandes/utilisateurs",
14
+ "comment définir un champ", "une relation entre deux entités", "index composite",
15
+ "épouser une table existante", "renommer les colonnes en snake_case".
16
+ ---
17
+
18
+ # add-crud — une ressource complète, générée
19
+
20
+ > ⚖️ **La confiance n'exclut pas le contrôle.** Ce que le générateur produit se relit ;
21
+ > ce que tu écris à la main se prouve par un test.
22
+
23
+ ## Le geste
24
+
25
+ ```bash
26
+ npx nodefony create entity Article title:string body:text? published:bool
27
+ ```
28
+
29
+ Une seule commande produit la chaîne entière : la table, son interface de ligne, les schémas de
30
+ validation d'entrée, le service CRUD, le controller (REST **et** WebSocket dans la même méthode)
31
+ et les tests. **N'écris aucun de ces fichiers à la main** — non par principe, mais parce que le
32
+ gabarit porte des détails qui ne se devinent pas : le repository résolu au premier usage (l'ORM ne
33
+ se connecte qu'au démarrage), la pagination bornée **et son tri déclaré**, les codes
34
+ 201/204/404/409/422, et l'en-tête `Location`.
35
+
36
+ ## La grammaire de champs
37
+
38
+ `nom:type[?|!][:index]` — **non-null par défaut**, `?` rend facultatif, `!` pose une contrainte
39
+ d'unicité, `:index` un index simple.
40
+
41
+ | Type | Ce que ça produit |
42
+ | -------- | ----------------------- |
43
+ | `string` | texte court (une ligne) |
44
+ | `text` | texte long |
45
+ | `int` | entier |
46
+ | `float` | décimal |
47
+ | `bool` | booléen |
48
+ | `json` | document |
49
+ | `date` | horodatage |
50
+ | `uuid` | identifiant |
51
+
52
+ **Une relation** s'écrit `ref:<Entité>` :
53
+
54
+ ```bash
55
+ npx nodefony create entity Comment body:text ref:Article
56
+ ```
57
+
58
+ La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`.
59
+ Les clés étrangères ne sont pas émises : une contrainte déclarée dans le `CREATE TABLE`
60
+ n'atteindrait jamais une base déjà en place. C'est le domaine des migrations.
61
+
62
+ **Un index de table** porte plusieurs colonnes, et c'est le seul à le pouvoir :
63
+
64
+ ```bash
65
+ npx nodefony create entity Visit siteId:uuid path:string at:date --index "siteId,at" --unique "siteId,path"
66
+ ```
67
+
68
+ Les deux options sont **répétables** — un couple par index. Sur un schéma réel, la majorité des
69
+ index utiles sont composites : c'est ainsi qu'une table est réellement interrogée.
70
+
71
+ ## Épouser une table qui existe déjà
72
+
73
+ Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
74
+
75
+ ```bash
76
+ npx nodefony create entity Session token:string! --table user_sessions --column-case snake --id-name session_id
77
+ ```
78
+
79
+ Faire suivre le TypeScript aurait transformé un réglage de nommage en refonte : le service, le
80
+ controller, le tri par défaut et les tests générés nomment tous la propriété, pas la colonne.
81
+
82
+ ## Toute lecture de liste se BORNE
83
+
84
+ Avant le format, la règle qui décide si l'application tient en production : **un `find` sans
85
+ borne matérialise la table ENTIÈRE.** Indolore sur les quelques lignes du poste de développement,
86
+ fatal sur les dizaines de milliers de la production — et le code est identique dans les deux cas,
87
+ donc rien ne prévient.
88
+
89
+ Le service d'une entité hérite `findPage({ limit: 25 })` : il ne charge que **`limit + 1`** lignes
90
+ et rend `{ items, hasNext }` — la ligne excédentaire est ce qui répond « il en reste », sans
91
+ compter la table. Sinon `find(criteria, { limit })`.
92
+
93
+ Il te faut une projection de colonnes, une CTE, une agrégation ? Descends au natif **avec son
94
+ type** :
95
+
96
+ ```ts
97
+ import type { DrizzleDb } from "@nodefony/drizzle";
98
+ const db = orm.getNativeConnection<DrizzleDb>();
99
+ ```
100
+
101
+ Sans le paramètre de type tu reçois `unknown`, et il ne te reste qu'un `as any` — que le contrôle
102
+ refuse.
103
+
104
+ ## La liste rend une PAGE — et il n'y a qu'un dialecte
105
+
106
+ La route de liste ne rend pas un tableau : elle rend
107
+ `{ items, limit, offset, hasNext, total? }`. Un tableau ne dit pas s'il en reste — le client qui
108
+ reçoit 25 lignes ne peut pas distinguer « c'est tout » de « demande la suite ».
109
+
110
+ Quatre paramètres, les mêmes **partout** dans Nodefony (tes routes, celles du framework, la console
111
+ d'administration) :
112
+
113
+ | Paramètre | Exemple | Effet |
114
+ | ----------------- | ------------------------------ | ------------------------------------------------------------------ |
115
+ | `limit` | `?limit=50` | taille de page, bornée par le plafond de la route |
116
+ | `offset` | `?offset=100` | décalage |
117
+ | **`order`** | `?order=createdAt:DESC,id:ASC` | tri, plusieurs champs, sens explicite |
118
+ | `withTotal=false` | `?withTotal=false` | économise le `COUNT(*)` quand on n'affiche pas les numéros de page |
119
+
120
+ **Un champ non triable est refusé par un 400**, jamais accepté puis ignoré : une page rendue dans
121
+ un ordre qui n'est pas celui demandé, sans un mot, est un mensonge que personne ne voit. Les champs
122
+ acceptés sont la constante `SORTABLE` en tête du controller généré — c'est là qu'on en ajoute ou
123
+ qu'on en retire un.
124
+
125
+ > 🔴 **N'écris JAMAIS ton propre lecteur de `limit`/`offset`/`sort`.** `parsePageQuery` (exporté par
126
+ > `nodefony`) est LE traducteur : il lit tout d'un coup et applique l'allowlist. Deux dialectes dans
127
+ > une même application divergent, et c'est le client qui l'apprend. Pire, **deux appels dans le
128
+ > MÊME handler** dont un seul connaît l'allowlist font refuser en 400 ce que l'autre vient
129
+ > d'accepter — aucun test unitaire ne le voit, chaque appel étant correct isolément.
130
+
131
+ ```ts
132
+ const page = parsePageQuery(query, {
133
+ defaultLimit: 25,
134
+ maxLimit: 100,
135
+ sortable: SORTABLE,
136
+ });
137
+ ```
138
+
139
+ Ce contrat vaut aussi quand tu écris une liste **à la main** (un endpoint d'administration, un
140
+ listing filtré) : le côté serveur déclare ce qu'il sait trier, le point d'entrée le demande, et le
141
+ refus tombe tout seul.
142
+
143
+ ## Les trois vérités à savoir avant de livrer
144
+
145
+ 1. **La table naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
146
+ 2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
147
+ créée n'apparaîtra pas dans une base existante.
148
+ 3. **La production ne fabrique JAMAIS le schéma.** Elle l'applique par des migrations, écrites par
149
+ `npx nodefony orm:generate` et posées par `npx nodefony orm:migrate` — c'est un geste à part,
150
+ avec ses refus et ses interdits : skill **`nodefony-migrate-schema`**.
151
+
152
+ ## Ce qui refuse AVANT d'écrire
153
+
154
+ Le générateur s'arrête plutôt que de produire un fichier bancal — lis le message, il nomme le
155
+ geste :
156
+
157
+ - **hors projet** (aucun `nodefony.config.ts` au-dessus) ;
158
+ - **`@nodefony/drizzle` absent** de la cible ;
159
+ - **entité déjà déclarée** ;
160
+ - **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
161
+ homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
162
+ colonne inconnue. **`User` fait exception** : l'identité appartient à l'application, et
163
+ `create entity User firstName:string(100)?` écrit l'entité avec les colonnes du contrat plus
164
+ les tiennes. Trois refus s'y appliquent alors — renommer la table ou changer la casse des
165
+ colonnes (des requêtes les écrivent en dur), la poser ailleurs que dans l'application racine
166
+ (l'ordre de chargement n'y est pas garanti), et déclarer un champ obligatoire **sans valeur par
167
+ défaut** (le framework crée des utilisateurs sans le connaître : le semis d'administrateur
168
+ échouerait, et sans code d'erreur). Ni service ni contrôleur générique ne sont produits — une
169
+ ressource REST publique sur l'annuaire serait une faille, et `UserService` existe déjà ;
170
+ - **colonne inconnue, répétée, ou implicite absente** (`createdAt` sans horodatages).
171
+
172
+ ## La suppression naît gardée — vérifie-le
173
+
174
+ Si `@nodefony/security` est dans les dépendances, l'action de suppression porte
175
+ `@IsGranted("ROLE_ADMIN")`. **Sans le module, elle n'est protégée par rien**, et le commentaire du
176
+ fichier généré le dit. Mesuré sur une application réelle avant correction : le CRUD répondait
177
+ **204 à un DELETE anonyme**.
178
+
179
+ Pour la protéger : → skill `nodefony-protect-route`.
180
+
181
+ ## Prouver
182
+
183
+ ```bash
184
+ npm run build # le code généré compile-t-il ?
185
+ npm test # les tests générés couvrent la couche donnée
186
+ npx nodefony doctor # câblage : entité orpheline, service non listé, route en :param
187
+ npx nodefony inspect entities # ce que l'application enregistre VRAIMENT
188
+ ```
189
+
190
+ `npm test` est le premier diagnostic, jamais le dernier geste.
191
+
192
+ ## Voisins
193
+
194
+ | Besoin | Skill |
195
+ | ---------------------------------------- | ------------------------------- |
196
+ | Un service métier injectable | `nodefony-add-service` |
197
+ | Faire suivre une base DÉJÀ en place | `nodefony-migrate-schema` |
198
+ | Réserver une route à certaines personnes | `nodefony-protect-route` |
199
+ | Un flux temps réel | `nodefony-add-realtime-channel` |