nestgen-cli 0.0.7 → 0.1.0

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
@@ -1,11 +1,17 @@
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 } from './dist/engine/module-generator.js';
11
+ import { generateResource } from './dist/engine/resource-generator.js';
12
+ import { parseResourceFields } from './dist/engine/resource-spec.js';
13
+ import { configFileName, defaultConfig, loadConfigIfPresent, writeConfig } from './dist/engine/project-config.js';
14
+ import { parseArchitectureProfile } from './dist/engine/architecture-profile.js';
9
15
 
10
16
  // ────── Resolve __dirname compatible ES Modules
11
17
  const __filename = fileURLToPath(import.meta.url);
@@ -15,30 +21,155 @@ const __dirname = path.dirname(__filename);
15
21
  const ROOT_PATH = process.env.NESTGEN_ROOT || path.resolve(__dirname, './nestjs-generator');
16
22
  const GENERATE_SCRIPT = path.join(ROOT_PATH, 'generate_project.sh');
17
23
  const ADD_MODULE_SCRIPT = path.join(ROOT_PATH, './features/add_module.sh');
24
+ const SUPPORTED_ORMS = new Set(['typeorm', 'prisma']);
25
+ const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i;
18
26
 
19
27
  // ────── Logo CLI
20
28
  function printLogo() {
21
- console.log(chalk.magentaBright(`
29
+ console.log(
30
+ chalk.magentaBright(`
22
31
  ███╗ ██╗███████╗███████╗████████╗ ██████╗ ███████╗███╗ ██╗
23
32
  ████╗ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██╔════╝████╗ ██║
24
33
  ██╔██╗ ██║█████╗ ███████╗ ██║ ██║ ███╗█████╗ ██╔██╗ ██║
25
34
  ██║╚██╗██║██╔══╝ ╚════██║ ██║ ██║ ██║██╔══╝ ██║╚██╗██║
26
35
  ██║ ╚████║███████╗███████║ ██║ ╚██████╔╝███████╗██║ ╚████║
27
36
  ╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
28
- `));
37
+ `),
38
+ );
29
39
  console.log(chalk.cyan.bold('✨ NestGen CLI — Générateur modulaire NestJS'));
30
40
  }
31
41
 
32
42
  // ────── Helpers
