nestgen-cli 0.1.0 → 0.2.2

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 (33) hide show
  1. package/dist/cli/doctor-output.js +45 -0
  2. package/dist/cli/output.js +65 -0
  3. package/dist/engine/file-transaction.js +166 -0
  4. package/dist/engine/generated-code.js +26 -0
  5. package/dist/engine/generation-manifest.js +118 -0
  6. package/dist/engine/migration-plan.js +78 -0
  7. package/dist/engine/module-generator.js +80 -65
  8. package/dist/engine/project-config.js +5 -2
  9. package/dist/engine/project-path.js +25 -0
  10. package/dist/engine/project-preflight.js +96 -0
  11. package/dist/engine/resource-generator.js +1104 -68
  12. package/dist/engine/resource-spec.js +254 -11
  13. package/examples/store-api/README.md +76 -0
  14. package/examples/store-api/definitions/customers.resource.json +14 -0
  15. package/examples/store-api/definitions/order-items.resource.json +26 -0
  16. package/examples/store-api/definitions/orders.resource.json +22 -0
  17. package/examples/store-api/definitions/products.resource.json +19 -0
  18. package/examples/store-api/nestgen.example.json +7 -0
  19. package/nestgen.js +488 -84
  20. package/nestjs-generator/features/configure_bootstrap.mjs +5 -2
  21. package/nestjs-generator/features/docker.sh +6 -4
  22. package/nestjs-generator/features/git.sh +1 -1
  23. package/nestjs-generator/features/inject_module_to_app.sh +8 -6
  24. package/nestjs-generator/features/logger.sh +8 -4
  25. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  26. package/nestjs-generator/features/operations.sh +18 -0
  27. package/nestjs-generator/features/preflight.mjs +2 -44
  28. package/nestjs-generator/features/prisma.sh +3 -3
  29. package/nestjs-generator/features/typeorm.sh +3 -3
  30. package/nestjs-generator/features/utils.sh +5 -3
  31. package/nestjs-generator/generate_project.sh +15 -9
  32. package/package.json +11 -4
  33. package/readme.md +234 -13
package/nestgen.js CHANGED
@@ -7,11 +7,27 @@ import path from 'path';
7
7
  import fs from 'fs';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { inspectProject } from './nestjs-generator/features/preflight.mjs';
10
- import { generateModule } from './dist/engine/module-generator.js';
11
- import { generateResource } from './dist/engine/resource-generator.js';
12
- import { parseResourceFields } from './dist/engine/resource-spec.js';
10
+ import { generateModule, planModuleGeneration } from './dist/engine/module-generator.js';
11
+ import { generateResource, planResourceGeneration } from './dist/engine/resource-generator.js';
12
+ import {
13
+ parseResourceFields,
14
+ parseResourceIndexes,
15
+ parseResourceListOptions,
16
+ parseResourceRelations,
17
+ } from './dist/engine/resource-spec.js';
13
18
  import { configFileName, defaultConfig, loadConfigIfPresent, writeConfig } from './dist/engine/project-config.js';
14
19
  import { parseArchitectureProfile } from './dist/engine/architecture-profile.js';
20
+ import { planResourceMigration } from './dist/engine/migration-plan.js';
21
+ import { formatDoctorOutput } from './dist/cli/doctor-output.js';
22
+ import {
23
+ heading,
24
+ success,
25
+ formatError,
26
+ formatConfig,
27
+ formatPlan,
28
+ formatMigration,
29
+ formatHelp,
30
+ } from './dist/cli/output.js';
15
31
 
16
32
  // ────── Resolve __dirname compatible ES Modules
17
33
  const __filename = fileURLToPath(import.meta.url);
@@ -26,17 +42,7 @@ const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
26
42
 
27
43
  // ────── Logo CLI
28
44
  function printLogo() {
29
- console.log(
30
- chalk.magentaBright(`
31
- ███╗ ██╗███████╗███████╗████████╗ ██████╗ ███████╗███╗ ██╗
32
- ████╗ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║
33
- ██╔██╗ ██║█████╗ ███████╗ ██║ ██║ ███╗█████╗ ██╔██╗ ██║
34
- ██║╚██╗██║██╔══╝ ╚════██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║
35
- ██║ ╚████║███████╗███████║ ██║ ╚██████╔╝███████╗██║ ╚████║
36
- ╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
37
- `),
38
- );
39
- console.log(chalk.cyan.bold('✨ NestGen CLI — Générateur modulaire NestJS'));
45
+ console.log(heading('CLI', 'Générateur modulaire NestJS'));
40
46
  }
41
47
 
42
48
  // ────── Helpers
@@ -68,6 +74,11 @@ export function validateOrm(value) {
68
74
  return orm;
69
75
  }
70
76
 
