nestgen-cli 0.0.7 → 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.
Files changed (35) hide show
  1. package/dist/engine/architecture-profile.js +10 -0
  2. package/dist/engine/file-transaction.js +166 -0
  3. package/dist/engine/generated-code.js +26 -0
  4. package/dist/engine/generation-manifest.js +118 -0
  5. package/dist/engine/migration-plan.js +78 -0
  6. package/dist/engine/module-generator.js +157 -0
  7. package/dist/engine/project-config.js +42 -0
  8. package/dist/engine/project-path.js +25 -0
  9. package/dist/engine/project-preflight.js +96 -0
  10. package/dist/engine/resource-generator.js +1153 -0
  11. package/dist/engine/resource-spec.js +287 -0
  12. package/examples/store-api/README.md +76 -0
  13. package/examples/store-api/definitions/customers.resource.json +14 -0
  14. package/examples/store-api/definitions/order-items.resource.json +26 -0
  15. package/examples/store-api/definitions/orders.resource.json +22 -0
  16. package/examples/store-api/definitions/products.resource.json +19 -0
  17. package/examples/store-api/nestgen.example.json +7 -0
  18. package/nestgen.js +700 -86
  19. package/nestjs-generator/features/add_module.sh +68 -24
  20. package/nestjs-generator/features/configure_bootstrap.mjs +42 -0
  21. package/nestjs-generator/features/docker.sh +169 -36
  22. package/nestjs-generator/features/git.sh +4 -2
  23. package/nestjs-generator/features/inject_module_to_app.sh +22 -69
  24. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  25. package/nestjs-generator/features/operations.sh +16 -0
  26. package/nestjs-generator/features/preflight.mjs +13 -0
  27. package/nestjs-generator/features/prisma.sh +48 -8
  28. package/nestjs-generator/features/resource_name.mjs +57 -0
  29. package/nestjs-generator/features/swagger.sh +7 -20
  30. package/nestjs-generator/features/typeorm.sh +68 -7
  31. package/nestjs-generator/features/update_app_module.mjs +146 -0
  32. package/nestjs-generator/features/utils.sh +52 -14
  33. package/nestjs-generator/generate_project.sh +46 -70
  34. package/package.json +63 -31
  35. package/readme.md +298 -120
package/nestgen.js CHANGED
@@ -1,11 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execSync } from 'child_process';
3
+ import { spawnSync } from 'child_process';
4
4
  import inquirer from 'inquirer';
5
5
  import chalk from 'chalk';
6
6
  import path from 'path';
7
7
  import fs from 'fs';
8
8
  import { fileURLToPath } from 'url';
9
+ import { inspectProject } from './nestjs-generator/features/preflight.mjs';
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';
18
+ import { configFileName, defaultConfig, loadConfigIfPresent, writeConfig } from './dist/engine/project-config.js';
19
+ import { parseArchitectureProfile } from './dist/engine/architecture-profile.js';
20
+ import { planResourceMigration } from './dist/engine/migration-plan.js';
9
21
 
10
22
  // ────── Resolve __dirname compatible ES Modules
11
23
  const __filename = fileURLToPath(import.meta.url);
@@ -15,30 +27,216 @@ const __dirname = path.dirname(__filename);
15
27
  const ROOT_PATH = process.env.NESTGEN_ROOT || path.resolve(__dirname, './nestjs-generator');
16
28
  const GENERATE_SCRIPT = path.join(ROOT_PATH, 'generate_project.sh');
17
29
  const ADD_MODULE_SCRIPT = path.join(ROOT_PATH, './features/add_module.sh');
30
+ const SUPPORTED_ORMS = new Set(['typeorm', 'prisma']);
31
+ const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
18
32
 
19
33
  // ────── Logo CLI
20
34
  function printLogo() {
21
- console.log(chalk.magentaBright(`
35
+ console.log(
36
+ chalk.magentaBright(`
22
37
  ███╗ ██╗███████╗███████╗████████╗ ██████╗ ███████╗███╗ ██╗
23
38
  ████╗ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║
24
39
  ██╔██╗ ██║█████╗ ███████╗ ██║ ██║ ███╗█████╗ ██╔██╗ ██║
25
40
  ██║╚██╗██║██╔══╝ ╚════██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║
26
41
  ██║ ╚████║███████╗███████║ ██║ ╚██████╔╝███████╗██║ ╚████║
27
42
  ╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
28
- `));
43
+ `),
44
+ );
29
45
  console.log(chalk.cyan.bold('✨ NestGen CLI — Générateur modulaire NestJS'));
30
46
  }