33
- function isNestProject() {
43
+ export function isNestProject() {
34
44
  return fs.existsSync(path.resolve('./src/app.module.ts'));
35
45
  }
36
46
 
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 };
47
+ export function validateModuleName(value) {
48
+ const moduleName = String(value ?? '').trim();
49
+
50
+ if (!MODULE_NAME_PATTERN.test(moduleName)) {
51
+ throw new Error(
52
+ 'Le nom du module doit commencer par une lettre et ne contenir que des lettres, chiffres, tirets ou underscores.',
53
+ );
54
+ }
55
+
56
+ return moduleName.toLowerCase();
57
+ }
58
+
59
+ export function validateOrm(value) {
60
+ const orm = String(value ?? '')
61
+ .trim()
62
+ .toLowerCase();
63
+
64
+ if (!SUPPORTED_ORMS.has(orm)) {
65
+ throw new Error(`ORM non supporté : ${value}. Valeurs acceptées : ${[...SUPPORTED_ORMS].join(', ')}.`);
66
+ }
67
+
68
+ return orm;
69
+ }
70
+
71
+ export function resolveProjectPath(value) {
72
+ const projectPath = String(value ?? '').trim();
73
+
74
+ if (!projectPath || projectPath.includes('\0')) {
75
+ throw new Error('Le dossier cible doit être un chemin non vide valide.');
76
+ }
77
+
78
+ return path.resolve(projectPath);
79
+ }
80
+
81
+ export function parseModuleArgs(args) {
82
+ const parsed = parseCliArgs(args);
83
+ if (parsed.command !== 'module' || !parsed.positionals[0])
84
+ throw new Error('La commande module requiert un nom de module.');
85
+ return { moduleName: validateModuleName(parsed.positionals[0]), orm: validateOrm(parsed.options.orm ?? 'typeorm') };
86
+ }
87
+
88
+ export function parseCliArgs(args) {
89
+ const options = {
90
+ orm: undefined,
91
+ profile: undefined,
92
+ packageManager: undefined,
93
+ noInteractive: false,
94
+ quiet: false,
95
+ verbose: false,
96
+ color: true,
97
+ help: false,
98
+ version: false,
99
+ dryRun: false,
100
+ };
101
+ const positionals = [];
102
+ for (let index = 0; index < args.length; index += 1) {
103
+ const argument = args[index];
104
+ if (argument === '--orm') {
105
+ const value = args[++index];
106
+ if (!value || value.startsWith('-')) throw new Error('--orm requiert une valeur.');
107
+ options.orm = value;
108
+ } else if (argument.startsWith('--orm=')) options.orm = argument.slice(6);
109
+ else if (argument === '--profile') options.profile = args[++index];
110
+ else if (argument.startsWith('--profile=')) options.profile = argument.slice(10);
111
+ else if (argument === '--package-manager') options.packageManager = args[++index];
112
+ else if (argument.startsWith('--package-manager=')) options.packageManager = argument.slice(18);
113
+ else if (argument === '--fields') options.fields = args[++index];
114
+ else if (argument.startsWith('--fields=')) options.fields = argument.slice(9);
115
+ else if (argument === '--route') options.route = args[++index];
116
+ else if (argument.startsWith('--route=')) options.route = argument.slice(8);
117
+ else if (argument === '--table') options.table = args[++index];
118
+ else if (argument.startsWith('--table=')) options.table = argument.slice(8);
119
+ else if (argument === '--no-interactive') options.noInteractive = true;
120
+ else if (argument === '--quiet') options.quiet = true;
121
+ else if (argument === '--verbose') options.verbose = true;
122
+ else if (argument === '--dry-run') options.dryRun = true;
123
+ else if (argument === '--no-color') options.color = false;
124
+ else if (argument === '--help' || argument === '-h') options.help = true;
125
+ else if (argument === '--version' || argument === '-V') options.version = true;
126
+ else if (argument.startsWith('-')) throw new Error(`Option inconnue : ${argument}`);
127
+ else positionals.push(argument);
128
+ }
129
+ return { command: positionals.shift(), positionals, options };
130
+ }
131
+
132
+ export function createModulePlan(projectRoot, moduleName, orm) {
133
+ const normalizedName = validateModuleName(moduleName);
134
+ const normalizedOrm = validateOrm(orm);
135
+ const resourceRoot = `src/app/${normalizedName}`;
136
+ return {
137
+ operation: 'module',
138
+ module: normalizedName,
139
+ orm: normalizedOrm,
140
+ projectRoot,
141
+ files: [
142
+ `${resourceRoot}/core/domain/entities/${normalizedName}.entity.ts`,
143
+ `${resourceRoot}/core/domain/ports/${normalizedName}.repository.ts`,
144
+ `${resourceRoot}/core/application/commands/create-${normalizedName}.command.ts`,
145
+ `${resourceRoot}/core/application/commands/create-${normalizedName}.handler.ts`,
146
+ `${resourceRoot}/interfaces/dtos/create-${normalizedName}.dto.ts`,
147
+ `${resourceRoot}/interfaces/controllers/${normalizedName}.controller.ts`,
148
+ `${resourceRoot}/${normalizedName}.module.ts`,
149
+ ],
150
+ mutations: [
151
+ 'Ajoute le module dans @Module({ imports }) de src/app.module.ts.',
152
+ 'Ajoute CqrsModule dans src/app.module.ts si nécessaire.',
153
+ ...(normalizedOrm === 'typeorm'
154
+ ? ['Ajoute la configuration TypeORM racine seulement si elle est absente.']
155
+ : []),
156
+ ],
157
+ };
158
+ }
159
+
160
+ export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
161
+ const result = spawnSync('bash', [scriptPath, ...args], {
162
+ env: { ...process.env, ...env },
163
+ stdio: quiet ? 'pipe' : 'inherit',
164
+ });
165
+
166
+ if (result.error) {
167
+ throw result.error;
168
+ }
169
+
170
+ if (result.status !== 0) {
171
+ throw new Error(`Le script ${path.basename(scriptPath)} a échoué avec le code ${result.status ?? 'inconnu'}.`);
172
+ }
42
173
  }
43
174
 
44
175
  // ────── Commande : INIT
@@ -49,12 +180,14 @@ async function askInitQuestions() {
49
180
  name: 'projectName',
50
181
  message: '📛 Nom du projet :',
51
182
  default: 'my-app',
183
+ validate: validatePromptValue(validateModuleName),
52
184
  },
