@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.
- package/README.md +151 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +27 -163
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/card.js +1 -2
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +217 -20
- package/package.json +17 -12
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
- package/skills/nodefony-add-service/SKILL.md +14 -3
- package/skills/nodefony-authentication/SKILL.md +234 -0
- package/skills/nodefony-browser/SKILL.md +65 -21
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/references/sondes.md +13 -0
- package/skills/nodefony-browser/scripts/audit.mjs +59 -10
- package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
- package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
- package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +78 -82
- package/skills/nodefony-dev/scripts/docs.mjs +24 -8
- package/skills/nodefony-devops/SKILL.md +98 -74
- package/skills/nodefony-devops/references/compose.md +8 -0
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +282 -48
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +30 -5
- package/skills/nodefony-devops/references/variables.md +66 -31
- package/skills/nodefony-migrate-schema/SKILL.md +5 -2
- package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
- package/skills/nodefony-protect-route/SKILL.md +124 -21
- package/skills/nodefony-security-review/SKILL.md +127 -0
- package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
- package/skills/nodefony-troubleshoot/SKILL.md +225 -0
- package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
- package/skills/nodefony-upgrade/SKILL.md +130 -0
- 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 };
|