nestgen-cli 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,6 +52,16 @@ export function loadGenerationManifest(projectRoot) {
52
52
  const generations = value.generations;
53
53
  if (!generations || typeof generations !== 'object' || Array.isArray(generations))
54
54
  throw new Error('Le manifeste de génération est invalide.');
55
+ for (const entry of Object.values(generations)) {
56
+ if (!entry || typeof entry !== 'object')
57
+ throw new Error('Le manifeste de génération est invalide.');
58
+ const files = entry.files;
59
+ if (!files || typeof files !== 'object' || Array.isArray(files))
60
+ throw new Error('Le manifeste de génération est invalide.');
61
+ for (const fingerprint of Object.values(files))
62
+ if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/.test(fingerprint))
63
+ throw new Error('Empreinte invalide dans le manifeste de génération.');
64
+ }
55
65
  return normalizeManifest(value);
56
66
  }
57
67
  function resourceDefinition(name, orm, sourceRoot, route, table, profile, fields, indexes, relations, list) {
@@ -83,10 +93,19 @@ export function withGenerationManifest(projectRoot, changes, definition) {
83
93
  const files = Object.fromEntries(changes
84
94
  .map((change) => [change.path, sha256(change.content)])
85
95
  .sort(([left], [right]) => left.localeCompare(right)));
96
+ // A later generation can legitimately rewrite shared files (AppModule, Prisma schema,
97
+ // test support). Keep every owner aligned with the content committed by this transaction.
98
+ const generations = Object.fromEntries(Object.entries(existing.generations).map(([key, entry]) => [
99
+ key,
100
+ {
101
+ ...entry,
102
+ files: Object.fromEntries(Object.entries(entry.files).map(([file, fingerprint]) => [file, files[file] ?? fingerprint])),
103
+ },
104
+ ]));
86
105
  const manifest = normalizeManifest({
87
106
  ...existing,
88
107
  generations: {
89
- ...existing.generations,
108
+ ...generations,
90
109
  [generationKey(definition)]: {
91
110
  definition,
92
111
  files,
@@ -106,6 +125,34 @@ export function withGenerationManifest(projectRoot, changes, definition) {
106
125
  },
107
126
  ];
108
127
  }
128
+ /** Check all recorded paths once. Older manifests may hold historical hashes for shared files. */
129
+ export function checkGeneratedFiles(projectRoot) {
130
+ const manifest = loadGenerationManifest(projectRoot);
131
+ if (!manifest)
132
+ throw new Error('Manifeste de génération introuvable. Générez d’abord une ressource ou un module.');
133
+ const expected = new Map();
134
+ for (const entry of Object.values(manifest.generations)) {
135
+ for (const [file, fingerprint] of Object.entries(entry.files)) {
136
+ projectPath(projectRoot, file);
137
+ const hashes = expected.get(file) ?? new Set();
138
+ hashes.add(fingerprint);
139
+ expected.set(file, hashes);
140
+ }
141
+ }
142
+ if (expected.size === 0)
143
+ throw new Error('Le manifeste de génération ne contient aucun fichier à vérifier.');
144
+ const missing = [];
145
+ const modified = [];
146
+ for (const [file, fingerprints] of [...expected].sort(([left], [right]) => left.localeCompare(right))) {
147
+ const target = projectPath(projectRoot, file);
148
+ const stat = fs.lstatSync(target, { throwIfNoEntry: false });
149
+ if (!stat)
150
+ missing.push(file);
151
+ else if (!stat.isFile() || !fingerprints.has(sha256(fs.readFileSync(target, 'utf8'))))
152
+ modified.push(file);
153
+ }
154
+ return { checkedFiles: expected.size, missing, modified };
155
+ }
109
156
  /** Report generated files changed after their recorded successful generation. */
110
157
  export function modifiedGeneratedFiles(projectRoot, entry) {
111
158
  return Object.entries(entry.files)
@@ -1,6 +1,6 @@
1
- # Exemple Store API — NestGen CLI 0.2.2
1
+ # Exemple Store API — NestGen CLI 0.2.4
2
2
 
3
- Cette application exemple décrit une API de vente avec des clients, produits, commandes et lignes de commande. Elle est validée avec `nestgen-cli@0.2.2` et TypeORM, avec le profil d’architecture `advanced`.
3
+ Cette application exemple décrit une API de vente avec des clients, produits, commandes et lignes de commande. Elle est validée avec `nestgen-cli@0.2.4` et TypeORM, avec le profil d’architecture `advanced`.
4
4
 
5
5
  Les fichiers de `definitions/` sont la source de vérité. Ils restent volontairement déclaratifs : l’application est reconstruite par le CLI au lieu de conserver une copie de fichiers générés qui pourrait diverger du produit.
6
6
 
@@ -11,7 +11,7 @@ Créez un dossier de travail, installez la version exacte du CLI puis générez
11
11
  ```bash
12
12
  mkdir nestgen-store-api && cd nestgen-store-api
13
13
  npm init --yes
14
- npm install nestgen-cli@0.2.2
14
+ npm install nestgen-cli@0.2.4
15
15
 
16
16
  ./node_modules/.bin/nestgen init store-api \
17
17
  --no-interactive \
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "0.2.2",
3
+ "cliVersion": "0.2.4",
4
4
  "orm": "typeorm",
5
5
  "profile": "advanced",
6
6
  "resources": ["customers", "products", "orders", "order-items"]
package/nestgen.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from 'child_process';
4
- import inquirer from 'inquirer';
4
+ import input from '@inquirer/input';
5
+ import select from '@inquirer/select';
6
+ import confirm from '@inquirer/confirm';
5
7
  import chalk from 'chalk';
6
8
  import path from 'path';
7
9
  import fs from 'fs';
@@ -28,6 +30,7 @@ import {
28
30
  formatMigration,
29
31
  formatHelp,
30
32
  } from './dist/cli/output.js';
33
+ import { checkGeneratedFiles } from './dist/engine/generation-manifest.js';
31
34
 
32
35
  // ────── Resolve __dirname compatible ES Modules
33
36
  const __filename = fileURLToPath(import.meta.url);
@@ -241,72 +244,57 @@ export function runBashScript(scriptPath, args = [], env = {}, quiet = false) {
241
244
 
242
245
  // ────── Commande : INIT
243
246
  async function askInitQuestions() {
244
- return await inquirer.prompt([
245
- {
246
- type: 'input',
247
- name: 'projectName',
248
- message: '📛 Nom du projet :',
249
- default: 'my-app',
250
- validate: validatePromptValue(validateModuleName),
251
- },
252
- {
253
- type: 'input',
254
- name: 'projectPath',
255
- message: '📁 Dossier cible :',
256
- default: './',
257
- validate: validatePromptValue(resolveProjectPath),
258
- },
259
- {
260
- type: 'list',
261
- name: 'packageManager',
262
- message: '📦 Package manager :',
263
- choices: ['pnpm', 'yarn', 'npm'],
264
- default: 'pnpm',
265
- },
266
- {
267
- type: 'list',
268
- name: 'orm',
269
- message: '🧠 ORM :',
270
- choices: ['typeorm', 'prisma'],
271
- default: 'typeorm',
272
- },
273
- {
274
- type: 'confirm',
275
- name: 'withSwagger',
276
- message: '📚 Activer Swagger ?',
277
- default: true,
278
- },
279
- {
280
- type: 'confirm',
281
- name: 'withDocker',
282
- message: '🐳 Activer Docker ?',
283
- default: false,
284
- },
285
- {
286
- type: 'confirm',
287
- name: 'withOperations',
288
- message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
289
- default: false,
290
- },
291
- {
292
- type: 'confirm',
293
- name: 'withGit',
294
- message: '🔃 Initialiser Git ?',
295
- default: true,
296
- },
297
- {
298
- type: 'input',
299
- name: 'modules',
300
- message: '📦 Modules à générer (séparés par des espaces) :',
301
- default: 'user',
302
- filter: (input) =>
303
- input
304
- .split(' ')
305
- .map((s) => s.trim())
306
- .filter(Boolean),
307
- validate: validatePromptValue((modules) => modules.forEach(validateModuleName)),
308
- },
309
- ]);
247
+ const projectName = await input({
248
+ message: '📛 Nom du projet :',
249
+ default: 'my-app',
250
+ validate: validatePromptValue(validateModuleName),
251
+ });
252
+ const projectPath = await input({
253
+ message: '📁 Dossier cible :',
254
+ default: './',
255
+ validate: validatePromptValue(resolveProjectPath),
256
+ });
257
+ const packageManager = await select({
258
+ message: '📦 Package manager :',
259
+ choices: ['pnpm', 'yarn', 'npm'].map((value) => ({ name: value, value })),
260
+ default: 'pnpm',
261
+ });
262
+ const orm = await select({
263
+ message: '🧠 ORM :',
264
+ choices: ['typeorm', 'prisma'].map((value) => ({ name: value, value })),
265
+ default: 'typeorm',
266
+ });
267
+ const withSwagger = await confirm({ message: '📚 Activer Swagger ?', default: true });
268
+ const withDocker = await confirm({ message: '🐳 Activer Docker ?', default: false });
269
+ const withOperations = await confirm({
270
+ message: '🩺 Activer le socle d’exploitation (santé, logs, arrêt propre) ?',
271
+ default: false,
272
+ });
273
+ const withGit = await confirm({ message: '🔃 Initialiser Git ?', default: true });
274
+ const modulesInput = await input({
275
+ message: '📦 Modules à générer (séparés par des espaces) :',
276
+ default: 'user',
277
+ validate: validatePromptValue((value) => parseModulesInput(value).forEach(validateModuleName)),
278
+ });
279
+
280
+ return {
281
+ projectName,
282
+ projectPath,
283
+ packageManager,
284
+ orm,
285
+ withSwagger,
286
+ withDocker,
287
+ withOperations,
288
+ withGit,
289
+ modules: parseModulesInput(modulesInput),
290
+ };
291
+ }
292
+
293
+ function parseModulesInput(value) {
294
+ return value
295
+ .split(' ')
296
+ .map((moduleName) => moduleName.trim())
297
+ .filter(Boolean);
310
298
  }
311
299
 
312
300
  function validatePromptValue(validator) {
@@ -393,23 +381,19 @@ async function runModuleGeneration(parsed) {
393
381
  orm = validateOrm(parsed.options.orm ?? config.orm);
394
382
  } else {
395
383
  if (parsed.options.noInteractive) throw new Error('module --no-interactive requiert un nom de module.');
396
- const answers = await inquirer.prompt([
397
- {
398
- type: 'input',
399
- name: 'moduleName',
384
+ moduleName = validateModuleName(
385
+ await input({
400
386
  message: '📦 Nom du module :',
401
387
  validate: validatePromptValue(validateModuleName),
402
- },
403
- {
404
- type: 'list',
405
- name: 'orm',
388
+ }),
389
+ );
390
+ orm = validateOrm(
391
+ await select({
406
392
  message: '🧠 ORM utilisé :',
407
- choices: ['typeorm', 'prisma'],
393
+ choices: ['typeorm', 'prisma'].map((value) => ({ name: value, value })),
408
394
  default: 'typeorm',
409
- },
410
- ]);
411
- moduleName = validateModuleName(answers.moduleName);
412
- orm = validateOrm(answers.orm);
395
+ }),
396
+ );
413
397
  }
414
398
 
415
399
  if (!isNestProject()) {
@@ -566,6 +550,7 @@ function printUsage() {
566
550
  console.log(
567
551
  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
568
552
  config init|show Gère nestgen.config.json
553
+ check Vérifie les fichiers générés sans les modifier
569
554
  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
555
  --package-manager <pm> Définit npm, pnpm ou yarn
571
556
  --project-path <chemin> Définit le dossier parent du projet init
@@ -731,14 +716,15 @@ function runDoctor(options = {}) {
731
716
  }
732
717
 
733
718
  function machineResult(command, result) {
734
- return { version: 1, ok: true, command, result };
719
+ return { version: 1, ok: command !== 'check' || result.ok, command, result };
735
720
  }
736
721
 
737
722
  function errorCode(error) {
738
723
  const message = error instanceof Error ? error.message : String(error);
739
724
  if (/^(Option inconnue|Commande inconnue|.* requiert |Route ou table invalide|Nom de )/.test(message))
740
725
  return 'USAGE';
741
- if (message.startsWith('Préflight échoué')) return 'PROJECT_INVALID';
726
+ if (message.startsWith('Préflight échoué') || message.toLowerCase().includes('manifeste de génération'))
727
+ return 'PROJECT_INVALID';
742
728
  if (/(existe déjà|Destination |Conflit |Transaction interrompue)/.test(message)) return 'CONFLICT';
743
729
  return 'INTERNAL';
744
730
  }
@@ -762,7 +748,9 @@ export async function main(args = process.argv.slice(2)) {
762
748
  if (parsed.options.help || !parsed.command) {
763
749
  if (machine)
764
750
  console.log(
765
- JSON.stringify(machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'doctor'] })),
751
+ JSON.stringify(
752
+ machineResult('help', { commands: ['init', 'module', 'resource', 'config', 'check', 'doctor'] }),
753
+ ),
766
754
  );
767
755
  else printUsage();
768
756
  return;
@@ -824,6 +812,21 @@ export async function main(args = process.argv.slice(2)) {
824
812
  result = runDoctor(parsed.options);
825
813
  break;
826
814
 
815
+ case 'check': {
816
+ if (parsed.positionals.length) throw new Error('check ne prend aucun argument.');
817
+ const report = checkGeneratedFiles(process.cwd());
818
+ result = { ok: report.missing.length === 0 && report.modified.length === 0, ...report };
819
+ if (!machine) {
820
+ if (result.ok) console.log(`${report.checkedFiles} fichiers générés vérifiés.`);
821
+ else {
822
+ for (const file of report.missing) console.log(`MANQUANT ${file}`);
823
+ for (const file of report.modified) console.log(`MODIFIÉ ${file}`);
824
+ }
825
+ }
826
+ if (!result.ok) process.exitCode = 1;
827
+ break;
828
+ }
829
+
827
830
  default:
828
831
  throw new Error(`Commande inconnue : ${parsed.command}. Utilise --help.`);
829
832
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestgen-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "NestGen CLI — Générateur modulaire NestJS (DDD, CQRS, Hexagonal)",
5
5
  "files": [
6
6
  "dist",
@@ -52,8 +52,10 @@
52
52
  },
53
53
  "licenses": "MIT",
54
54
  "dependencies": {
55
+ "@inquirer/confirm": "^6.3.3",
56
+ "@inquirer/input": "^5.1.7",
57
+ "@inquirer/select": "^5.2.6",
55
58
  "chalk": "^5.3.0",
56
- "inquirer": "^9.0.0",
57
59
  "prettier": "^3.9.9",
58
60
  "typescript": "^5.9.3"
59
61
  },
package/readme.md CHANGED
@@ -6,18 +6,19 @@
6
6
 
7
7
  NestGen CLI generates NestJS project scaffolding and TypeORM or Prisma resources. The canonical package is [`nestgen-cli`](https://www.npmjs.com/package/nestgen-cli); the canonical repository is [`el-hack/nestgen-cli`](https://github.com/el-hack/nestgen-cli).
8
8
 
9
- ## Published in 0.2.2 / Disponible dans 0.2.2
9
+ ## Published in 0.2.4 / Disponible dans 0.2.4
10
10
 
11
11
  | Available now / Disponible maintenant | Evidence / Preuve |
12
12
  | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
13
- | Nest project, module and TypeORM or Prisma resource generation | The commands are shipped by `nestgen-cli@0.2.2` and covered by the CLI contract tests. |
13
+ | Nest project, module and TypeORM or Prisma resource generation | The commands are shipped by `nestgen-cli@0.2.4` and covered by the CLI contract tests. |
14
14
  | Declarative resource relations, safe updates and migration plans | The generated manifest protects manual edits; migrations remain explicitly reviewed and applied by the developer. |
15
+ | Read-only generation drift check | `nestgen check` compares recorded file fingerprints and returns a nonzero status for missing or modified generated files. |
15
16
  | Operational foundation and authorization metadata | Both are optional extension points; NestGen does not configure an identity provider, a global guard or an external monitoring service. |
16
17
  | Store API example | The package ships the [version-pinned example](examples/store-api/README.md), which CI reconstructs from its resource definitions. |
17
18
 
18
19
  ## Planned (not released) / Prévu (non publié)
19
20
 
20
- The [roadmap](roadmap.md) contains topics under consideration after 0.2.2. They have no release date and are not part of the npm contract until they appear in a versioned guide and `CHANGELOG.md`. La roadmap recense des pistes après 0.2.2 ; elles n’ont ni date de sortie ni engagement de compatibilité avant leur publication dans un guide versionné et le changelog.
21
+ The [roadmap](roadmap.md) contains topics under consideration after 0.2.4. They have no release date and are not part of the npm contract until they appear in a versioned guide and `CHANGELOG.md`. La roadmap recense des pistes après 0.2.4 ; elles n’ont ni date de sortie ni engagement de compatibilité avant leur publication dans un guide versionné et le changelog.
21
22
 
22
23
  ## Supported contract
23
24
 
@@ -52,11 +53,11 @@ For Prisma, run `npx prisma generate` and apply the schema with `npx prisma db p
52
53
  Install the announced release and verify the exact binary before generating anything. Installez la version annoncée et vérifiez le binaire exact avant toute génération.
53
54
 
54
55
  ```bash
55
- npm install --global nestgen-cli@0.2.2
56
- nestgen --version # 0.2.2
56
+ npm install --global nestgen-cli@0.2.4
57
+ nestgen --version # 0.2.4
57
58
  ```
58
59
 
59
- Then follow the [Store API example](examples/store-api/README.md): it generates customers, products, orders and order items; starts PostgreSQL; creates an explicit migration; and runs generated tests. The versioned [guide](docs/versions/0.2.2.md) documents the commands and constraints used by the demo. Ensuite, suivez l’exemple Store API : il couvre la génération, PostgreSQL, une migration explicite, les tests générés et la documentation de la version.
60
+ Then follow the [Store API example](examples/store-api/README.md): it generates customers, products, orders and order items; starts PostgreSQL; creates an explicit migration; and runs generated tests. The versioned [guide](docs/versions/0.2.4.md) documents the commands and constraints used by the demo. Ensuite, suivez l’exemple Store API : il couvre la génération, PostgreSQL, une migration explicite, les tests générés et la documentation de la version.
60
61
 
61
62
  The CI verifies the public commands against the announced package version, reconstructs the example and installs the npm tarball in a clean consumer. It validates reproducibility of these flows, not a production certification / pas une certification de production.
62
63
 
@@ -65,7 +66,7 @@ The CI verifies the public commands against the announced package version, recon
65
66
  The following flow is the supported end-to-end TypeORM path. Prerequisites: Node.js `>=24.15.0 <27`, npm 11 and Docker with Compose. It uses Docker for PostgreSQL and creates an explicit migration.
66
67
 
67
68
  ```bash
68
- npm install --global nestgen-cli@0.2.2
69
+ npm install --global nestgen-cli@0.2.4
69
70
  nestgen --version
70
71
  nestgen init
71
72
  ```
@@ -131,9 +132,12 @@ nestgen module <name> [--orm typeorm|prisma] [--application name] [--dry-run]
131
132
  nestgen resource <name> --fields name:type[,name:type] [--route route] [--table table] [--profile simple|advanced] [--application name]
132
133
  nestgen config init [--orm typeorm|prisma] [--profile simple|advanced] [--package-manager npm|pnpm|yarn]
133
134
  nestgen config show
135
+ nestgen check
134
136
  nestgen doctor
135
137
  ```
136
138
 
139
+ `nestgen check` compares the files recorded in `.nestgen/generation-manifest.json` with their current contents. It is read-only, exits with status `1` when a recorded file is missing or modified, and supports `--json` for CI. Run it from the generated Nest project. It reports generated-file drift; it does not validate runtime behavior or database migrations. New generations update recorded fingerprints for shared files such as `app.module.ts` across all affected resources.
140
+
137
141
  Pour la CI, `init` accepte toutes les réponses interactives sous forme d’options :
138
142
 
139
143
  ```bash