53
185
  {
54
186
  type: 'input',
55
187
  name: 'projectPath',
56
188
  message: '📁 Dossier cible :',
57
189
  default: './',
190
+ validate: validatePromptValue(resolveProjectPath),
58
191
  },
59
192
  {
60
193
  type: 'list',
@@ -93,76 +226,73 @@ async function askInitQuestions() {
93
226
  name: 'modules',
94
227
  message: '📦 Modules à générer (séparés par des espaces) :',
95
228
  default: 'user',
96
- filter: (input) => input.split(' ').map(s => s.trim()).filter(Boolean),
97
- }
229
+ filter: (input) =>
230
+ input
231
+ .split(' ')
232
+ .map((s) => s.trim())
233
+ .filter(Boolean),
234
+ validate: validatePromptValue((modules) => modules.forEach(validateModuleName)),
235
+ },
98
236
  ]);
99
237
  }
100
238
 
101
- async function runInteractiveInit() {
102
- printLogo();
239
+ function validatePromptValue(validator) {
240
+ return (value) => {
241
+ try {
242
+ validator(value);
243
+ return true;
244
+ } catch (error) {
245
+ return error.message;
246
+ }
247
+ };
248
+ }
249
+
250
+ async function runInteractiveInit(options) {
251
+ 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
+ );
103
256
 
104
257
  if (!fs.existsSync(GENERATE_SCRIPT)) {
105
- console.log(chalk.red(`❌ Script introuvable : ${GENERATE_SCRIPT}`));
106
- process.exit(1);
258
+ throw new Error(`Script introuvable : ${GENERATE_SCRIPT}`);
107
259
  }
108
260
 
109
261
  const answers = await askInitQuestions();
110
- const {
111
- projectName,
112
- projectPath,
113
- packageManager,
114
- orm,
115
- withSwagger,
116
- withDocker,
117
- withGit,
118
- modules,
119
- } = answers;
262
+ const { projectName, projectPath, packageManager, orm, withSwagger, withDocker, withGit, modules } = answers;
120
263
 
121
264
  const env = {
122
- APP_NAME: projectName,
123
- PROJECT_PATH: path.resolve(projectPath),
265
+ APP_NAME: validateModuleName(projectName),
266
+ PROJECT_PATH: resolveProjectPath(projectPath),
124
267
  PM: packageManager,
125
- ORM: orm,
268
+ ORM: validateOrm(orm),
126
269
  WITH_SWAGGER: withSwagger ? 'y' : 'n',
127
270
  WITH_DOCKER: withDocker ? 'y' : 'n',
128
271
  WITH_GIT: withGit ? 'y' : 'n',
129
- MODULES: modules.join(' '),
272
+ MODULES: modules.map(validateModuleName).join(' '),
130
273
  };
131
274
 
132
- const envExport = Object.entries(env)
133
- .map(([key, val]) => `${key}="${val}"`)
134
- .join(' ');
135
-
136
275
  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
- }
276
+ runBashScript(GENERATE_SCRIPT, [], env);
145
277
  }
146
278
 
147
279
  // ────── Commande : MODULE