31
47
 
32
48
  // ────── Helpers
33
- function isNestProject() {
49
+ export function isNestProject() {
34
50
  return fs.existsSync(path.resolve('./src/app.module.ts'));
35
51
  }
36
52
 
37
- function parseModuleArgs(args) {
38
- const moduleName = args[1];
39
- const ormArg = args.find(arg => arg.startsWith('--orm='));
40
- const orm = ormArg ? ormArg.split('=')[1] : 'typeorm';
41
- return { moduleName, orm };
53
+ export function validateModuleName(value) {
54
+ const moduleName = String(value ?? '').trim();
55
+
56
+ if (!MODULE_NAME_PATTERN.test(moduleName)) {
57
+ throw new Error(
58
+ 'Le nom du module doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores.',
59
+ );
60
+ }
61
+
62
+ return moduleName.toLowerCase();
63
+ }
64
+
65
+ export function validateOrm(value) {
66
+ const orm = String(value ?? '')
67
+ .trim()
68
+ .toLowerCase();
69
+
70
+ if (!SUPPORTED_ORMS.has(orm)) {
71
+ throw new Error(`ORM non supporté : ${value}. Valeurs acceptées : ${[...SUPPORTED_ORMS].join(', ')}.`);
72
+ }
73
+
74
+ return orm;
75
+ }
76
+
77
+ function validatePackageManager(value) {
78
+ if (!['npm', 'pnpm', 'yarn'].includes(value)) throw new Error('Package manager invalide.');
79
+ return value;
80
+ }
81
+
82
+ export function resolveProjectPath(value) {
83
+ const projectPath = String(value ?? '').trim();
84
+
85
+ if (!projectPath || projectPath.includes('\0')) {
86
+ throw new Error('Le dossier cible doit être un chemin non vide valide.');
87
+ }
88
+
89
+ return path.resolve(projectPath);
90
+ }
91
+
92
+ export function parseModuleArgs(args) {
93
+ const parsed = parseCliArgs(args);
94
+ if (parsed.command !== 'module' || !parsed.positionals[0])
95
+ throw new Error('La commande module requiert un nom de module.');
96
+ return { moduleName: validateModuleName(parsed.positionals[0]), orm: validateOrm(parsed.options.orm ?? 'typeorm') };
97
+ }
98
+
99
+ export function parseCliArgs(args) {
100
+ const options = {
101
+ orm: undefined,
102
+ profile: undefined,
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,
113
+ noInteractive: false,
114
+ quiet: false,
115
+ verbose: false,
116
+ color: true,
117
+ help: false,
118
+ version: false,
119
+ dryRun: false,
120
+ update: false,
121
+ migrationName: undefined,
122
+ json: false,
123
+ };
124
+ const positionals = [];
125
+ for (let index = 0; index < args.length; index += 1) {
126
+ const argument = args[index];
127
+ if (argument === '--orm') {
128
+ const value = args[++index];
129
+ if (!value || value.startsWith('-')) throw new Error('--orm requiert une valeur.');
130
+ options.orm = value;
131
+ } else if (argument.startsWith('--orm=')) options.orm = argument.slice(6);
132
+ else if (argument === '--profile') options.profile = args[++index];
133
+ else if (argument.startsWith('--profile=')) options.profile = argument.slice(10);
134
+ else if (argument === '--package-manager') options.packageManager = args[++index];
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);
153
+ else if (argument === '--fields') options.fields = args[++index];
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);
159
+ else if (argument === '--route') options.route = args[++index];
160
+ else if (argument.startsWith('--route=')) options.route = argument.slice(8);
161
+ else if (argument === '--table') options.table = args[++index];
162
+ else if (argument.startsWith('--table=')) options.table = argument.slice(8);
163
+ else if (argument === '--no-interactive') options.noInteractive = true;
164
+ else if (argument === '--quiet') options.quiet = true;
165
+ else if (argument === '--verbose') options.verbose = true;
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;
171
+ else if (argument === '--no-color') options.color = false;
172
+ else if (argument === '--help' || argument === '-h') options.help = true;
173
+ else if (argument === '--version' || argument === '-V') options.version = true;
174
+ else if (argument.startsWith('-')) throw new Error(`Option inconnue : ${argument}`);
175
+ else positionals.push(argument);
176
+ }
177
+ return { command: positionals.shift(), positionals, options };
178
+ }
179
+
180
+ export function createModulePlan(projectRoot, moduleName, orm, sourceRoot = 'src') {
181
+ const normalizedName = validateModuleName(moduleName);
182
+ const normalizedOrm = validateOrm(orm);
183
+ const resourceRoot = `${sourceRoot}/app/${normalizedName}`;
184
+ return {
185
+ operation: 'module',
186
+ module: normalizedName,
187
+ orm: normalizedOrm,
188
+ projectRoot,
189
+ files: [
190
+ `${resourceRoot}/core/domain/entities/${normalizedName}.entity.ts`,
191
+ `${resourceRoot}/core/domain/ports/${normalizedName}.repository.ts`,
192
+ `${resourceRoot}/core/application/commands/create-${normalizedName}.command.ts`,
193
+ `${resourceRoot}/core/application/commands/create-${normalizedName}.handler.ts`,
194
+ `${resourceRoot}/interfaces/dtos/create-${normalizedName}.dto.ts`,
195
+ `${resourceRoot}/interfaces/controllers/${normalizedName}.controller.ts`,
196
+ `${resourceRoot}/${normalizedName}.module.ts`,
197
+ ],
198
+ mutations: [
199
+ `Ajoute le module dans @Module({ imports }) de ${sourceRoot}/app.module.ts.`,
200
+ `Ajoute CqrsModule dans ${sourceRoot}/app.module.ts si nécessaire.`,
201
+ ...(normalizedOrm === 'typeorm'
202
+ ? ['Ajoute la configuration TypeORM racine seulement si elle est absente.']
203
+ : []),
204
+ ],
205
+ };
206
+ }
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
+
227
+ export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
228
+ const result = spawnSync('bash', [scriptPath, ...args], {
229
+ env: { ...process.env, ...env },
230
+ stdio: quiet ? 'pipe' : 'inherit',
231
+ });
232
+
233
+ if (result.error) {
234
+ throw result.error;
235
+ }
236
+
237
+ if (result.status !== 0) {
238
+ throw new Error(`Le script ${path.basename(scriptPath)} a échoué avec le code ${result.status ?? 'inconnu'}.`);
239
+ }
42
240
  }
