@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
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extrait les erreurs du journal d'une application Nodefony — regroupées, datées, avec le geste qui suit.
4
+ *
5
+ * Un journal de développement fait vite des milliers de lignes, dont l'immense
6
+ * majorité dit que tout va bien. Le lire en entier noie l'erreur ; le couper
7
+ * (`tail`) la coupe aussi, sans le dire. Ce script lit TOUT, ne garde que les
8
+ * sévérités qui comptent, recolle une pile d'appels à la ligne qui l'a levée,
9
+ * fusionne les répétitions, et nomme le premier geste quand il le connaît.
10
+ *
11
+ * Il lit les deux formes que le framework écrit dans `logs/` : le texte
12
+ * (`nodefony-detached.log` d'un serveur lancé avec `--detach`, `nodefony-<pid>.log`
13
+ * du pilote fichier, ou tout fichier où tu as redirigé la sortie) et le JSON par
14
+ * ligne (`nodefony-<pid>.jsonl`). Un plantage brut de Node — `SyntaxError: …`
15
+ * hors du format du journal — est reconnu aussi.
16
+ *
17
+ * `@usage` node node_modules/@nodefony/devkit/skills/nodefony-troubleshoot/scripts/log-errors.mjs
18
+ * `@usage` node node_modules/@nodefony/devkit/skills/nodefony-troubleshoot/scripts/log-errors.mjs logs/dev.log
19
+ * `@usage` node node_modules/@nodefony/devkit/skills/nodefony-troubleshoot/scripts/log-errors.mjs --warnings --last 40
20
+ * `@option` --warnings - garde aussi les avertissements (WARNING)
21
+ * `@option` --last - nombre d'erreurs distinctes rendues, les plus récentes (défaut 15)
22
+ * `@option` --json - la même réponse, sérialisée, pour rechaîner
23
+ * `@option` --root - racine du projet (défaut : le dossier courant)
24
+ * `@output` les erreurs distinctes, de la plus ancienne à la plus récente, avec fichier, heure, nombre et piste
25
+ *
26
+ * Sorties : `0` aucune erreur · `1` au moins une erreur · `64` usage · `66` aucun journal lisible.
27
+ */
28
+
29
+ import {
30
+ readFileSync,
31
+ readdirSync,
32
+ statSync,
33
+ openSync,
34
+ fstatSync,
35
+ readSync,
36
+ closeSync,
37
+ realpathSync,
38
+ } from "node:fs";
39
+ import path from "node:path";
40
+ import process from "node:process";
41
+ import { fileURLToPath } from "node:url";
42
+ import { findProjectRoot } from "nodefony/project-root";
43
+
44
+ /**
45
+ * Racine de l'APPLICATION vue depuis `cwd` — le dossier qui porte
46
+ * `nodefony.config.ts` —, ou `cwd` lui-même hors application. Un script lancé
47
+ * depuis `frontend/` ou `modules/blog/` travaille ainsi sur l'app entière.
48
+ * `nodefony/project-root` est le sous-chemin LÉGER du cœur (`node:` seul) :
49
+ * importer `nodefony` entier coûtait jusqu'à plusieurs secondes à froid.
50
+ *
51
+ * @param {string} cwd - le dossier de lancement.
52
+ * @returns {string} la racine retenue.
53
+ */
54
+ const appRoot = (cwd) => findProjectRoot(cwd) ?? cwd;
55
+
56
+ /** Erreurs distinctes rendues par défaut — au-delà, personne ne lit. */
57
+ const DEFAULT_LAST = 15;
58
+
59
+ /** Octets relus depuis la FIN d'un journal : un journal de plusieurs Go ne doit pas saturer la mémoire. */
60
+ const MAX_SCAN_BYTES = 8 * 1024 * 1024;
61
+
62
+ /** Sévérités du journal (RFC 5424), de la plus grave à la plus anodine. */
63
+ const SEVERITY = {
64
+ EMERGENCY: 0,
65
+ ALERT: 1,
66
+ CRITIC: 2,
67
+ ERROR: 3,
68
+ WARNING: 4,
69
+ NOTICE: 5,
70
+ INFO: 6,
71
+ DEBUG: 7,
72
+ };
73
+
74
+ /** Codes ANSI de couleur : le journal texte en porte quand il est écrit pour un terminal. */
75
+ const ANSI = /\u001b\[[0-9;]*m/gu;
76
+
77
+ /** Une ligne du journal texte : `HH:MM:SS.mmm SÉVÉRITÉ MSGID : message`. */
78
+ const PDU_LINE =
79
+ /^(\d{2}:\d{2}:\d{2}\.\d{3})\s+(EMERGENCY|ALERT|CRITIC|ERROR|WARNING|NOTICE|INFO|DEBUG)\s+(.+?)\s+:\s?(.*)$/u;
80
+
81
+ /** Une erreur levée HORS du journal — le plantage brut que Node écrit lui-même. */
82
+ const RAW_ERROR =
83
+ /^(?:Uncaught )?((?:[A-Z][A-Za-z]*)?Error)(?: \[[A-Z_]+\])?: (.+)$/u;
84
+
85
+ /** Une ligne de pile d'appels, rattachée à l'erreur qui la précède. */
86
+ const STACK_LINE = /^\s+at\s/u;
87
+
88
+ /**
89
+ * Ce que le message dit, et le premier geste qui en découle.
90
+ *
91
+ * Seulement des causes ÉTABLIES : une piste fausse coûte plus qu'aucune piste,
92
+ * parce qu'on la suit. Ce qui n'est pas ici se lit dans le message lui-même.
93
+ */
94
+ const HINTS = [
95
+ {
96
+ when: /does not provide an export named/u,
97
+ hint: "un `dist/` en retard sur les sources, ou deux copies de `nodefony` : `npm run build`, puis `npm ls nodefony`",
98
+ },
99
+ {
100
+ when: /Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND/u,
101
+ hint: "dépendance ou build manquant : `npm install`, puis `npm run build`",
102
+ },
103
+ {
104
+ when: /BootConfigurationError/u,
105
+ hint: "configuration refusée au démarrage (sortie 78) : le message NOMME la clé — corrige-la, ne la retire pas",
106
+ },
107
+ {
108
+ when: /EADDRINUSE|already in use/u,
109
+ hint: "port déjà tenu : `npx nodefony status` nomme qui le tient (une ligne « retrying » juste après = rattrapé seul)",
110
+ },
111
+ {
112
+ when: /no such table|no such column|does not exist|doesn't exist|Unknown column|migrations? à appliquer/u,
113
+ hint: "schéma de base en retard sur les entités : skill `nodefony-migrate-schema`",
114
+ },
115
+ {
116
+ when: /ECONNREFUSED|ENOTFOUND|getaddrinfo/u,
117
+ hint: "un service extérieur (base, cache, fournisseur) ne répond pas : `npx nodefony doctor --live`",
118
+ },
119
+ {
120
+ when: /^SIG(?:TERM|INT|HUP)$/u,
121
+ hint: "arrêt DEMANDÉ au processus — pas une panne",
122
+ },
123
+ ];
124
+
125
+ const FLAGS = new Set([
126
+ "--warnings",
127
+ "--last",
128
+ "--json",
129
+ "--root",
130
+ "--help",
131
+ "-h",
132
+ ]);
133
+
134
+ /**
135
+ * Lit les arguments, et REFUSE ce qu'il ne comprend pas.
136
+ *
137
+ * Un drapeau mal tapé qu'on ignorerait rendrait « aucune erreur » — la pire
138
+ * réponse possible d'un outil de dépannage.
139
+ *
140
+ * @param argv - les arguments, sans `node` ni le chemin du script.
141
+ * @returns les options lues.
142
+ * @throws Error quand un drapeau est inconnu ou qu'une valeur manque.
143
+ */
144
+ export function parseArgs(argv, cwd = process.cwd()) {
145
+ const opts = {
146
+ files: [],
147
+ warnings: false,
148
+ last: DEFAULT_LAST,
149
+ json: false,
150
+ root: appRoot(cwd),
151
+ help: false,
152
+ };
153
+ for (let i = 0; i < argv.length; i++) {
154
+ const a = argv[i];
155
+ if (!a.startsWith("-")) {
156
+ opts.files.push(a);
157
+ continue;
158
+ }
159
+ if (!FLAGS.has(a)) throw new Error(`option inconnue : ${a}`);
160
+ if (a === "--help" || a === "-h") opts.help = true;
161
+ else if (a === "--warnings") opts.warnings = true;
162
+ else if (a === "--json") opts.json = true;
163
+ else {
164
+ const v = argv[++i];
165
+ if (v === undefined || v.startsWith("-"))
166
+ throw new Error(`${a} attend une valeur`);
167
+ if (a === "--root") opts.root = path.resolve(v);
168
+ else {
169
+ const n = Number(v);
170
+ if (!Number.isInteger(n) || n < 1)
171
+ throw new Error(`--last attend un entier positif, reçu « ${v} »`);
172
+ opts.last = n;
173
+ }
174
+ }
175
+ }
176
+ return opts;
177
+ }
178
+
179
+ /**
180
+ * Heure locale `HH:MM:SS.mmm` d'un horodatage — la forme du journal texte, pour
181
+ * qu'un même événement lu dans ses deux formes se reconnaisse.
182
+ *
183
+ * @param ms - horodatage en millisecondes.
184
+ * @returns l'heure formatée.
185
+ */
186
+ function timeOfDay(ms) {
187
+ const d = new Date(ms);
188
+ const p = (n, w = 2) => String(n).padStart(w, "0");
189
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
190
+ }
191
+
192
+ /**
193
+ * Découpe un journal en événements, quelle que soit sa forme (texte ou JSON par ligne).
194
+ *
195
+ * Les lignes d'une même erreur — même heure, même émetteur, même sévérité, ou
196
+ * pile d'appels qui suit — forment UN événement : une pile de sept lignes est
197
+ * une erreur, pas sept.
198
+ *
199
+ * @param text - le contenu du journal.
200
+ * @param source - le nom du fichier, rendu avec chaque événement.
201
+ * @returns les événements, dans l'ordre du journal.
202
+ */
203
+ export function extractEvents(text, source) {
204
+ const events = [];
205
+ let last = null;
206
+ for (const rawLine of text.split(/\r?\n/u)) {
207
+ const line = rawLine.replace(ANSI, "");
208
+ if (line.trim() === "") continue;
209
+ let entry = null;
210
+ if (line.startsWith("{")) {
211
+ try {
212
+ const o = JSON.parse(line);
213
+ if (typeof o.severityName === "string" && o.severityName in SEVERITY) {
214
+ entry = {
215
+ time:
216
+ typeof o.timeStamp === "number" ? timeOfDay(o.timeStamp) : null,
217
+ severity: o.severityName,
218
+ msgid: String(o.msgid ?? ""),
219
+ message:
220
+ typeof o.payload === "string"
221
+ ? o.payload
222
+ : JSON.stringify(o.payload),
223
+ };
224
+ }
225
+ } catch {
226
+ // Une ligne JSON tronquée (écriture en cours) n'est pas une erreur du journal.
227
+ }
228
+ } else {
229
+ const m = PDU_LINE.exec(line);
230
+ if (m)
231
+ entry = {
232
+ time: m[1],
233
+ severity: m[2],
234
+ msgid: m[3].trim(),
235
+ message: m[4],
236
+ };
237
+ else {
238
+ const r = RAW_ERROR.exec(line.trim());
239
+ if (r)
240
+ entry = {
241
+ time: null,
242
+ severity: "ERROR",
243
+ msgid: "node",
244
+ message: line.trim(),
245
+ };
246
+ else if (last && STACK_LINE.test(line)) {
247
+ last.detail.push(line.trim());
248
+ continue;
249
+ }
250
+ }
251
+ }
252
+ if (entry === null) {
253
+ last = null;
254
+ continue;
255
+ }
256
+ const sameError =
257
+ last !== null &&
258
+ entry.time !== null &&
259
+ last.time === entry.time &&
260
+ last.msgid === entry.msgid &&
261
+ last.severity === entry.severity;
262
+ if (sameError) {
263
+ last.detail.push(entry.message);
264
+ continue;
265
+ }
266
+ last = { ...entry, source, detail: [] };
267
+ events.push(last);
268
+ }
269
+ return events;
270
+ }
271
+
272
+ /**
273
+ * Garde ce qui compte et fusionne les répétitions.
274
+ *
275
+ * Deux événements identiques (même sévérité, émetteur et message) n'en font
276
+ * qu'un, compté — qu'ils viennent d'un même fichier ou des deux formes du même
277
+ * journal. L'ordre rendu est chronologique, par dernière occurrence.
278
+ *
279
+ * @param events - les événements extraits.
280
+ * @param options - `warnings` garde aussi les avertissements.
281
+ * @returns les erreurs distinctes, chacune avec son nombre, sa première et sa dernière heure, et sa piste.
282
+ */
283
+ export function summarize(events, { warnings = false } = {}) {
284
+ const ceiling = warnings ? SEVERITY.WARNING : SEVERITY.ERROR;
285
+ const byKey = new Map();
286
+ const seen = new Set();
287
+ for (const e of events) {
288
+ if (SEVERITY[e.severity] > ceiling) continue;
289
+ // Le même événement lu dans ses deux formes (texte ET JSON) ne compte qu'une fois.
290
+ const occurrence = `${e.time}|${e.severity}|${e.msgid}|${e.message}`;
291
+ if (e.time !== null && seen.has(occurrence)) continue;
292
+ seen.add(occurrence);
293
+ const key = `${e.severity}|${e.msgid}|${e.message}`;
294
+ const known = byKey.get(key);
295
+ if (known) {
296
+ known.count++;
297
+ known.lastTime = e.time ?? known.lastTime;
298
+ byKey.delete(key);
299
+ byKey.set(key, known);
300
+ continue;
301
+ }
302
+ const text = [e.message, ...e.detail].join("\n");
303
+ byKey.set(key, {
304
+ severity: e.severity,
305
+ msgid: e.msgid,
306
+ message: e.message,
307
+ detail: e.detail,
308
+ source: e.source,
309
+ count: 1,
310
+ firstTime: e.time,
311
+ lastTime: e.time,
312
+ hint: HINTS.find((h) => h.when.test(text))?.hint ?? null,
313
+ });
314
+ }
315
+ return [...byKey.values()];
316
+ }
317
+
318
+ /**
319
+ * Lit la fin d'un fichier — tout, s'il est sous le plafond.
320
+ *
321
+ * @param file - chemin du fichier.
322
+ * @returns le texte relu.
323
+ */
324
+ function readTail(file) {
325
+ // Une seule ouverture : taille et lecture sur le MÊME descripteur — le chemin
326
+ // peut changer entre un `stat` et un `read` (js/file-system-race).
327
+ const fd = openSync(file, "r");
328
+ try {
329
+ const size = fstatSync(fd).size;
330
+ if (size <= MAX_SCAN_BYTES) return readFileSync(fd, "utf8");
331
+ const buf = Buffer.alloc(MAX_SCAN_BYTES);
332
+ readSync(fd, buf, 0, MAX_SCAN_BYTES, size - MAX_SCAN_BYTES);
333
+ // La première ligne est coupée en plein milieu : on la jette.
334
+ return buf.toString("utf8").replace(/^[^\n]*\n/u, "");
335
+ } finally {
336
+ closeSync(fd);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Les journaux à lire : ceux donnés, sinon ceux du dossier `logs/` du projet.
342
+ *
343
+ * @param opts - les options lues.
344
+ * @returns les fichiers lisibles, du plus ancien au plus récent.
345
+ */
346
+ export function journalFiles(opts) {
347
+ if (opts.files.length > 0)
348
+ return opts.files.map((f) => path.resolve(opts.root, f));
349
+ const dir = path.join(opts.root, "logs");
350
+ let names;
351
+ try {
352
+ names = readdirSync(dir);
353
+ } catch {
354
+ return [];
355
+ }
356
+ return names
357
+ .filter((n) => n.endsWith(".log") || n.endsWith(".jsonl"))
358
+ .map((n) => path.join(dir, n))
359
+ .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
360
+ }
361
+
362
+ /** Âge lisible d'un fichier : un journal d'hier ne parle pas de la panne d'aujourd'hui. */
363
+ function age(ms) {
364
+ const min = Math.round((Date.now() - ms) / 60_000);
365
+ if (min < 1) return "moins d'une minute";
366
+ if (min < 120) return `${min} min`;
367
+ return `${Math.round(min / 60)} h`;
368
+ }
369
+
370
+ const USAGE = `Usage : node log-errors.mjs [journal…] [--warnings] [--last N] [--json] [--root DIR]
371
+
372
+ Sans fichier : lit logs/*.log et logs/*.jsonl du projet.
373
+ Sorties : 0 aucune erreur · 1 au moins une erreur · 64 usage · 66 aucun journal lisible.`;
374
+
375
+ /**
376
+ * Point d'entrée.
377
+ *
378
+ * @returns le code de sortie.
379
+ */
380
+ export function main(argv = process.argv.slice(2)) {
381
+ let opts;
382
+ try {
383
+ opts = parseArgs(argv);
384
+ } catch (e) {
385
+ process.stderr.write(`${e.message}\n\n${USAGE}\n`);
386
+ return 64;
387
+ }
388
+ if (opts.help) {
389
+ process.stdout.write(`${USAGE}\n`);
390
+ return 0;
391
+ }
392
+ const files = journalFiles(opts);
393
+ const read = [];
394
+ const events = [];
395
+ for (const file of files) {
396
+ let text;
397
+ try {
398
+ text = readTail(file);
399
+ } catch (e) {
400
+ process.stderr.write(`illisible : ${file} — ${e.message}\n`);
401
+ continue;
402
+ }
403
+ const rel = path.relative(opts.root, file) || file;
404
+ read.push({ file: rel, age: age(statSync(file).mtimeMs) });
405
+ events.push(...extractEvents(text, rel));
406
+ }
407
+ if (read.length === 0) {
408
+ process.stderr.write(
409
+ "Aucun journal lisible.\n" +
410
+ "En développement, le journal part sur la SORTIE du terminal qui a lancé le serveur, pas dans un fichier.\n" +
411
+ "Pour l'avoir ici : `npx nodefony development --detach` (journal dans logs/nodefony-detached.log),\n" +
412
+ "ou redirige la sortie dans un fichier, puis passe-le en argument.\n",
413
+ );
414
+ return 66;
415
+ }
416
+ const all = summarize(events, opts);
417
+ const shown = all.slice(-opts.last);
418
+ if (opts.json) {
419
+ process.stdout.write(
420
+ `${JSON.stringify({ read, total: all.length, errors: shown }, null, 2)}\n`,
421
+ );
422
+ return all.length > 0 ? 1 : 0;
423
+ }
424
+ process.stdout.write(
425
+ `Journaux lus : ${read.map((r) => `${r.file} (écrit il y a ${r.age})`).join(", ")}\n\n`,
426
+ );
427
+ if (all.length === 0) {
428
+ process.stdout.write(
429
+ `Aucune erreur${opts.warnings ? " ni avertissement" : ""}.\n`,
430
+ );
431
+ return 0;
432
+ }
433
+ process.stdout.write(
434
+ `${all.length} erreur${all.length > 1 ? "s" : ""} distincte${all.length > 1 ? "s" : ""}` +
435
+ `${all.length > shown.length ? ` — les ${shown.length} plus récentes` : ""}\n\n`,
436
+ );
437
+ for (const e of shown) {
438
+ const when =
439
+ e.count > 1
440
+ ? `${e.firstTime ?? "?"} → ${e.lastTime ?? "?"}, ×${e.count}`
441
+ : (e.firstTime ?? "heure inconnue");
442
+ process.stdout.write(
443
+ `${e.severity} ${e.msgid} [${e.source}, ${when}]\n ${e.message}\n`,
444
+ );
445
+ for (const d of e.detail.slice(0, 4)) process.stdout.write(` ${d}\n`);
446
+ if (e.detail.length > 4)
447
+ process.stdout.write(` … ${e.detail.length - 4} ligne(s) de plus\n`);
448
+ if (e.hint) process.stdout.write(` ➜ ${e.hint}\n`);
449
+ process.stdout.write("\n");
450
+ }
451
+ return 1;
452
+ }
453
+
454
+ // Exécuté directement (et non importé par son test) : rendre le code.
455
+ // La comparaison passe par une URL, jamais par un chemin : sous Windows, `D:\…`
456
+ // verrait son `d:` lu comme un protocole.
457
+ if (
458
+ process.argv[1] !== undefined &&
459
+ // Chemins RÉELS des deux côtés : Node résout `import.meta.url` en suivant
460
+ // les liens, pas `argv[1]` — lancé par `/var/…` (macOS) ou un dossier lié,
461
+ // la comparaison brute échouait et le script ne faisait RIEN.
462
+ realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
463
+ ) {
464
+ process.exitCode = main();
465
+ }
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: nodefony-upgrade
3
+ description: >
4
+ Monte une application Nodefony vers une nouvelle version du framework sans rien laisser derrière :
5
+ tous les paquets ensemble et sous le bon canal, puis `nodefony upgrade` pour la documentation des
6
+ agents, les skills et les fichiers livrés qu'un `npm install` ne refait pas, puis les ruptures
7
+ annoncées corrigées dans le code, le schéma migré et la preuve que tout tient. À charger AVANT de
8
+ lancer la montée de version, pas après un premier rouge. Une panne sans rapport avec une montée →
9
+ `nodefony-troubleshoot`.
10
+ Déclencheurs : "monter nodefony de version", "mettre à jour nodefony", "passer à la nouvelle
11
+ beta", "upgrade nodefony", "npm update nodefony", "une nouvelle version est sortie", "mes paquets
12
+ nodefony sont en retard", "après la mise à jour plus rien ne compile", "AGENTS.md est périmé".
13
+ metadata:
14
+ version: 1.0.0
15
+ ---
16
+
17
+ # nodefony-upgrade — les paquets, puis l'application, puis la preuve
18
+
19
+ > **Maintenance** : vérité courante, jamais un journal. Éditer en place ; l'historique vit dans
20
+ > `git log`. Pas de `references/` : ce qui doit atteindre un agent s'exécute ou vit dans ce corps.
21
+
22
+ ## Passer la main
23
+
24
+ | Ce que tu fais en réalité | Le skill |
25
+ | ---------------------------------------------------------------- | ------------------------- |
26
+ | Rien n'a été mis à jour, et quelque chose a cessé de marcher | `nodefony-troubleshoot` |
27
+ | Une rupture touche une table ou une colonne : migration à écrire | `nodefony-migrate-schema` |
28
+ | Une rupture touche la connexion, `/me`, l'identité de la requête | `nodefony-authentication` |
29
+ | Monter l'image et la déployer une fois l'application au niveau | `nodefony-devops` |
30
+ | Coder autre chose dans la foulée | `nodefony-dev` |
31
+
32
+ ## 1. La règle — trois couches, trois gestes
33
+
34
+ Une montée de version touche trois couches, et `npm install` n'en fait qu'une :
35
+
36
+ 1. **Les paquets** (`node_modules`) — `npm install`.
37
+ 2. **Ce que les paquets ont déposé dans l'application** à sa création : le bloc framework
38
+ d'`AGENTS.md`, les pointeurs de `.agents/skills/`, les copies livrées (décor Keycloak…). Ils
39
+ restent sur l'ancienne version sans que rien ne casse — `nodefony upgrade`.
40
+ 3. **Ton code**, quand la version annonce une rupture — toi, guidé par le changelog et le
41
+ compilateur.
42
+
43
+ Sauter la couche 2 ne fait échouer aucun test : un agent lit simplement une porte qui décrit
44
+ l'ancienne version. Sauter la couche 3 se voit au premier `typecheck`.
45
+
46
+ ## 2. La procédure
47
+
48
+ **Avant** : arbre propre, tests verts. Une montée sur un arbre déjà rouge mélange deux pannes.
49
+
50
+ ```bash
51
+ git status
52
+ npm test
53
+ npx nodefony outdated
54
+ ```
55
+
56
+ `outdated` liste les paquets en retard, regroupés. Note la version cible.
57
+
58
+ **Monter TOUS les paquets Nodefony, ensemble, sous le bon canal.** Ils sortent verrouillés : même
59
+ version pour `nodefony` et chaque `@nodefony/*`, devDependencies comprises (`@nodefony/devkit`
60
+ porte les skills). Nomme chacun de ceux que ton `package.json` déclare :
61
+
62
+ ```bash
63
+ npm install nodefony@beta @nodefony/http@beta @nodefony/framework@beta @nodefony/security@beta
64
+ npm install -D @nodefony/devkit@beta
65
+ ```
66
+
67
+ ⚠️ Pendant la série 10, **`beta` est le canal de la préversion** ; `latest` sert encore l'ancienne
68
+ lignée 7 en JavaScript. Un `npm install nodefony` sans canal fait REDESCENDRE l'application.
69
+
70
+ **Remettre l'application au niveau des paquets** :
71
+
72
+ ```bash
73
+ npx nodefony upgrade
74
+ ```
75
+
76
+ Elle réécrit le bloc framework d'`AGENTS.md` (le texte hors des marqueurs `<!-- nodefony:start -->`
77
+ / `<!-- nodefony:end -->` ne bouge pas), pose les pointeurs des skills apparus, rafraîchit les
78
+ copies livrées restées intactes. Ce qu'elle ne tranche pas est listé sous « À trancher à la main » :
79
+ une copie que l'application a retouchée, un `SKILL.md` écrit à la main, un skill que plus aucun
80
+ paquet ne livre. **Lis cette liste** : chaque ligne est une décision qui t'appartient.
81
+
82
+ **Lire les ruptures annoncées.** Le changelog est livré dans le paquet `nodefony` :
83
+ `node_modules/nodefony/CHANGELOG.md`, celui de la version INSTALLÉE — lu après la montée, il
84
+ couvre donc toutes les versions franchies, sans réseau. Absent (une version qui ne le livrait pas
85
+ encore) : `https://github.com/nodefony/nodefony-core/blob/main/CHANGELOG.md`. Dans la
86
+ section de chaque version franchie, les entrées marquées **`(breaking)`** sont celles qui
87
+ demandent une retouche de ton code — un nom renvoyé qui change, une option retirée, un défaut
88
+ inversé. Une rupture de comportement ne se voit pas au compilateur : seul le changelog la dit.
89
+
90
+ **Reconstruire, puis laisser le compilateur et les tests désigner le reste** :
91
+
92
+ ```bash
93
+ npm run build
94
+ npm run typecheck
95
+ npm test
96
+ npx nodefony doctor
97
+ ```
98
+
99
+ Un symbole renommé ou retiré sort au `typecheck` avec son fichier et sa ligne. Un symbole
100
+ **déprécié** apparaît barré dans l'éditeur : il marche encore toute la série, son TSDoc nomme le
101
+ remplaçant. `doctor` signale ce qui reste en retard, chaque constat avec son geste.
102
+
103
+ **Si une rupture touche le schéma**, ou si le changelog l'annonce : `npx nodefony orm:migrate:status`,
104
+ puis la procédure de `nodefony-migrate-schema`.
105
+
106
+ ## 3. Pièges
107
+
108
+ - **Monter un paquet seul n'est pas monter Nodefony.** Deux versions différentes de `nodefony`
109
+ dans `node_modules` se voient comme deux copies du cœur : l'application démarre amputée, ou pas
110
+ du tout. `npm ls nodefony` doit rendre une seule version.
111
+ - **Le canal par défaut ment pendant la série 10** : sans `@beta`, npm installe la 7.
112
+ - **Un vert ne prouve pas que la documentation des agents a suivi.** Seuls `doctor` ou
113
+ `npx nodefony upgrade --check` le voient.
114
+ - **`upgrade` ne recrée pas un `AGENTS.md` supprimé** : l'absence peut être un choix. Elle le dit.
115
+ - **Ne corrige pas une copie livrée « en retard » à la main** : si elle est intacte, `upgrade` la
116
+ remplace ; si tu l'as retouchée, reporte tes retouches sur la nouvelle version du paquet, à la
117
+ main et en connaissance de cause.
118
+
119
+ ## 4. Gate — la montée est faite quand
120
+
121
+ ```bash
122
+ npx nodefony upgrade --check
123
+ npm run typecheck
124
+ npm test
125
+ npx nodefony doctor
126
+ ```
127
+
128
+ `upgrade --check` sort en 0 (sinon il dit ce qui reste en retard), le typecheck et les tests sont
129
+ verts, `doctor` ne signale rien de neuf. En intégration continue, `upgrade --check` refuse une
130
+ branche qui a monté ses paquets sans remettre l'application au niveau.
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.152.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 };