nestgen-cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/nestgen.js CHANGED
@@ -7,11 +7,17 @@ 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';
15
21
 
16
22
  // ────── Resolve __dirname compatible ES Modules
17
23
  const __filename = fileURLToPath(import.meta.url);
@@ -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,6 +205,25 @@ 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
229
  env: { ...process.env, ...env },
@@ -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('\n🚀 Lancement de la génération du projet...\n');
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,375 @@ 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(JSON.stringify(output, null, 2));
423
+ return output;
317
424
  }
318
425
 
319
426
  if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
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}`);
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}`);
323
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(JSON.stringify(output, null, 2));
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
+ `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(machine = false) {
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
+ add(
591
+ 'NODE_VERSION',
592
+ nodeMajor === 24 && nodeMinor >= 15 ? 'INFO' : 'ERROR',
593
+ nodeMajor === 24 && nodeMinor >= 15 ? 'Version Node.js compatible.' : 'Node.js doit être >=24.15.0 et <27.',
594
+ 'Installe une version Node.js supportée puis relance doctor.',
595
+ process.versions.node,
596
+ );
597
+ add(
598
+ 'GENERATOR_SCRIPTS',
599
+ fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT) ? 'INFO' : 'ERROR',
600
+ fs.existsSync(GENERATE_SCRIPT) && fs.existsSync(ADD_MODULE_SCRIPT)
601
+ ? 'Scripts du générateur disponibles.'
602
+ : 'Un script du générateur est introuvable.',
603
+ 'Réinstalle NestGen.',
604
+ `${GENERATE_SCRIPT}, ${ADD_MODULE_SCRIPT}`,
605
+ );
606
+ const projectRoot = process.cwd();
607
+ const packagePath = path.join(projectRoot, 'package.json');
608
+ let manifest;
609
+ if (!fs.existsSync(packagePath)) {
610
+ add(
611
+ 'PROJECT_PACKAGE',
612
+ 'WARNING',
613
+ 'Aucun package.json de projet détecté.',
614
+ 'Exécute doctor à la racine du projet Nest.',
615
+ packagePath,
616
+ );
617
+ } else {
618
+ try {
619
+ manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
620
+ const dependencies = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
621
+ for (const dependency of ['@nestjs/common', '@nestjs/core'])
622
+ add(
623
+ `DEPENDENCY_${dependency.replace(/^@/, '').replaceAll(/[/-]/g, '_').toUpperCase()}`,
624
+ dependencies[dependency] ? 'INFO' : 'ERROR',
625
+ dependencies[dependency] ? `${dependency} est déclaré.` : `${dependency} est absent.`,
626
+ `Installe ${dependency} avec le package manager du projet.`,
627
+ dependencies[dependency] ?? '',
628
+ );
629
+ const nestVersion = String(dependencies['@nestjs/common'] ?? '');
630
+ if (nestVersion)
631
+ add(
632
+ 'NEST_VERSION',
633
+ /(?:\^|~)?12(?:\.|$)/.test(nestVersion) ? 'INFO' : 'WARNING',
634
+ /(?:\^|~)?12(?:\.|$)/.test(nestVersion)
635
+ ? 'La version Nest déclarée est supportée.'
636
+ : 'La version Nest déclarée est hors de la matrice testée.',
637
+ 'Utilise Nest 12 ou vérifie cette combinaison avec la matrice de compatibilité.',
638
+ nestVersion,
639
+ );
640
+ if (dependencies['@nestjs/typeorm'] || dependencies.typeorm)
641
+ add(
642
+ 'TYPEORM_COHERENCE',
643
+ dependencies['@nestjs/typeorm'] && dependencies.typeorm ? 'INFO' : 'WARNING',
644
+ dependencies['@nestjs/typeorm'] && dependencies.typeorm
645
+ ? 'Les dépendances TypeORM sont cohérentes.'
646
+ : 'Le support TypeORM est incomplet.',
647
+ 'Installe @nestjs/typeorm et typeorm ensemble, ou utilise Prisma.',
648
+ '',
649
+ );
650
+ if (dependencies['@prisma/client'])
651
+ add(
652
+ 'PRISMA_CLIENT',
653
+ 'INFO',
654
+ '@prisma/client est déclaré.',
655
+ 'Exécute npx prisma generate après une modification du schéma.',
656
+ dependencies['@prisma/client'],
657
+ );
658
+ } catch (error) {
659
+ add(
660
+ 'PROJECT_PACKAGE',
661
+ 'ERROR',
662
+ 'package.json est invalide.',
663
+ 'Corrige le JSON du manifeste.',
664
+ error.message,
665
+ );
666
+ }
667
+ }
373
668
  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'}.`,