148
- async function runModuleGeneration(args) {
149
- printLogo();
280
+ async function runModuleGeneration(parsed) {
281
+ if (!parsed.options.quiet) printLogo();
150
282
 
151
283
  let moduleName, orm;
152
284
 
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
- }
285
+ if (parsed.positionals[0]) {
286
+ moduleName = validateModuleName(parsed.positionals[0]);
287
+ orm = validateOrm(parsed.options.orm ?? 'typeorm');
159
288
  } else {
289
+ if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
160
290
  const answers = await inquirer.prompt([
161
291
  {
162
292
  type: 'input',
163
293
  name: 'moduleName',
164
294
  message: '📦 Nom du module :',
165
- validate: input => !!input || 'Le nom du module est requis',
295
+ validate: validatePromptValue(validateModuleName),
166
296
  },
167
297
  {
168
298
  type: 'list',
@@ -172,47 +302,152 @@ async function runModuleGeneration(args) {
172
302
  default: 'typeorm',
173
303
  },
174
304
  ]);
175
- moduleName = answers.moduleName;
176
- orm = answers.orm;
305
+ moduleName = validateModuleName(answers.moduleName);
306
+ orm = validateOrm(answers.orm);
177
307
  }
178
308
 
179
309
  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);
310
+ throw new Error('Aucun projet NestJS détecté dans ce dossier. Lance cette commande depuis un projet NestJS.');
311
+ }
312
+
313
+ if (parsed.options.dryRun) {
314
+ inspectProject(process.cwd(), orm);
315
+ console.log(JSON.stringify(createModulePlan(process.cwd(), moduleName, orm), null, 2));
316
+ return;
317
+ }
318
+
319
+ if (!parsed.options.quiet) console.log(chalk.cyan(`\n⚙️ Génération du module ${moduleName}...\n`));
320
+ if (process.env.NESTGEN_ROOT) {
321
+ runBashScript(ADD_MODULE_SCRIPT, [moduleName, orm], {}, parsed.options.quiet);
322
+ return;
323
+ }
324
+
325
+ generateModule(process.cwd(), moduleName, orm);
326
+ }
327
+
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].');
331
+ 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`;
337
+ if (!/^[a-z][a-z0-9/-]*$/.test(route) || !/^[a-z][a-z0-9_]*$/.test(table))
338
+ throw new Error('Route ou table invalide.');
339
+ if (parsed.options.dryRun) {
340
+ console.log(JSON.stringify({ operation: 'resource', name, route, table, orm, profile, fields }, null, 2));
341
+ return;
183
342
  }
343
+ generateResource(process.cwd(), { name, route, table, fields, orm, profile });
344
+ }
345
+
346
+ function printUsage() {
347
+ 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
349
+ config init|show Gère nestgen.config.json
350
+ 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`,
353
+ );
354
+ }
184
355
 
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}`}`);
185
373
  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);
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'}.`,
377
+ );
378
+ } catch (error) {
379
+ console.log(
380
+ `INFO Projet Nest: ${error.message} — Lance doctor depuis un projet Nest compatible pour analyser son intégration.`,
381
+ );
193
382
  }
383
+ if (checks.some(([, , valid]) => !valid)) throw new Error('Des prérequis NestGen sont manquants.');
194
384
  }
195
385
 
196
386
  // ────── 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;
387
+ export async function main(args = process.argv.slice(2)) {
388
+ const parsed = parseCliArgs(args);
389
+ if (!parsed.options.color) chalk.level = 0;
390
+ if (parsed.options.version) {
391
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
392
+ console.log(packageJson.version);
393
+ return;
394
+ }
395
+ if (parsed.options.help || !parsed.command) {
396
+ printUsage();
397
+ return;
398
+ }
399
+
400
+ switch (parsed.command) {
401
+ case 'init':
402
+ await runInteractiveInit(parsed.options);
403
+ break;
404
+
405
+ case 'module':
406
+ await runModuleGeneration(parsed);
407
+ break;
408
+
409
+ case 'resource':
410
+ runResourceGeneration(parsed);
411
+ break;
412
+
413
+ case 'config': {
414
+ const action = parsed.positionals[0];
415
+ if (action === 'show') {
416
+ console.log(JSON.stringify(loadConfigIfPresent(process.cwd()) ?? defaultConfig, null, 2));
417
+ break;
418
+ }
419
+ if (action === 'init') {
420
+ const config = {
421
+ ...defaultConfig,
422
+ orm: validateOrm(parsed.options.orm ?? defaultConfig.orm),
423
+ profile: parseArchitectureProfile(parsed.options.profile),
424
+ packageManager: parsed.options.packageManager ?? defaultConfig.packageManager,
425
+ };
426
+ if (!['npm', 'pnpm', 'yarn'].includes(config.packageManager))
427
+ throw new Error('Package manager invalide.');
428
+ if (parsed.options.dryRun) {
429
+ console.log(JSON.stringify({ operation: 'config-init', file: configFileName, config }, null, 2));
430
+ break;
431
+ }
432
+ writeConfig(process.cwd(), config);
433
+ console.log(configFileName);
434
+ break;
435
+ }
436
+ throw new Error('config requiert init ou show.');
437
+ }
438
+
439
+ case 'doctor':
440
+ runDoctor();
441
+ break;
442
+
443
+ default:
444
+ throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
445
+ }
446
+ }
447
+
448
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === __filename) {
449
+ main().catch((error) => {
450
+ console.error(chalk.red(`❌ ${error.message}`));
451
+ process.exitCode = 1;
452
+ });
218
453
  }