77
+ function validatePackageManager(value) {
78
+ if (!['npm', 'pnpm', 'yarn'].includes(value)) throw new Error('Package manager invalide.');
79
+ return value;
80
+ }
81
+
71
82
  export function resolveProjectPath(value) {
72
83
  const projectPath = String(value ?? '').trim();
73
84
 
@@ -90,6 +101,15 @@ export function parseCliArgs(args) {
90
101
  orm: undefined,
91
102
  profile: undefined,
92
103
  packageManager: undefined,
104
+ projectPath: undefined,
105
+ docker: undefined,
106
+ swagger: undefined,
107
+ operations: undefined,
108
+ git: undefined,
109
+ modules: undefined,
110
+ application: undefined,
111
+ indexes: undefined,
112
+ definitionFile: undefined,
93
113
  noInteractive: false,
94
114
  quiet: false,
95
115
  verbose: false,
@@ -97,6 +117,9 @@ export function parseCliArgs(args) {
97
117
  help: false,
98
118
  version: false,
99
119
  dryRun: false,
120
+ update: false,
121
+ migrationName: undefined,
122
+ json: false,
100
123
  };
101
124
  const positionals = [];
102
125
  for (let index = 0; index < args.length; index += 1) {
@@ -110,8 +133,29 @@ export function parseCliArgs(args) {
110
133
  else if (argument.startsWith('--profile=')) options.profile = argument.slice(10);
111
134
  else if (argument === '--package-manager') options.packageManager = args[++index];
112
135
  else if (argument.startsWith('--package-manager=')) options.packageManager = argument.slice(18);
136
+ else if (argument === '--project-path') options.projectPath = args[++index];
137
+ else if (argument.startsWith('--project-path=')) options.projectPath = argument.slice(15);
138
+ else if (argument === '--docker') options.docker = true;
139
+ else if (argument === '--no-docker') options.docker = false;
140
+ else if (argument === '--swagger') options.swagger = true;
141
+ else if (argument === '--no-swagger') options.swagger = false;
142
+ else if (argument === '--operations') options.operations = true;
143
+ else if (argument === '--no-operations') options.operations = false;
144
+ else if (argument === '--git') options.git = true;
145
+ else if (argument === '--no-git') options.git = false;
146
+ else if (argument === '--modules') options.modules = args[++index];
147
+ else if (argument.startsWith('--modules=')) options.modules = argument.slice(10);
148
+ else if (argument === '--application') {
149
+ const value = args[++index];
150
+ if (!value || value.startsWith('-')) throw new Error('--application requiert une valeur.');
151
+ options.application = value;
152
+ } else if (argument.startsWith('--application=')) options.application = argument.slice(14);
113
153
  else if (argument === '--fields') options.fields = args[++index];
114
154
  else if (argument.startsWith('--fields=')) options.fields = argument.slice(9);
155
+ else if (argument === '--indexes') options.indexes = args[++index];
156
+ else if (argument.startsWith('--indexes=')) options.indexes = argument.slice(10);
157
+ else if (argument === '--file') options.definitionFile = args[++index];
158
+ else if (argument.startsWith('--file=')) options.definitionFile = argument.slice(7);
115
159
  else if (argument === '--route') options.route = args[++index];
116
160
  else if (argument.startsWith('--route=')) options.route = argument.slice(8);
117
161
  else if (argument === '--table') options.table = args[++index];
@@ -120,6 +164,10 @@ export function parseCliArgs(args) {
120
164
  else if (argument === '--quiet') options.quiet = true;
121
165
  else if (argument === '--verbose') options.verbose = true;
122
166
  else if (argument === '--dry-run') options.dryRun = true;
167
+ else if (argument === '--update') options.update = true;
168
+ else if (argument === '--migration-name') options.migrationName = args[++index];
169
+ else if (argument.startsWith('--migration-name=')) options.migrationName = argument.slice(17);
170
+ else if (argument === '--json') options.json = true;
123
171
  else if (argument === '--no-color') options.color = false;
124
172
  else if (argument === '--help' || argument === '-h') options.help = true;
125
173
  else if (argument === '--version' || argument === '-V') options.version = true;
@@ -129,10 +177,10 @@ export function parseCliArgs(args) {
129
177
  return { command: positionals.shift(), positionals, options };
130
178
  }
131
179
 
132
- export function createModulePlan(projectRoot, moduleName, orm) {
180
+ export function createModulePlan(projectRoot, moduleName, orm, sourceRoot = 'src') {
133
181
  const normalizedName = validateModuleName(moduleName);
134
182
  const normalizedOrm = validateOrm(orm);
135
- const resourceRoot = `src/app/${normalizedName}`;
183
+ const resourceRoot = `${sourceRoot}/app/${normalizedName}`;
136
184
  return {
137
185
  operation: 'module',
138
186
  module: normalizedName,
@@ -148,8 +196,8 @@ export function createModulePlan(projectRoot, moduleName, orm) {
148
196
  `${resourceRoot}/${normalizedName}.module.ts`,
149
197
  ],
150
198
  mutations: [
151
- 'Ajoute le module dans @Module({ imports }) de src/app.module.ts.',
152
- 'Ajoute CqrsModule dans src/app.module.ts si nécessaire.',
199
+ `Ajoute le module dans @Module({ imports }) de ${sourceRoot}/app.module.ts.`,
200
+ `Ajoute CqrsModule dans ${sourceRoot}/app.module.ts si nécessaire.`,
153
201
  ...(normalizedOrm === 'typeorm'
154
202
  ? ['Ajoute la configuration TypeORM racine seulement si elle est absente.']
155
203
  : []),
@@ -157,9 +205,28 @@ export function createModulePlan(projectRoot, moduleName, orm) {
157
205
  };
158
206
  }
159
207
 
208
+ function dryRunOutput(operation, plan) {
209
+ const conflicts = [
210
+ ...(plan.featureConflict ? [{ path: '', reason: plan.featureConflict }] : []),
211
+ ...plan.preview.filter((change) => change.status === 'conflict').map(({ path, reason }) => ({ path, reason })),
212
+ ];
213
+ return {
214
+ version: 1,
215
+ operation,
216
+ changes: plan.preview.map(({ path, operation: fileOperation, status, diff, reason }) => ({
217
+ path,
218
+ operation: fileOperation,
219
+ status,
220
+ ...(diff ? { diff } : {}),
221
+ ...(reason ? { reason } : {}),
222
+ })),
223
+ conflicts,
224
+ };
225
+ }
226
+
160
227
  export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
161
228
  const result = spawnSync('bash', [scriptPath, ...args], {
162
- env: { ...process.env, ...env },
229
+ env: { ...process.env, ...(chalk.level === 0 ? { NO_COLOR: '1' } : {}), ...env },
163
230
  stdio: quiet ? 'pipe' : 'inherit',
164
231
  });
165
232
 
@@ -215,6 +282,12 @@ async function askInitQuestions() {
215
282
  message: '🐳 Activer Docker ?',
216
283
  default: false,
217
284
  },
285
+ {
286
+ type: 'confirm',
287
+ name: 'withOperations',
288
+ message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
289
+ default: false,
290
+ },
218
291
  {
219
292
  type: 'confirm',
220
293
  name: 'withGit',
@@ -249,42 +322,75 @@ function validatePromptValue(validator) {
249
322
 
250
323
  async function runInteractiveInit(options) {
251
324
  if (!options.quiet) printLogo();
252
- if (options.noInteractive)
253
- throw new Error(
254
- 'init --no-interactive requiert des options de projet qui ne sont pas encore prises en charge.',
255
- );
256
-
257
325
  if (!fs.existsSync(GENERATE_SCRIPT)) {
258
326
  throw new Error(`Script introuvable : ${GENERATE_SCRIPT}`);
259
327
  }
260
328
 
261
- const answers = await askInitQuestions();
262
- const { projectName, projectPath, packageManager, orm, withSwagger, withDocker, withGit, modules } = answers;
329
+ const answers = options.noInteractive
330
+ ? (() => {
331
+ const missing = [
332
+ ['nom du projet', options.projectName],
333
+ ['--project-path', options.projectPath],
334
+ ['--package-manager', options.packageManager],
335
+ ['--orm', options.orm],
336
+ ['--docker ou --no-docker', options.docker],
337
+ ['--swagger ou --no-swagger', options.swagger],
338
+ ['--operations ou --no-operations', options.operations],
339
+ ['--git ou --no-git', options.git],
340
+ ['--modules', options.modules],
341
+ ]
342
+ .filter(([, value]) => value === undefined)
343
+ .map(([name]) => name);
344
+ if (missing.length) throw new Error(`init --no-interactive requiert : ${missing.join(', ')}.`);
345
+ return {
346
+ projectName: options.projectName,
347
+ projectPath: options.projectPath,
348
+ packageManager: options.packageManager,
349
+ orm: options.orm,
350
+ withDocker: options.docker,
351
+ withSwagger: options.swagger,
352
+ withOperations: options.operations,
353
+ withGit: options.git,
354
+ modules: options.modules ? options.modules.split(',').filter(Boolean) : [],
355
+ };
356
+ })()
357
+ : await askInitQuestions();
358
+ const { projectName, projectPath, packageManager, orm, withSwagger, withDocker, withOperations, withGit, modules } =
359
+ answers;
263
360
 
264
361
  const env = {
265
362
  APP_NAME: validateModuleName(projectName),
266
363
  PROJECT_PATH: resolveProjectPath(projectPath),
267
- PM: packageManager,
364
+ PM: validatePackageManager(packageManager),
268
365
  ORM: validateOrm(orm),
269
366
  WITH_SWAGGER: withSwagger ? 'y' : 'n',
367
+ WITH_OPERATIONS: withOperations ? 'y' : 'n',
270
368
  WITH_DOCKER: withDocker ? 'y' : 'n',
271
369
  WITH_GIT: withGit ? 'y' : 'n',
272
370
  MODULES: modules.map(validateModuleName).join(' '),
273
371
  };
274
372
 
275
- console.log('\n🚀 Lancement de la génération du projet...\n');
276
- runBashScript(GENERATE_SCRIPT, [], env);
373
+ if (!options.quiet) console.log(chalk.cyan(' › Génération du projet…'));
374
+ runBashScript(GENERATE_SCRIPT, [], env, options.quiet);
375
+ return {
376
+ projectName: env.APP_NAME,
377
+ projectPath: env.PROJECT_PATH,
378
+ packageManager: env.PM,
379
+ orm: env.ORM,
380
+ operations: env.WITH_OPERATIONS === 'y',
381
+ };
277
382
  }
278
383
 
279
384
  // ────── Commande : MODULE
280
385
  async function runModuleGeneration(parsed) {
281
386
  if (!parsed.options.quiet) printLogo();
387
+ const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
282
388
 
283
389
  let moduleName, orm;
284
390
 
285
391
  if (parsed.positionals[0]) {
286
392
  moduleName = validateModuleName(parsed.positionals[0]);
287
- orm = validateOrm(parsed.options.orm ?? 'typeorm');
393
+ orm = validateOrm(parsed.options.orm ?? config.orm);
288
394
  } else {
289
395
  if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
290
396
  const answers = await inquirer.prompt([
@@ -311,109 +417,385 @@ async function runModuleGeneration(parsed) {
311
417
  }
312
418
 
313
419
  if (parsed.options.dryRun) {
314
- inspectProject(process.cwd(), orm);
315
- console.log(JSON.stringify(createModulePlan(process.cwd(), moduleName, orm), null, 2));
316
- return;
420
+ const plan = await planModuleGeneration(process.cwd(), moduleName, orm, parsed.options.application);
421
+ const output = dryRunOutput('module', plan);
422
+ if (!parsed.options.json) console.log(formatPlan(output, parsed.options.verbose));
423
+ return output;
317
424
  }
318
425
 
319
- if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
426
+ if (!parsed.options.quiet) console.log(chalk.cyan(` › Génération du module ${moduleName}…`));
320
427
  if (process.env.NESTGEN_ROOT) {
321
428
  runBashScript(ADD_MODULE_SCRIPT, [moduleName, orm], {}, parsed.options.quiet);
322
- return;
429
+ return { module: moduleName, orm, generated: true };
430
+ }
431
+
432
+ await generateModule(process.cwd(), moduleName, orm, parsed.options.application);
433
+ return { module: moduleName, orm, generated: true };
434
+ }
435
+
436
+ export function loadResourceDefinition(file, projectRoot = process.cwd()) {
437
+ const candidate = path.resolve(projectRoot, String(file ?? ''));
438
+ if (!candidate.startsWith(`${path.resolve(projectRoot)}${path.sep}`))
439
+ throw new Error('--file doit désigner un fichier situé dans le projet courant.');
440
+ let value;
441
+ try {
442
+ value = JSON.parse(fs.readFileSync(candidate, 'utf8'));
443
+ } catch (error) {
444
+ throw new Error(`Définition de ressource invalide (${candidate}) : ${error.message}`);
445
+ }
446
+ if (!value || typeof value !== 'object' || Array.isArray(value))
447
+ throw new Error(`Définition de ressource invalide (${candidate}) : un objet JSON est requis.`);
448
+ if (value.version !== 1) throw new Error(`Définition de ressource invalide (${candidate}) : version 1 requise.`);
449
+ if (!Array.isArray(value.fields) || value.fields.some((field) => typeof field !== 'string'))
450
+ throw new Error(`Définition de ressource invalide (${candidate}) : fields doit être un tableau de chaînes.`);
451
+ for (const [index, field] of value.fields.entries()) {
452
+ try {
453
+ parseResourceFields([field]);
454
+ } catch (error) {
455
+ throw new Error(`Définition de ressource invalide (${candidate}) : fields[${index}] — ${error.message}`);
456
+ }
457
+ }
458
+ const fields = parseResourceFields(value.fields);
459
+ if (value.relations !== undefined && !Array.isArray(value.relations))
460
+ throw new Error(`Définition de ressource invalide (${candidate}) : relations doit être un tableau.`);
461
+ let relations;
462
+ try {
463
+ relations = parseResourceRelations(value.relations ?? []);
464
+ } catch (error) {
465
+ throw new Error(`Définition de ressource invalide (${candidate}) : relations — ${error.message}`);
466
+ }
467
+ const relationFields = relationIndexFields(relations);
468
+ try {
469
+ parseResourceListOptions(value.list, [...fields, ...relationFields]);
470
+ } catch (error) {
471
+ throw new Error(`Définition de ressource invalide (${candidate}) : list — ${error.message}`);
323
472
  }
473
+ if (
474
+ value.indexes !== undefined &&
475
+ (!Array.isArray(value.indexes) || value.indexes.some((index) => typeof index !== 'string'))
476
+ )
477
+ throw new Error(`Définition de ressource invalide (${candidate}) : indexes doit être un tableau de chaînes.`);
478
+ try {
479
+ if (
480
+ value.uniqueIndexes !== undefined &&
481
+ (!Array.isArray(value.uniqueIndexes) || value.uniqueIndexes.some((index) => typeof index !== 'string'))
482
+ )
483
+ throw new Error(
484
+ `Définition de ressource invalide (${candidate}) : uniqueIndexes doit être un tableau de chaînes.`,
485
+ );
486
+ parseResourceIndexes(value.indexes ?? [], [...fields, ...relationFields], value.uniqueIndexes ?? []);
487
+ } catch (error) {
488
+ throw new Error(`Définition de ressource invalide (${candidate}) : indexes — ${error.message}`);
489
+ }
490
+ return value;
491
+ }
324
492
 
325
- generateModule(process.cwd(), moduleName, orm);
493
+ function relationIndexFields(relations) {
494
+ return relations
495
+ .filter((relation) => relation.type === 'belongsTo')
496
+ .map((relation) => ({
497
+ name: `${relation.field ?? relation.target.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Id`,
498
+ }));
326
499
  }
327
500
 
328
- function runResourceGeneration(parsed) {
329
- const name = validateModuleName(parsed.positionals[0] ?? '');
330
- if (!parsed.options.fields) throw new Error('resource requiert --fields champ:type[,champ:type].');
501
+ async function runResourceGeneration(parsed) {
502
+ const definition = parsed.options.definitionFile ? loadResourceDefinition(parsed.options.definitionFile) : {};
503
+ const name = validateModuleName(parsed.positionals[0] ?? definition.name ?? '');
504
+ const fieldValues = parsed.options.fields?.split(',').filter(Boolean) ?? definition.fields;
505
+ if (!fieldValues) throw new Error('resource requiert --fields ou --file avec fields.');
331
506
  const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
332
- const orm = validateOrm(parsed.options.orm ?? config.orm);
333
- const profile = parseArchitectureProfile(parsed.options.profile ?? config.profile);
334
- const fields = parseResourceFields(parsed.options.fields.split(',').filter(Boolean));
335
- const route = parsed.options.route ?? `${name}s`;
336
- const table = parsed.options.table ?? `${name}s`;
507
+ const orm = validateOrm(parsed.options.orm ?? definition.orm ?? config.orm);
508
+ const profile = parseArchitectureProfile(parsed.options.profile ?? definition.profile ?? config.profile);
509
+ const fields = parseResourceFields(fieldValues);
510
+ const relations = parseResourceRelations(definition.relations ?? []);
511
+ const relationFields = relationIndexFields(relations);
512
+ const list = parseResourceListOptions(definition.list, [...fields, ...relationFields]);
513
+ const indexes = parseResourceIndexes(
514
+ parsed.options.indexes?.split(',').filter(Boolean) ?? definition.indexes ?? [],
515
+ [...fields, ...relationFields],
516
+ definition.uniqueIndexes ?? [],
517
+ );
518
+ const route = parsed.options.route ?? definition.route ?? `${name}s`;
519
+ const table = parsed.options.table ?? definition.table ?? `${name}s`;
337
520
  if (!/^[a-z][a-z0-9/-]*$/.test(route) || !/^[a-z][a-z0-9_]*$/.test(table))
338
521
  throw new Error('Route ou table invalide.');
522
+ if (parsed.options.migrationName && !parsed.options.update) throw new Error('--migration-name requiert --update.');
523
+ const resourceOptions = {
524
+ name,
525
+ route,
526
+ table,
527
+ fields,
528
+ indexes,
529
+ relations,
530
+ list,
531
+ orm,
532
+ profile,
533
+ application: parsed.options.application,
534
+ update: parsed.options.update,
535
+ };
536
+ const migration = parsed.options.migrationName
537
+ ? planResourceMigration(
538
+ process.cwd(),
539
+ inspectProject(process.cwd(), orm, { application: parsed.options.application }).sourceRoot,
540
+ name,
541
+ orm,
542
+ parsed.options.migrationName,
543
+ fields,
544
+ indexes,
545
+ relations,
546
+ )
547
+ : undefined;
339
548
  if (parsed.options.dryRun) {
340
- console.log(JSON.stringify({ operation: 'resource', name, route, table, orm, profile, fields }, null, 2));
341
- return;
549
+ const plan = await planResourceGeneration(process.cwd(), resourceOptions);
550
+ const output = { ...dryRunOutput('resource', plan), ...(migration ? { migration } : {}) };
551
+ if (!parsed.options.json) console.log(formatPlan(output, parsed.options.verbose));
552
+ return output;
342
553
  }
343
- generateResource(process.cwd(), { name, route, table, fields, orm, profile });
554
+ await generateResource(process.cwd(), resourceOptions);
555
+ return {
556
+ resource: name,
557
+ orm,
558
+ profile,
559
+ generated: true,
560
+ ...(parsed.options.update ? { updated: true } : {}),
561
+ ...(migration ? { migration } : {}),
562
+ };
344
563
  }
345
564
 
346
565
  function printUsage() {
347
566
  console.log(
348
- `Usage: nestgen <commande> [options]\n\nCommandes:\n init Génère un projet NestJS en mode interactif\n module <nom> [--orm <orm>] Génère un module\n resource <nom> --fields ... Génère un CRUD TypeORM
567
+ formatHelp(`Usage: nestgen <commande> [options]\n\nCommandes:\n init [nom] Génère un projet NestJS\n module <nom> [--orm <orm>] Génère un module\n resource <nom> --fields ... Génère un CRUD TypeORM ou Prisma
349
568
  config init|show Gère nestgen.config.json
350
569
  doctor Vérifie l'installation\n\nOptions:\n -h, --help Affiche cette aide\n -V, --version Affiche la version\n --no-interactive Refuse les prompts\n --quiet Supprime les sorties non essentielles\n --verbose Active les diagnostics\n --no-color Désactive les couleurs\n --profile <simple|advanced> Choisit le profil d'architecture
351
- --package-manager <pm> Définit le package manager du config init
352
- --dry-run Affiche le plan sans écrire`,
570
+ --package-manager <pm> Définit npm, pnpm ou yarn
571
+ --project-path <chemin> Définit le dossier parent du projet init
572
+ --docker, --no-docker Active ou désactive Docker pour init
573
+ --swagger, --no-swagger Active ou désactive Swagger pour init
574
+ --operations, --no-operations Active ou désactive le socle d’exploitation pour init
575
+ --git, --no-git Active ou désactive Git pour init
576
+ --modules <a,b> Définit les modules init, séparés par des virgules
577
+ --application <nom> Cible une application d’un workspace Nest
578
+ --update Met à jour une ressource déjà générée sans écraser les fichiers modifiés
579
+ --migration-name <nom> Prépare une commande de migration officielle avec --update, sans l’appliquer
580
+ --dry-run Affiche le plan sans écrire
581
+ --json Retourne un résultat JSON versionné
582
+ --indexes <a+b,c> Ajoute des index composites ou simples\n\nChamps resource : string, number, integer, decimal(precision;scale), enum(VALEUR|VALEUR), boolean, date, uuid.`),
353
583
  );
354
584
  }
355
585
 
356
- function runDoctor() {
357
- const checks = [
358
- [
359
- 'Node.js',
360
- process.versions.node,
361
- Number(process.versions.node.split('.')[0]) >= 24,
362
- 'Installe Node.js 24 LTS ou une version supportée.',
363
- ],
364
- [
365
- 'Scripts NestGen',
366
- `${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
367
- fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT),
368
- 'Réinstalle NestGen.',
369
- ],
370
- ];
371
- for (const [name, detail, valid, advice] of checks)
372
- console.log(`${valid ? 'OK' : 'ERREUR'} ${name}: ${detail}${valid ? '' : ` — ${advice}`}`);
586
+ function runDoctor(options = {}) {
587
+ const diagnostics = [];
588
+ const add = (id, severity, cause, action, detail) => diagnostics.push({ id, severity, cause, action, detail });
589
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
590
+ const supportedNode = (nodeMajor === 24 && nodeMinor >= 15) || nodeMajor === 25 || nodeMajor === 26;
591
+ add(
592
+ 'NODE_VERSION',
593
+ supportedNode ? 'INFO' : 'ERROR',
594
+ supportedNode ? 'Version Node.js compatible.' : 'Node.js doit être >=24.15.0 et <27.',
595
+ 'Installe une version Node.js supportée puis relance doctor.',
596
+ process.versions.node,
597
+ );
598
+ add(
599
+ 'GENERATOR_SCRIPTS',
600
+ fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT) ? 'INFO' : 'ERROR',
601
+ fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT)
602
+ ? 'Scripts du générateur disponibles.'
603
+ : 'Un script du générateur est introuvable.',
604
+ 'Réinstalle NestGen.',
605
+ `${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
606
+ );
607
+ const projectRoot = process.cwd();
608
+ const packagePath = path.join(projectRoot, 'package.json');
609
+ let manifest;
610
+ if (!fs.existsSync(packagePath)) {
611
+ add(
612
+ 'PROJECT_PACKAGE',
613
+ 'WARNING',
614
+ 'Aucun package.json de projet détecté.',
615
+ 'Exécute doctor à la racine du projet Nest.',
616
+ packagePath,
617
+ );
618
+ } else {
619
+ try {
620
+ manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
621
+ const dependencies = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
622
+ for (const dependency of ['@nestjs/common', '@nestjs/core'])
623
+ add(
624
+ `DEPENDENCY_${dependency.replace(/^@/, '').replaceAll(/[/-]/g, '_').toUpperCase()}`,
625
+ dependencies[dependency] ? 'INFO' : 'ERROR',
626
+ dependencies[dependency] ? `${dependency} est déclaré.` : `${dependency} est absent.`,
627
+ `Installe ${dependency} avec le package manager du projet.`,
628
+ dependencies[dependency] ?? '',
629
+ );
630
+ const nestVersion = String(dependencies['@nestjs/common'] ?? '');
631
+ if (nestVersion)
632
+ add(
633
+ 'NEST_VERSION',
634
+ /(?:\^|~)?12(?:\.|$)/.test(nestVersion) ? 'INFO' : 'WARNING',
635
+ /(?:\^|~)?12(?:\.|$)/.test(nestVersion)
636
+ ? 'La version Nest déclarée est supportée.'
637
+ : 'La version Nest déclarée est hors de la matrice testée.',
638
+ 'Utilise Nest 12 ou vérifie cette combinaison avec la matrice de compatibilité.',
639
+ nestVersion,
640
+ );
641
+ if (dependencies['@nestjs/typeorm'] || dependencies.typeorm)
642
+ add(
643
+ 'TYPEORM_COHERENCE',
644
+ dependencies['@nestjs/typeorm'] && dependencies.typeorm ? 'INFO' : 'WARNING',
645
+ dependencies['@nestjs/typeorm'] && dependencies.typeorm
646
+ ? 'Les dépendances TypeORM sont cohérentes.'
647
+ : 'Le support TypeORM est incomplet.',
648
+ 'Installe @nestjs/typeorm et typeorm ensemble, ou utilise Prisma.',
649
+ '',
650
+ );
651
+ if (dependencies['@prisma/client'])
652
+ add(
653
+ 'PRISMA_CLIENT',
654
+ 'INFO',
655
+ '@prisma/client est déclaré.',
656
+ 'Exécute npx prisma generate après une modification du schéma.',
657
+ dependencies['@prisma/client'],
658
+ );
659
+ } catch (error) {
660
+ add(
661
+ 'PROJECT_PACKAGE',
662
+ 'ERROR',
663
+ 'package.json est invalide.',
664
+ 'Corrige le JSON du manifeste.',
665
+ error.message,
666
+ );
667
+ }
668
+ }
373
669
  try {
374
- const project = inspectProject(process.cwd(), 'typeorm');
375
- console.log(
376
- `OK Projet Nest: ${project.packageManager ?? 'lockfile absent'}; connexion TypeORM racine: ${project.hasRootTypeOrmConnection ? 'présente' : 'absente'}.`,
670
+ const config = loadConfigIfPresent(projectRoot);
671
+ add(
672
+ 'NESTGEN_CONFIG',
673
+ 'INFO',
674
+ config
675
+ ? 'Configuration NestGen valide.'
676
+ : 'Configuration NestGen absente ; les valeurs par défaut seront utilisées.',
677
+ config ? 'Aucune action requise.' : 'Exécute nestgen config init pour figer les choix du projet.',
678
+ config ? `orm=${config.orm}; profile=${config.profile}; packageManager=${config.packageManager}` : '',
377
679
  );
680
+ if (config) {
681
+ const lockfile =
682
+ config.packageManager === 'npm'
683
+ ? 'package-lock.json'
684
+ : config.packageManager === 'pnpm'
685
+ ? 'pnpm-lock.yaml'
686
+ : 'yarn.lock';
687
+ add(
688
+ 'PACKAGE_MANAGER_LOCKFILE',
689
+ fs.existsSync(path.join(projectRoot, lockfile)) ? 'INFO' : 'WARNING',
690
+ fs.existsSync(path.join(projectRoot, lockfile))
691
+ ? `Le lockfile ${lockfile} correspond à la configuration.`
692
+ : `Le lockfile ${lockfile} est absent.`,
693
+ `Installe les dépendances avec ${config.packageManager} pour créer le lockfile attendu.`,
694
+ lockfile,
695
+ );
696
+ }
378
697
  } catch (error) {
379
- console.log(
380
- `INFO Projet Nest: ${error.message} — Lance doctor depuis un projet Nest compatible pour analyser son intégration.`,
698
+ add(
699
+ 'NESTGEN_CONFIG',
700
+ 'ERROR',
701
+ 'Configuration NestGen invalide.',
702
+ 'Corrige nestgen.config.json ou recrée-la.',
703
+ error.message,
381
704
  );
382
705
  }
383
- if (checks.some(([, , valid]) => !valid)) throw new Error('Des prérequis NestGen sont manquants.');
706
+ let project;
707
+ try {
708
+ project = inspectProject(projectRoot, 'typeorm', { cqrs: false });
709
+ add(
710
+ 'NEST_INTEGRATION',
711
+ 'INFO',
712
+ 'Structure Nest et intégration TypeORM valides.',
713
+ 'Aucune action requise.',
714
+ project.sourceRoot,
715
+ );
716
+ } catch (error) {
717
+ project = { error: error.message };
718
+ add(
719
+ 'NEST_INTEGRATION',
720
+ 'WARNING',
721
+ 'Le projet ne remplit pas toutes les préconditions TypeORM.',
722
+ 'Lis le diagnostic puis complète la structure ou les dépendances indiquées.',
723
+ error.message,
724
+ );
725
+ }
726
+ if (!options.json) {
727
+ const output = formatDoctorOutput(diagnostics, options);
728
+ if (output) console.log(output);
729
+ }
730
+ return { diagnostics, project };
731
+ }
732
+
733
+ function machineResult(command, result) {
734
+ return { version: 1, ok: true, command, result };
735
+ }
736
+
737
+ function errorCode(error) {
738
+ const message = error instanceof Error ? error.message : String(error);
739
+ if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
740
+ return 'USAGE';
741
+ if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
742
+ if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
743
+ return 'INTERNAL';
384
744
  }
385
745
 
386
746
  // ────── Entrée CLI
387
747
  export async function main(args = process.argv.slice(2)) {
388
748
  const parsed = parseCliArgs(args);
749
+ const machine = parsed.options.json;
750
+ if (machine) {
751
+ parsed.options.quiet = true;
752
+ parsed.options.color = false;
753
+ parsed.options.noInteractive = true;
754
+ }
389
755
  if (!parsed.options.color) chalk.level = 0;
390
756
  if (parsed.options.version) {
391
757
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
392
- console.log(packageJson.version);
393
- return;
758
+ const result = { cliVersion: packageJson.version };
759
+ console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
760
+ return result;
394
761
  }
395
762
  if (parsed.options.help || !parsed.command) {
396
- printUsage();
763
+ if (machine)
764
+ console.log(
765
+ JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
766
+ );
767
+ else printUsage();
397
768
  return;
398
769
  }
399
770
 
771
+ let result;
400
772
  switch (parsed.command) {
401
773
  case 'init':
402
- await runInteractiveInit(parsed.options);
774
+ if (parsed.positionals.length > 1) throw new Error('init accepte au plus un nom de projet.');
775
+ result = await runInteractiveInit({ ...parsed.options, projectName: parsed.positionals[0] });
403
776
  break;
404
777
 
405
778
  case 'module':
406
- await runModuleGeneration(parsed);
779
+ result = await runModuleGeneration(parsed);
407
780
  break;
408
781
 
409
782
  case 'resource':
410
- runResourceGeneration(parsed);
783
+ result = await runResourceGeneration(parsed);
411
784
  break;
412
785
 
413
786
  case 'config': {
414
787
  const action = parsed.positionals[0];
415
788
  if (action === 'show') {
416
- console.log(JSON.stringify(loadConfigIfPresent(process.cwd()) ?? defaultConfig, null, 2));
789
+ result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
790
+ if (!machine)
791
+ console.log(
792
+ formatConfig(
793
+ result,
794
+ fs.existsSync(path.join(process.cwd(), configFileName))
795
+ ? configFileName
796
+ : 'Valeurs par défaut',
797
+ ),
798
+ );
417
799
  break;
418
800
  }
419
801
  if (action === 'init') {
@@ -426,28 +808,50 @@ export async function main(args = process.argv.slice(2)) {
426
808
  if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
427
809
  throw new Error('Package manager invalide.');
428
810
  if (parsed.options.dryRun) {
429
- console.log(JSON.stringify({ operation: 'config-init', file: configFileName, config }, null, 2));
811
+ result = { operation: 'config-init', file: configFileName, config };
812
+ if (!parsed.options.quiet) console.log(formatConfig(config, 'Aperçu · aucun fichier écrit'));
430
813
  break;
431
814
  }
432
815
  writeConfig(process.cwd(), config);
433
- console.log(configFileName);
816
+ result = { file: configFileName, created: true, config };
817
+ if (!parsed.options.quiet) console.log(success(`Configuration créée : ${configFileName}`));
434
818
  break;
435
819
  }
436
820
  throw new Error('config requiert init ou show.');
437
821
  }
438
822
 
439
823
  case 'doctor':
440
- runDoctor();
824
+ result = runDoctor(parsed.options);
441
825
  break;
442
826
 
443
827
  default:
444
828
  throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
445
829
  }
830
+ if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
831
+ else if (!parsed.options.quiet && result) {
832
+ if (parsed.command === 'module' && result.generated)
833
+ console.log(success(`Module ${result.module} généré (${result.orm}).`));
834
+ if (parsed.command === 'resource' && result.generated) {
835
+ console.log(
836
+ success(
837
+ `Ressource ${result.resource} ${result.updated ? 'mise à jour' : 'générée'} (${result.orm} · ${result.profile}).`,
838
+ ),
839
+ );
840
+ if (result.migration) console.log(formatMigration(result.migration));
841
+ }
842
+ }
843
+ return result;
446
844
  }
447
845
 
448
846
  if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
449
847
  main().catch((error) => {
450
- console.error(chalk.red(`❌ ${error.message}`));
848
+ if (process.argv.includes('--json')) {
849
+ const message = error instanceof Error ? error.message : String(error);
850
+ console.error(JSON.stringify({ version: 1, ok: false, error: { code: errorCode(error), message } }));
851
+ } else {
852
+ if (process.argv.includes('--no-color')) chalk.level = 0;
853
+ console.error(formatError(error.message));
854
+ }
451
855
  process.exitCode = 1;
452
856
  });
453
857
  }