43
241
 
44
242
  // ────── Commande : INIT
@@ -49,12 +247,14 @@ async function askInitQuestions() {
49
247
  name: 'projectName',
50
248
  message: '📛 Nom du projet :',
51
249
  default: 'my-app',
250
+ validate: validatePromptValue(validateModuleName),
52
251
  },
53
252
  {
54
253
  type: 'input',
55
254
  name: 'projectPath',
56
255
  message: '📁 Dossier cible :',
57
256
  default: './',
257
+ validate: validatePromptValue(resolveProjectPath),
58
258
  },
59
259
  {
60
260
  type: 'list',
@@ -82,6 +282,12 @@ async function askInitQuestions() {
82
282
  message: '🐳 Activer Docker ?',
83
283
  default: false,
84
284
  },
285
+ {
286
+ type: 'confirm',
287
+ name: 'withOperations',
288
+ message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
289
+ default: false,
290
+ },
85
291
  {
86
292
  type: 'confirm',
87
293
  name: 'withGit',
@@ -93,76 +299,106 @@ async function askInitQuestions() {
93
299
  name: 'modules',
94
300
  message: '📦 Modules à générer (séparés par des espaces) :',
95
301
  default: 'user',
96
- filter: (input) => input.split(' ').map(s => s.trim()).filter(Boolean),
97
- }
302
+ filter: (input) =>
303
+ input
304
+ .split(' ')
305
+ .map((s) => s.trim())
306
+ .filter(Boolean),
307
+ validate: validatePromptValue((modules) => modules.forEach(validateModuleName)),
308
+ },
98
309
  ]);
99
310
  }
100
311
 