669
+ const config = loadConfigIfPresent(projectRoot);
670
+ add(
671
+ 'NESTGEN_CONFIG',
672
+ 'INFO',
673
+ config
674
+ ? 'Configuration NestGen valide.'
675
+ : 'Configuration NestGen absente ; les valeurs par défaut seront utilisées.',
676
+ config ? 'Aucune action requise.' : 'Exécute nestgen config init pour figer les choix du projet.',
677
+ config ? `orm=${config.orm}; profile=${config.profile}; packageManager=${config.packageManager}` : '',
377
678
  );
679
+ if (config) {
680
+ const lockfile =
681
+ config.packageManager === 'npm'
682
+ ? 'package-lock.json'
683
+ : config.packageManager === 'pnpm'
684
+ ? 'pnpm-lock.yaml'
685
+ : 'yarn.lock';
686
+ add(
687
+ 'PACKAGE_MANAGER_LOCKFILE',
688
+ fs.existsSync(path.join(projectRoot, lockfile)) ? 'INFO' : 'WARNING',
689
+ fs.existsSync(path.join(projectRoot, lockfile))
690
+ ? `Le lockfile ${lockfile} correspond à la configuration.`
691
+ : `Le lockfile ${lockfile} est absent.`,
692
+ `Installe les dépendances avec ${config.packageManager} pour créer le lockfile attendu.`,
693
+ lockfile,
694
+ );
695
+ }
378
696
  } catch (error) {
379
- console.log(
380
- `INFO Projet Nest: ${error.message} — Lance doctor depuis un projet Nest compatible pour analyser son intégration.`,
697
+ add(
698
+ 'NESTGEN_CONFIG',
699
+ 'ERROR',
700
+ 'Configuration NestGen invalide.',
701
+ 'Corrige nestgen.config.json ou recrée-la.',
702
+ error.message,
381
703
  );
382
704
  }
383
- if (checks.some(([, , valid]) => !valid)) throw new Error('Des prérequis NestGen sont manquants.');
705
+ let project;
706
+ try {
707
+ project = inspectProject(projectRoot, 'typeorm', { cqrs: false });
708
+ add(
709
+ 'NEST_INTEGRATION',
710
+ 'INFO',
711
+ 'Structure Nest et intégration TypeORM valides.',
712
+ 'Aucune action requise.',
713
+ project.sourceRoot,
714
+ );
715
+ } catch (error) {
716
+ project = { error: error.message };
717
+ add(
718
+ 'NEST_INTEGRATION',
719
+ 'WARNING',
720
+ 'Le projet ne remplit pas toutes les préconditions TypeORM.',
721
+ 'Lis le diagnostic puis complète la structure ou les dépendances indiquées.',
722
+ error.message,
723
+ );
724
+ }
725
+ if (!machine)
726
+ for (const diagnostic of diagnostics)
727
+ console.log(`${diagnostic.severity} ${diagnostic.id}: ${diagnostic.cause} — ${diagnostic.action}`);
728
+ return { diagnostics, project };
729
+ }
730
+
731
+ function machineResult(command, result) {
732
+ return { version: 1, ok: true, command, result };
733
+ }
734
+
735
+ function errorCode(error) {
736
+ const message = error instanceof Error ? error.message : String(error);
737
+ if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
738
+ return 'USAGE';
739
+ if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
740
+ if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
741
+ return 'INTERNAL';
384
742
  }
