@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.
- package/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# @nodefony/devkit
|
|
2
|
+
|
|
3
|
+
L'outillage de **développement** d'une application Nodefony : sa carte de visite,
|
|
4
|
+
les **skills d'agent** qui disent comment faire les tâches courantes, et les
|
|
5
|
+
portes qui mènent au reste.
|
|
6
|
+
|
|
7
|
+
Il répond à la question que tout le monde se pose en arrivant sur une application
|
|
8
|
+
— humain qui reprend un projet, agent qui code : **qui répond ici, et où faut-il
|
|
9
|
+
aller ensuite ?**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx nodefony card # -j pour du JSON (| jq)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> La commande est servie par le **cœur**, pas par ce module : elle doit répondre
|
|
16
|
+
> sur une application pas encore construite et dans un terminal sans `NODE_ENV`,
|
|
17
|
+
> deux cas où aucun module n'est chargé. Ce paquet, lui, sert la même carte en
|
|
18
|
+
> **HTTP** — et c'est la seule porte qui connaisse les modules réellement
|
|
19
|
+
> CHARGÉS.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ma-boutique 1.4.0 — development (nodefony 10.0.0)
|
|
23
|
+
|
|
24
|
+
Modules chargés (7) : drizzle, framework, frontend, http, security, studio, user
|
|
25
|
+
|
|
26
|
+
Où aller :
|
|
27
|
+
AGENTS.md
|
|
28
|
+
Les instructions de cette application — générateurs disponibles, table
|
|
29
|
+
tâche → fichier, gates à passer. À lire AVANT d'écrire du code.
|
|
30
|
+
node_modules/nodefony/docs/catalogue.md
|
|
31
|
+
Le catalogue des briques — quel module prendre pour quel besoin.
|
|
32
|
+
…
|
|
33
|
+
|
|
34
|
+
Quoi lancer :
|
|
35
|
+
npx nodefony doctor
|
|
36
|
+
diagnostic STATIQUE : il répond même quand l'application ne démarre plus.
|
|
37
|
+
…
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
`nodefony create app` l'ajoute déjà — en **`devDependencies`**, et déclaré
|
|
43
|
+
`policy: "dev"` :
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// nodefony.config.ts
|
|
47
|
+
use("@nodefony/devkit", {}, { policy: "dev" }),
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Les deux moitiés comptent : la `devDependency` fait qu'un `npm ci --omit=dev` ne
|
|
51
|
+
l'installe pas ; la `policy` fait qu'un déploiement qui installerait tout ne le
|
|
52
|
+
charge pas quand même. **En production, le module n'est même pas importé** — le
|
|
53
|
+
coût y est nul, pas « faible ».
|
|
54
|
+
|
|
55
|
+
Corollaire : **la route** n'existe que hors production. La **commande**, elle,
|
|
56
|
+
répond toujours — elle ne dépend pas de ce module.
|
|
57
|
+
|
|
58
|
+
## Ce qu'il expose
|
|
59
|
+
|
|
60
|
+
| Porte | Pour qui |
|
|
61
|
+
| ------------------------------- | ------------------------------------------------------------ |
|
|
62
|
+
| `GET /nodefony/devkit/api/card` | Studio, un script authentifié — modules réellement CHARGÉS |
|
|
63
|
+
| **`POST /nodefony/mcp`** | **un agent qui appelle des outils** (Model Context Protocol) |
|
|
64
|
+
| `buildCard()` (export du cœur) | une porte de plus, à écrire — rien à réimplémenter |
|
|
65
|
+
| `npx nodefony card` | un agent, un humain au terminal — **servie par le cœur** |
|
|
66
|
+
| `skills/` (dossier du paquet) | l'agent de codage que vous utilisez déjà — voir ci-dessous |
|
|
67
|
+
|
|
68
|
+
La route de la carte vit sous `/nodefony/<module>/api`, que le pare-feu d'une
|
|
69
|
+
application réelle couvre : un agent qui code ne s'authentifie pas et n'a pas de
|
|
70
|
+
navigateur — d'où la commande, qui reste la porte utile.
|
|
71
|
+
|
|
72
|
+
## Le serveur MCP — les mêmes réponses, en outils
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Quatre outils : **`nodefony_inspect`** (ce qui est monté), **`nodefony_check`**
|
|
79
|
+
(ce qui manque), **`nodefony_symbols`** (ce qu'une API du framework signifie),
|
|
80
|
+
**`nodefony_card`** (par où commencer). Ce sont les mêmes briques que les
|
|
81
|
+
commandes du même nom — une source, plusieurs portes.
|
|
82
|
+
|
|
83
|
+
**Ce n'est pas un process de plus.** Depuis la révision `2026-07-28` du transport
|
|
84
|
+
« Streamable HTTP », un serveur MCP est un simple endpoint `POST` sans session :
|
|
85
|
+
c'est donc une **route de votre application**. Elle n'existe que pendant qu'elle
|
|
86
|
+
tourne, suit chaque rechargement du serveur de développement, et n'a aucun cache
|
|
87
|
+
à invalider.
|
|
88
|
+
|
|
89
|
+
**Ce qui la protège**, et il faut le savoir avant de s'étonner d'un `403` :
|
|
90
|
+
|
|
91
|
+
- toute **adresse non locale** est refusée (`mcp.allowRemote`) ;
|
|
92
|
+
- toute **origine de navigateur** non déclarée est refusée (`mcp.allowedOrigins`).
|
|
93
|
+
Un client MCP natif n'envoie pas d'en-tête `Origin` ; une page web en envoie
|
|
94
|
+
toujours un. C'est ce qui ferme le détournement DNS, seul vecteur réel contre
|
|
95
|
+
un serveur local ;
|
|
96
|
+
- le module étant `policy: "dev"`, **la route n'existe pas en production** ;
|
|
97
|
+
- les outils sont en **lecture seule**, et la liste est une allowlist
|
|
98
|
+
(`mcp.tools`).
|
|
99
|
+
|
|
100
|
+
### Protéger la porte par OAuth 2.1
|
|
101
|
+
|
|
102
|
+
Par défaut la porte est anonyme, et ce qui la borne est le périmètre ci-dessus.
|
|
103
|
+
Déclarez un serveur d'autorisation, et elle prend son rôle de **resource
|
|
104
|
+
server** : elle publie ses métadonnées, valide le porteur présenté, et refuse en
|
|
105
|
+
disant où obtenir un jeton.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
use("@nodefony/devkit", {
|
|
109
|
+
mcp: {
|
|
110
|
+
authorization: {
|
|
111
|
+
// Un seul réglage commande : vide = porte anonyme, comme avant.
|
|
112
|
+
authorizationServers: ["https://auth.example"],
|
|
113
|
+
// L'URI PUBLIQUE de la porte — l'audience que les jetons doivent porter.
|
|
114
|
+
// Elle s'écrit : la déduire de l'en-tête `Host` permettrait à un Host
|
|
115
|
+
// forgé d'obtenir un jeton d'audience arbitraire ET de passer la
|
|
116
|
+
// vérification.
|
|
117
|
+
resource: "https://mon-app.example/nodefony/mcp",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> **Les scopes ne s'écrivent pas ici.** Ce que la porte publie
|
|
124
|
+
> (`scopes_supported`) et nomme dans son défi est **dérivé** des outils qu'elle
|
|
125
|
+
> sert : l'union de leurs `IMcpTool.scopes`. Pour qu'un scope soit publié, le
|
|
126
|
+
> poser sur l'outil qu'il ouvre — le seul endroit où il a un effet. Une liste
|
|
127
|
+
> écrite à côté du code annonçait au client autre chose que ce qu'on exige de
|
|
128
|
+
> lui, dans les deux sens et sans qu'aucun contrôle ne s'en aperçoive.
|
|
129
|
+
|
|
130
|
+
Le document est alors servi sur
|
|
131
|
+
`GET /.well-known/oauth-protected-resource/nodefony/mcp` (RFC 9728 — le suffixe
|
|
132
|
+
s'**insère** entre l'hôte et le chemin, ce qui permet plusieurs ressources par
|
|
133
|
+
hôte), et une requête sans jeton reçoit :
|
|
134
|
+
|
|
135
|
+
```http
|
|
136
|
+
HTTP/1.1 401 Unauthorized
|
|
137
|
+
WWW-Authenticate: Bearer resource_metadata="https://mon-app.example/.well-known/oauth-protected-resource/nodefony/mcp", scope="admin:read"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
C'est cet en-tête qui rend l'autorisation **apprenable** : sans lui, un refus
|
|
141
|
+
est un mur — le client ignore qu'un jeton existe et où le demander.
|
|
142
|
+
|
|
143
|
+
> 🔴 **La vérification du jeton est fournie par l'application.** Ce module est
|
|
144
|
+
> `policy: "dev"` et ne porte aucune cryptographie : il cherche un service
|
|
145
|
+
> `accessTokenVerifier` dans le conteneur (contrat `IAccessTokenVerifier` du cœur).
|
|
146
|
+
> Déclarer un serveur d'autorisation **sans** ce service fait répondre `503` à
|
|
147
|
+
> la porte, avec un journal `CRITIC` — accepter des porteurs sans les lire
|
|
148
|
+
> serait pire que rester anonyme.
|
|
149
|
+
>
|
|
150
|
+
> `anonymous: true` laisse la porte ouverte aux outils publics tout en gardant
|
|
151
|
+
> les outils réservés retenus. C'est un choix qui s'écrit, jamais un défaut.
|
|
152
|
+
|
|
153
|
+
Le serveur est **dual-ère** : il répond à `server/discover` et aux métadonnées
|
|
154
|
+
par requête des clients modernes, **et** au handshake `initialize` des clients
|
|
155
|
+
déployés aujourd'hui — ce que la spec autorise explicitement. Il sert cinq
|
|
156
|
+
révisions (`2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26`,
|
|
157
|
+
`2024-11-05`) et **répond celle que le client demande** : annoncer la plus
|
|
158
|
+
récente à tous rendrait la porte injoignable par les clients dont le SDK ne la
|
|
159
|
+
connaît pas encore.
|
|
160
|
+
|
|
161
|
+
Éprouvé avec deux clients indépendants : Claude Code et Mistral Vibe.
|
|
162
|
+
|
|
163
|
+
### Vos propres outils
|
|
164
|
+
|
|
165
|
+
Ces quatre-là décrivent le framework ; ils ne savent rien de votre métier. Tout
|
|
166
|
+
module de votre application peut publier les siens en implémentant
|
|
167
|
+
`getMcpTools(): IMcpTool[]` :
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { Module, mcpText, type IMcpTool } from "nodefony";
|
|
171
|
+
|
|
172
|
+
class Shop extends Module {
|
|
173
|
+
getMcpTools(): IMcpTool[] {
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
name: "shop_stock",
|
|
177
|
+
description:
|
|
178
|
+
"Stock réel d'une référence produit. À utiliser avant de proposer " +
|
|
179
|
+
"une commande — la réponse vient de la base, pas d'un cache.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: { sku: { type: "string" } },
|
|
183
|
+
required: ["sku"],
|
|
184
|
+
},
|
|
185
|
+
handler: async (args) => mcpText(await this.stock(String(args.sku))),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Rien ne s'enregistre au démarrage : la liste est relue à chaque requête, et
|
|
193
|
+
`mcp.tools` ne filtre que les outils **intégrés** — le vôtre est publié dès qu'il
|
|
194
|
+
est déclaré. Un outil écarté (nom hors forme, nom déjà pris, handler absent) le
|
|
195
|
+
dit en `WARNING`, jamais en silence.
|
|
196
|
+
|
|
197
|
+
Un outil peut aussi se **réserver** — `scopes: ["shop:read"]` (tous exigés) ou
|
|
198
|
+
`requiresAuth: true` — et son handler reçoit alors l'appelant en second
|
|
199
|
+
paramètre. Il est alors absent de `tools/list` **et** inappelable en le nommant.
|
|
200
|
+
🔴 Tant que la porte n'authentifie personne, un tel outil ne sortira jamais :
|
|
201
|
+
c'est fermé par défaut, et c'est voulu. Détail et pièges :
|
|
202
|
+
[la documentation du module](./docs/index.md).
|
|
203
|
+
|
|
204
|
+
## Les skills d'agent
|
|
205
|
+
|
|
206
|
+
Le paquet livre six **skills** au format [Agent Skills](https://agentskills.io)
|
|
207
|
+
— la marche à suivre complète pour les tâches où un agent, sans eux, inventerait
|
|
208
|
+
du code : créer une ressource REST, ajouter un service injectable, réserver une
|
|
209
|
+
route à qui est habilité, ouvrir un canal temps réel, faire évoluer le schéma
|
|
210
|
+
d'une base sans la détruire, et voir puis MESURER un écran dans un navigateur
|
|
211
|
+
piloté. Ils sont lus par tout client conforme (Claude Code, Cursor, Copilot,
|
|
212
|
+
VS Code, Codex, Goose…).
|
|
213
|
+
|
|
214
|
+
`nodefony create app` les met à disposition à la création. Après un
|
|
215
|
+
`npm update`, une commande les remet à jour :
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx nodefony ai:sync # --dry-run pour voir sans écrire, --json pour un script
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
Skills d'agent — .agents/skills
|
|
223
|
+
|
|
224
|
+
= nodefony-add-crud @nodefony/devkit
|
|
225
|
+
= nodefony-add-realtime-channel @nodefony/devkit
|
|
226
|
+
= nodefony-add-service @nodefony/devkit
|
|
227
|
+
= nodefony-browser @nodefony/devkit
|
|
228
|
+
= nodefony-migrate-schema @nodefony/devkit
|
|
229
|
+
= nodefony-protect-route @nodefony/devkit
|
|
230
|
+
|
|
231
|
+
0 posé(s) · 0 mis à jour · 6 inchangé(s)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Le préfixe `nodefony-` vous laisse la place.** Ces pointeurs arrivent dans
|
|
235
|
+
`.agents/skills/`, le dossier où vous écrivez aussi les vôtres : sans namespace,
|
|
236
|
+
un `add-crud` propre à votre métier et le nôtre se disputeraient un nom — et
|
|
237
|
+
c'est `ai:sync` qui écraserait le vôtre à la synchronisation suivante. Vos skills
|
|
238
|
+
n'ont donc aucune contrainte de nommage, sauf à commencer par `nodefony-`.
|
|
239
|
+
|
|
240
|
+
**Ce qui est écrit chez vous est un POINTEUR, pas une copie.** Le contenu reste
|
|
241
|
+
dans le paquet et suit vos montées de version ; un skill recopié dans le projet
|
|
242
|
+
décrirait, six mois plus tard, un framework qui a changé — sans casser le build,
|
|
243
|
+
donc sans que personne le voie. Les pointeurs sont faits pour être **commités** :
|
|
244
|
+
votre équipe et votre intégration continue disposent alors des mêmes skills.
|
|
245
|
+
|
|
246
|
+
Le dossier visé, `.agents/skills/`, est celui que **tous** les clients conformes
|
|
247
|
+
lisent — pas le dossier propriétaire d'un seul. Si le vôtre ne scanne que le
|
|
248
|
+
sien, ajoutez-y ce chemin plutôt que de dupliquer un contenu qui divergerait.
|
|
249
|
+
|
|
250
|
+
Deux garanties de la commande : un pointeur déjà à jour n'est **pas réécrit**
|
|
251
|
+
(votre arbre git reste propre, l'horodatage ne bouge pas), et un pointeur que
|
|
252
|
+
plus aucun paquet ne livre est **signalé, jamais supprimé** — vous avez pu en
|
|
253
|
+
écrire un à la main sous le même nom.
|
|
254
|
+
|
|
255
|
+
> **Aucun `postinstall`** ne fait ce geste, volontairement : `--ignore-scripts`
|
|
256
|
+
> est courant, les scripts d'installation sont un vecteur d'attaque connu de
|
|
257
|
+
> l'écosystème npm, et écrire dans un dossier versionné à chaque installation
|
|
258
|
+
> produirait des différences surprises. La commande, elle, se lance quand vous
|
|
259
|
+
> le décidez. Un module tiers qui livre ses propres skills est servi par la même
|
|
260
|
+
> commande : elle scanne tout paquet `@nodefony/*` **et** les modules locaux de
|
|
261
|
+
> l'application, sans que le cœur ait à les connaître.
|
|
262
|
+
|
|
263
|
+
## Configuration
|
|
264
|
+
|
|
265
|
+
| Clé | Type | Défaut | Rôle |
|
|
266
|
+
| --------- | --------- | ------ | ---------------------- |
|
|
267
|
+
| `enabled` | `boolean` | `true` | Interrupteur du module |
|
|
268
|
+
|
|
269
|
+
La source unique est le schéma Zod de `nodefony/config/config.ts` : c'est lui qui
|
|
270
|
+
porte les défauts, les descriptions et la validation. Une clé inconnue ou mal
|
|
271
|
+
typée fait échouer le **boot**, en nommant le champ fautif.
|
|
272
|
+
|
|
273
|
+
Surcharge par l'application : `use("@nodefony/devkit", { enabled: false })`.
|
|
274
|
+
Par l'environnement : `NF__DEVKIT__ENABLED=false`.
|
|
275
|
+
|
|
276
|
+
## Ce qu'il ne fait pas
|
|
277
|
+
|
|
278
|
+
- **Il n'invente rien.** Tout ce qu'il rend est DÉRIVÉ de l'état du Kernel,
|
|
279
|
+
recalculé à chaque lecture, jamais mis en cache — une carte en cache mentirait
|
|
280
|
+
au premier module ajouté.
|
|
281
|
+
- **Il ne crée rien.** Le scaffold (`nodefony create …`), le diagnostic
|
|
282
|
+
(`nodefony doctor`) et l'introspection (`nodefony inspect`) vivent dans le
|
|
283
|
+
cœur : ils doivent répondre sans qu'aucun module soit installé, et quand
|
|
284
|
+
l'application est cassée.
|
|
285
|
+
- **Il ne dépend d'aucun fournisseur de modèle.** Son intérêt est de servir
|
|
286
|
+
l'agent que vous avez déjà.
|
|
287
|
+
- **Il ne s'installe pas tout seul dans votre projet.** Aucun `postinstall` :
|
|
288
|
+
les pointeurs de skills sont posés par `create app` à la création, et remis à
|
|
289
|
+
jour par `ai:sync` quand vous le demandez.
|
|
290
|
+
|
|
291
|
+
## Développer
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npm run build # rolldown → dist/ + déclarations .d.ts
|
|
295
|
+
npm run typecheck # tsgo --noEmit (sources + tests)
|
|
296
|
+
npm test # vitest
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Structure
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
@nodefony/devkit/
|
|
303
|
+
├── index.ts ← la classe Module + les exports publics
|
|
304
|
+
├── nodefony/
|
|
305
|
+
│ ├── config/config.ts ← schéma Zod = source unique des défauts
|
|
306
|
+
│ ├── config/defineModuleConfig.ts ← builder pur (valide, gèle)
|
|
307
|
+
│ ├── src/card.ts ← ré-export du cœur (la composition y vit)
|
|
308
|
+
│ ├── service/DevkitService.ts ← dérive la carte du Kernel (`container.get("devkit")`)
|
|
309
|
+
│ ├── controllers/DevkitController.ts ← la porte HTTP
|
|
310
|
+
│ └── interfaces/ ← l'API publique du service
|
|
311
|
+
├── skills/<nom>/SKILL.md ← les skills d'agent livrés par npm
|
|
312
|
+
├── docs/ ← documentation, surfacée dans Studio
|
|
313
|
+
└── tests/
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`dist/`, `docs/` et `skills/` sont les trois dossiers publiés (`files`) : un
|
|
317
|
+
skill se corrige ici, et la correction arrive chez l'utilisateur par
|
|
318
|
+
`npm update`, sans qu'il ait un fichier à réécrire.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
2
|
+
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
3
|
+
import { buildCard } from "./nodefony/src/card.js";
|
|
4
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
6
|
+
import DevkitService_default from "./nodefony/service/DevkitService.js";
|
|
7
|
+
import DevkitController_default from "./nodefony/controllers/DevkitController.js";
|
|
8
|
+
import McpController_default from "./nodefony/controllers/McpController.js";
|
|
9
|
+
import { Kernel, Module, services } from "nodefony";
|
|
10
|
+
import { controllers } from "@nodefony/framework";
|
|
11
|
+
//#region index.ts
|
|
12
|
+
let DevkitModule = class DevkitModule extends Module {
|
|
13
|
+
/**
|
|
14
|
+
* ⚠️ Ce module ne pose PLUS de commande CLI.
|
|
15
|
+
*
|
|
16
|
+
* `devkit:card` vivait ici, et n'existait donc que lorsque le module était
|
|
17
|
+
* chargé : hors développement (`policy: "dev"`) le CLI répondait
|
|
18
|
+
* `unknown command`, et sur une application non encore construite le Kernel
|
|
19
|
+
* refusait de démarrer avant elle. Une carte de visite qui disparaît selon
|
|
20
|
+
* l'environnement n'accueille personne. Elle est désormais servie par le cœur
|
|
21
|
+
* (`nodefony card`, alias `devkit:card`, standalone 0-boot — fast-path de
|
|
22
|
+
* `CliKernel.start`), qui ne lit que des fichiers.
|
|
23
|
+
*
|
|
24
|
+
* Ce module garde la porte HTTP : elle, et elle seule, connaît les modules
|
|
25
|
+
* réellement CHARGÉS.
|
|
26
|
+
*/
|
|
27
|
+
constructor(kernel) {
|
|
28
|
+
super("devkit", kernel, import.meta.url, defaults);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Valide la config au boot — défauts du schéma fusionnés avec ce que l'app
|
|
32
|
+
* passe dans `use()`. Une clé inconnue ou mal typée plante ICI, avec le champ
|
|
33
|
+
* fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
|
|
34
|
+
*/
|
|
35
|
+
async onKernelRegister() {
|
|
36
|
+
this.options = defineDevkitConfig(this.options ?? {});
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
DevkitModule = __decorate([
|
|
41
|
+
controllers([DevkitController_default, McpController_default]),
|
|
42
|
+
services([DevkitService_default]),
|
|
43
|
+
__decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])
|
|
44
|
+
], DevkitModule);
|
|
45
|
+
var devkit_default = DevkitModule;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { DevkitController_default as DevkitController, DevkitService_default as DevkitService, McpController_default as McpController, buildCard, devkit_default as default, defineDevkitConfig, devkitConfigSchema };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command } from "nodefony";
|
|
2
|
+
//#region nodefony/command/CardCommand.ts
|
|
3
|
+
/**
|
|
4
|
+
* `onReady` : les services sont construits, AUCUN serveur n'écoute. La carte se
|
|
5
|
+
* lit dans l'état du kernel — ouvrir un port pour la rendre serait payer un
|
|
6
|
+
* démarrage complet pour une réponse qui n'en dépend pas.
|
|
7
|
+
*/
|
|
8
|
+
const options = {
|
|
9
|
+
showBanner: false,
|
|
10
|
+
kernelEvent: "onReady"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* `nodefony devkit:card [-j]` — qui répond, et où aller ensuite.
|
|
14
|
+
*
|
|
15
|
+
* ## Pourquoi une commande, alors que la route existe
|
|
16
|
+
*
|
|
17
|
+
* La route HTTP vit sous `/nodefony`, que le pare-feu d'une application réelle
|
|
18
|
+
* couvre : un agent qui code ne s'authentifie pas, et n'a pas de navigateur. La
|
|
19
|
+
* porte qu'il a déjà, c'est le terminal. Même source (le service), deux rendus —
|
|
20
|
+
* ajouter une porte n'ajoute jamais une vérité.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ Le module est `policy: "dev"` : hors développement il n'est pas chargé, donc
|
|
23
|
+
* cette commande **n'existe pas**. C'est voulu — et c'est pour ça qu'elle
|
|
24
|
+
* s'invoque `NODE_ENV=development npx nodefony devkit:card` depuis un terminal
|
|
25
|
+
* qui n'aurait pas posé la variable.
|
|
26
|
+
*/
|
|
27
|
+
var CardCommand = class CardCommand extends Command {
|
|
28
|
+
constructor(cli) {
|
|
29
|
+
super("devkit:card", "Imprime la carte de visite de l application", cli, options);
|
|
30
|
+
this.addOption("-j, --json", "sortie JSON brute (scriptable, `| jq`)");
|
|
31
|
+
}
|
|
32
|
+
async generate(opts) {
|
|
33
|
+
const svc = this.kernel?.container?.get("devkit");
|
|
34
|
+
if (!svc) {
|
|
35
|
+
this.log("service « devkit » non enregistré — module non chargé (policy dev) ?", "ERROR");
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
const card = svc.getCard();
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
process.stdout.write(`${JSON.stringify(card, null, 2)}\n`);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(CardCommand.format(card));
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rend la carte pour un HUMAIN (ou un agent qui lit un terminal).
|
|
48
|
+
*
|
|
49
|
+
* Statique et PURE : elle ne touche ni au kernel ni au service, donc elle
|
|
50
|
+
* s'éprouve seule. Sortie sur `stdout` plutôt que par le journal — une carte
|
|
51
|
+
* de visite n'est pas un événement de log, et le préfixe horodaté rendrait le
|
|
52
|
+
* copier-coller inutilisable.
|
|
53
|
+
*/
|
|
54
|
+
static format(card) {
|
|
55
|
+
return [
|
|
56
|
+
`${card.app.name} ${card.app.version} — ${card.app.environment} (nodefony ${card.nodefony.version})`,
|
|
57
|
+
"",
|
|
58
|
+
`Modules chargés (${card.modules.length}) : ${card.modules.join(", ")}`,
|
|
59
|
+
"",
|
|
60
|
+
"Où aller :",
|
|
61
|
+
...card.portes.map((p) => ` ${p.ou}\n ${p.titre} — ${p.pourquoi}`),
|
|
62
|
+
"",
|
|
63
|
+
"Quoi lancer :",
|
|
64
|
+
...card.verbes.map((v) => ` ${v.commande}\n ${v.pourquoi}`),
|
|
65
|
+
""
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { CardCommand as default };
|