101
- async function runInteractiveInit() {
102
- printLogo();
312
+ function validatePromptValue(validator) {
313
+ return (value) => {
314
+ try {
315
+ validator(value);
316
+ return true;
317
+ } catch (error) {
318
+ return error.message;
319
+ }
320
+ };
321
+ }
103
322
 
323
+ async function runInteractiveInit(options) {
324
+ if (!options.quiet) printLogo();
104
325
  if (!fs.existsSync(GENERATE_SCRIPT)) {
105
- console.log(chalk.red(`❌ Script introuvable : ${GENERATE_SCRIPT}`));
106
- process.exit(1);
326
+ throw new Error(`Script introuvable : ${GENERATE_SCRIPT}`);
107
327
  }
108
328
 
109
- const answers = await askInitQuestions();
110
- const {
111
- projectName,
112
- projectPath,
113
- packageManager,
114
- orm,
115
- withSwagger,
116
- withDocker,
117
- withGit,
118
- modules,
119
- } = 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;
120
360
 
121
361
  const env = {
122
- APP_NAME: projectName,
123
- PROJECT_PATH: path.resolve(projectPath),
124
- PM: packageManager,
125
- ORM: orm,
362
+ APP_NAME: validateModuleName(projectName),
363
+ PROJECT_PATH: resolveProjectPath(projectPath),
364
+ PM: validatePackageManager(packageManager),
365
+ ORM: validateOrm(orm),
126
366
  WITH_SWAGGER: withSwagger ? 'y' : 'n',
367
+ WITH_OPERATIONS: withOperations ? 'y' : 'n',
127
368
  WITH_DOCKER: withDocker ? 'y' : 'n',
128
369
  WITH_GIT: withGit ? 'y' : 'n',
129
- MODULES: modules.join(' '),
370
+ MODULES: modules.map(validateModuleName).join(' '),
130
371
  };
131
372
 
132
- const envExport = Object.entries(env)
133
- .map(([key, val]) => `${key}="${val}"`)
134
- .join(' ');
135
-
136
- console.log('\n🚀 Lancement de la génération du projet...\n');
137
- try {
138
- execSync(`env ${envExport} bash "${GENERATE_SCRIPT}"`, {
139
- stdio: 'inherit',
140
- });
141
- } catch (err) {
142
- console.error(chalk.red('❌ Une erreur est survenue pendant la génération.'));
143
- process.exit(1);
144
- }
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
+ };
145
382
  }
146
383
 
147
384
  // ────── Commande : MODULE