385
743
 
386
744
  // ────── Entrée CLI
387
745
  export async function main(args = process.argv.slice(2)) {
388
746
  const parsed = parseCliArgs(args);
747
+ const machine = parsed.options.json;
748
+ if (machine) {
749
+ parsed.options.quiet = true;
750
+ parsed.options.color = false;
751
+ parsed.options.noInteractive = true;
752
+ }
389
753
  if (!parsed.options.color) chalk.level = 0;
390
754
  if (parsed.options.version) {
391
755
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
392
- console.log(packageJson.version);
393
- return;
756
+ const result = { cliVersion: packageJson.version };
757
+ console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
758
+ return result;
394
759
  }
395
760
  if (parsed.options.help || !parsed.command) {
396
- printUsage();
761
+ if (machine)
762
+ console.log(
763
+ JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
764
+ );
765
+ else printUsage();
397
766
  return;
398
767
  }
399
768
 
769
+ let result;
400
770
  switch (parsed.command) {
401
771
  case 'init':
402
- await runInteractiveInit(parsed.options);
772
+ if (parsed.positionals.length > 1) throw new Error('init accepte au plus un nom de projet.');
773
+ result = await runInteractiveInit({ ...parsed.options, projectName: parsed.positionals[0] });
403
774
  break;
404
775
 
405
776
  case 'module':
406
- await runModuleGeneration(parsed);
777
+ result = await runModuleGeneration(parsed);
407
778
  break;
408
779
 
409
780
  case 'resource':
410
- runResourceGeneration(parsed);
781
+ result = await runResourceGeneration(parsed);
411
782
  break;
412
783
 
413
784
  case 'config': {
414
785
  const action = parsed.positionals[0];
415
786
  if (action === 'show') {
416
- console.log(JSON.stringify(loadConfigIfPresent(process.cwd()) ?? defaultConfig, null, 2));
787
+ result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
788
+ if (!machine) console.log(JSON.stringify(result, null, 2));
417
789
  break;
418
790
  }
419
791
  if (action === 'init') {
@@ -426,28 +798,35 @@ export async function main(args = process.argv.slice(2)) {
426
798
  if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
427
799
  throw new Error('Package manager invalide.');
428
800
  if (parsed.options.dryRun) {
429
- console.log(JSON.stringify({ operation: 'config-init', file: configFileName, config }, null, 2));
801
+ result = { operation: 'config-init', file: configFileName, config };
802
+ if (!machine) console.log(JSON.stringify(result, null, 2));
430
803
  break;
431
804
  }
432
805
  writeConfig(process.cwd(), config);
433
- console.log(configFileName);
806
+ result = { file: configFileName, created: true, config };
807
+ if (!machine) console.log(configFileName);
434
808
  break;
435
809
  }
436
810
  throw new Error('config requiert init ou show.');
437
811
  }
438
812
 
439
813
  case 'doctor':
440
- runDoctor();
814
+ result = runDoctor(machine);
441
815
  break;
442
816
 
443
817
  default:
444
818
  throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
445
819
  }
820
+ if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
821
+ return result;
446
822
  }
447
823
 
448
824
  if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
449
825
  main().catch((error) => {
450
- console.error(chalk.red(`❌ ${error.message}`));
826
+ if (process.argv.includes('--json')) {
827
+ const message = error instanceof Error ? error.message : String(error);
828
+ console.error(JSON.stringify({ version: 1, ok: false, error: { code: errorCode(error), message } }));
829
+ } else console.error(chalk.red(`❌ ${error.message}`));
451
830
  process.exitCode = 1;
452
831
  });
453
832
  }