148
- async function runModuleGeneration(args) {
149
- printLogo();
385
+ async function runModuleGeneration(parsed) {
386
+ if (!parsed.options.quiet) printLogo();
387
+ const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
150
388
 
151
389
  let moduleName, orm;
152
390
 
153
- if (args.length > 1) {
154
- ({ moduleName, orm } = parseModuleArgs(args));
155
- if (!moduleName) {
156
- console.log(chalk.red('❌ Tu dois fournir un nom de module.'));
157
- process.exit(1);
158
- }
391
+ if (parsed.positionals[0]) {
392
+ moduleName = validateModuleName(parsed.positionals[0]);
393
+ orm = validateOrm(parsed.options.orm ?? config.orm);
159
394
  } else {
395
+ if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
160
396
  const answers = await inquirer.prompt([
161
397
  {
162
398
  type: 'input',
163
399
  name: 'moduleName',
164
400
  message: '📦 Nom du module :',
165
- validate: input => !!input || 'Le nom du module est requis',
401
+ validate: validatePromptValue(validateModuleName),
166
402
  },
167
403
  {
168
404
  type: 'list',
@@ -172,47 +408,425 @@ async function runModuleGeneration(args) {
172
408
  default: 'typeorm',
173
409
  },
174
410
  ]);
175
- moduleName = answers.moduleName;
176
- orm = answers.orm;
411
+ moduleName = validateModuleName(answers.moduleName);
412
+ orm = validateOrm(answers.orm);
177
413
  }
178
414
 
179
415
  if (!isNestProject()) {
180
- console.log(chalk.red('❌ Aucun projet NestJS détecté dans ce dossier.'));
181
- console.log('👉 Lance cette commande depuis un projet généré avec `nestgen init`.');
182
- process.exit(1);
416
+ throw new Error('Aucun projet NestJS détecté dans ce dossier. Lance cette commande depuis un projet NestJS.');
417
+ }
418
+
419
+ if (parsed.options.dryRun) {
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;
183
424
  }
184
425
 
426
+ if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
427
+ if (process.env.NESTGEN_ROOT) {
428
+ runBashScript(ADD_MODULE_SCRIPT, [moduleName, orm], {}, parsed.options.quiet);
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}`);
489
+ }
490
+ return value;
491
+ }
492
+
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
+ }));
499
+ }
500
+
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.');
506
+ const config = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
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`;
520
+ if (!/^[a-z][a-z0-9/-]*$/.test(route) || !/^[a-z][a-z0-9_]*$/.test(table))
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;
548
+ if (parsed.options.dryRun) {
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;
553
+ }
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
+ };
563
+ }
564
+
565
+ function printUsage() {
566
+ console.log(
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
568
+ config init|show Gère nestgen.config.json
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
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.`,
583
+ );
584
+ }
585
+
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
+ }
185
668
  try {
186
- console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
187
- execSync(`bash "${ADD_MODULE_SCRIPT}" "${moduleName}" "${orm}"`, {
188
- stdio: 'inherit',
189
- });
190
- } catch (err) {
191
- console.error(chalk.red('❌ Une erreur est survenue pendant la génération du module.'));
192
- process.exit(1);
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}` : '',
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
+ }
696
+ } catch (error) {
697
+ add(
698
+ 'NESTGEN_CONFIG',
699
+ 'ERROR',
700
+ 'Configuration NestGen invalide.',
701
+ 'Corrige nestgen.config.json ou recrée-la.',
702
+ error.message,
703
+ );
193
704
  }
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';
194
742
  }
195
743
 
196
744
  // ────── Entrée CLI
197
- const args = process.argv.slice(2);
198
- const command = args[0];
199
-
200
- switch (command) {
201
- case 'init':
202
- await runInteractiveInit();
203
- break;
204
-
205
- case 'module':
206
- await runModuleGeneration(args);
207
- break;
208
-
209
- default:
210
- printLogo();
211
- console.log(chalk.gray(`
212
- 📘 Commandes disponibles :
213
- ▸ nestgen init → Génère un projet complet NestJS (interactive)
214
- ▸ nestgen module [nom] [--orm=xxx] → Génère un module (interactive ou CLI)
215
- ▸ nestgen doctor → Diagnostic de l’installation CLI
216
- `));
217
- break;
745
+ export async function main(args = process.argv.slice(2)) {
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
+ }
753
+ if (!parsed.options.color) chalk.level = 0;
754
+ if (parsed.options.version) {
755
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
756
+ const result = { cliVersion: packageJson.version };
757
+ console.log(machine ? JSON.stringify(machineResult('version', result)) : packageJson.version);
758
+ return result;
759
+ }
760
+ if (parsed.options.help || !parsed.command) {
761
+ if (machine)
762
+ console.log(
763
+ JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
764
+ );
765
+ else printUsage();
766
+ return;
767
+ }
768
+
769
+ let result;
770
+ switch (parsed.command) {
771
+ case 'init':
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] });
774
+ break;
775
+
776
+ case 'module':
777
+ result = await runModuleGeneration(parsed);
778
+ break;
779
+
780
+ case 'resource':
781
+ result = await runResourceGeneration(parsed);
782
+ break;
783
+
784
+ case 'config': {
785
+ const action = parsed.positionals[0];
786
+ if (action === 'show') {
787
+ result = loadConfigIfPresent(process.cwd()) ?? defaultConfig;
788
+ if (!machine) console.log(JSON.stringify(result, null, 2));
789
+ break;
790
+ }
791
+ if (action === 'init') {
792
+ const config = {
793
+ ...defaultConfig,
794
+ orm: validateOrm(parsed.options.orm ?? defaultConfig.orm),
795
+ profile: parseArchitectureProfile(parsed.options.profile),
796
+ packageManager: parsed.options.packageManager ?? defaultConfig.packageManager,
797
+ };
798
+ if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
799
+ throw new Error('Package manager invalide.');
800
+ if (parsed.options.dryRun) {
801
+ result = { operation: 'config-init', file: configFileName, config };
802
+ if (!machine) console.log(JSON.stringify(result, null, 2));
803
+ break;
804
+ }
805
+ writeConfig(process.cwd(), config);
806
+ result = { file: configFileName, created: true, config };
807
+ if (!machine) console.log(configFileName);
808
+ break;
809
+ }
810
+ throw new Error('config requiert init ou show.');
811
+ }
812
+
813
+ case 'doctor':
814
+ result = runDoctor(machine);
815
+ break;
816
+
817
+ default:
818
+ throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
819
+ }
820
+ if (machine) console.log(JSON.stringify(machineResult(parsed.command, result)));
821
+ return result;
822
+ }
823
+
824
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
825
+ main().catch((error) => {
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}`));
830
+ process.exitCode = 1;
831
+ });
218
832
  }