nestcraftx 0.2.6 → 0.4.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.
Files changed (42) hide show
  1. package/AUDIT_ROADMAP.md +798 -0
  2. package/CHANGELOG.fr.md +22 -0
  3. package/CHANGELOG.md +22 -0
  4. package/PROGRESS.md +259 -0
  5. package/commands/demo.js +2 -42
  6. package/commands/generate.js +5 -4
  7. package/commands/help.js +29 -2
  8. package/commands/new.js +6 -49
  9. package/package.json +1 -1
  10. package/utils/app-module.updater.js +102 -0
  11. package/utils/cliParser.js +0 -45
  12. package/utils/configs/setupCleanArchitecture.js +2 -2
  13. package/utils/envGenerator.js +6 -0
  14. package/utils/file-system.js +90 -0
  15. package/utils/file-utils/packageJsonUtils.js +6 -0
  16. package/utils/file-utils/saveProjectConfig.js +6 -0
  17. package/utils/fullModeInput.js +8 -229
  18. package/utils/generators/application/dtoGenerator.js +252 -0
  19. package/utils/generators/application/dtoUpdater.js +5 -0
  20. package/utils/generators/database/setupDatabase.js +32 -14
  21. package/utils/generators/domain/entityGenerator.js +126 -0
  22. package/utils/generators/domain/entityUpdater.js +5 -0
  23. package/utils/generators/infrastructure/mapperGenerator.js +114 -0
  24. package/utils/generators/infrastructure/mapperUpdater.js +5 -0
  25. package/utils/generators/infrastructure/middlewareGenerator.js +645 -0
  26. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +229 -0
  27. package/utils/generators/infrastructure/repositoryGenerator.js +334 -0
  28. package/utils/generators/presentation/controllerGenerator.js +142 -0
  29. package/utils/helpers.js +115 -0
  30. package/utils/interactive/askEntityInputs.js +5 -68
  31. package/utils/interactive/entityBuilder.js +400 -0
  32. package/utils/lightModeInput.js +13 -246
  33. package/utils/setups/orms/typeOrmSetup.js +31 -4
  34. package/utils/setups/projectSetup.js +78 -8
  35. package/utils/setups/setupAuth.js +6 -7
  36. package/utils/setups/setupDatabase.js +2 -1
  37. package/utils/setups/setupMongoose.js +96 -30
  38. package/utils/setups/setupPrisma.js +118 -134
  39. package/utils/setups/setupSwagger.js +12 -8
  40. package/utils/shell.js +125 -10
  41. package/utils/userInput.js +37 -101
  42. package/utils/utils.js +29 -2164
@@ -86,36 +86,41 @@ async function setupPrisma(inputs) {
86
86
  for (const relation of inputs.entitiesData.relations) {
87
87
  const fromLower = relation.from.toLowerCase();
88
88
  const toLower = relation.to.toLowerCase();
89
- const fromCapitalized = capitalize(relation.from);
90
- const toCapitalized = capitalize(relation.to); // 'from' side (source)
91
89
 
92
90
  if (relation.type === "1-n") {
93
91
  // 'One' side: exclude the name of the other entity's list (e.g., 'articles')
94
92
  fieldsToExcludeMap.get(fromLower).push(`${toLower}s`);
95
- } else if (relation.type === "n-1") {
96
- // 'Many' side: exclude the foreign key (e.g., 'articleId') and the relation name (e.g., 'article')
97
- fieldsToExcludeMap.get(fromLower).push(`${toLower}id`, toLower);
98
- } // Add other relation types (1-1, n-n) if necessary here... // 'to' side (target)
99
- if (relation.type === "1-n") {
100
93
  // 'Many' side: exclude the foreign key (e.g., 'userId') and the relation name (e.g., 'user')
101
94
  fieldsToExcludeMap.get(toLower).push(`${fromLower}id`, fromLower);
102
95
  } else if (relation.type === "n-1") {
96
+ // 'Many' side: exclude the foreign key (e.g., 'articleId') and the relation name (e.g., 'article')
97
+ fieldsToExcludeMap.get(fromLower).push(`${toLower}id`, toLower);
103
98
  // 'One' side: exclude the name of the other entity's list (e.g., 'comments')
104
99
  fieldsToExcludeMap.get(toLower).push(`${fromLower}s`);
100
+ } else if (relation.type === "1-1") {
101
+ // 'from' side (source of relation, holds the FK): exclude foreign key and relation name
102
+ fieldsToExcludeMap.get(fromLower).push(`${toLower}id`, toLower);
103
+ // 'to' side (target of relation): exclude relation name
104
+ fieldsToExcludeMap.get(toLower).push(fromLower);
105
+ } else if (relation.type === "n-n") {
106
+ // Both sides hold list of other entity: exclude plurals
107
+ fieldsToExcludeMap.get(fromLower).push(`${toLower}s`);
108
+ fieldsToExcludeMap.get(toLower).push(`${fromLower}s`);
105
109
  }
106
110
  }
107
111
  }
108
112
  // 2. Initial generation of models WITHOUT incorrect relationship fields
109
113
  for (const entity of inputs.entitiesData.entities) {
110
114
  const entityNameLower = entity.name.toLowerCase();
115
+ const entityNameCap = capitalize(entity.name);
111
116
  // On utilise un Set pour suivre les noms de champs déjà écrits dans ce modèle
112
117
  const addedFields = new Set(["id", "createdat", "updatedat"]);
113
118
 
114
119
  schemaContent += `
115
120
  /**
116
- * ${entity.name} Model
121
+ * ${entityNameCap} Model
117
122
  */
118
- model ${entity.name} {
123
+ model ${entityNameCap} {
119
124
  id String @id @default(uuid())
120
125
  createdAt DateTime @default(now())
121
126
  updatedAt DateTime @updatedAt`;
@@ -142,7 +147,7 @@ async function setupPrisma(inputs) {
142
147
  !fieldsToExclude.includes(fieldNameLower) &&
143
148
  !addedFields.has(fieldNameLower)
144
149
  ) {
145
- schemaContent += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
150
+ schemaContent += `\n ${field.name} ${formatPrismaField(field)}`;
146
151
  addedFields.add(fieldNameLower);
147
152
  }
148
153
  }
@@ -159,30 +164,35 @@ async function setupPrisma(inputs) {
159
164
  const to = relation.to;
160
165
  const type = relation.type;
161
166
 
167
+ const fromCap = capitalize(from);
168
+ const toCap = capitalize(to);
169
+ const fromLow = from.toLowerCase();
170
+ const toLow = to.toLowerCase();
171
+
162
172
  // The replacement must be done on the entire generated schemaContent // Using a replacement function to update the content of `schemaContent`
163
173
  if (type === "1-n") {
164
174
  // "One" side (source): adds the list (to[])
165
175
  schemaContent = schemaContent.replace(
166
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
176
+ new RegExp(`model ${fromCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
167
177
  (match, content) => {
168
- const fieldLine = ` ${to}s ${to}[]`;
178
+ const fieldLine = ` ${toLow}s ${toCap}[]`;
169
179
  return match.includes(fieldLine)
170
180
  ? match
171
- : `model ${from} {${content}\n${fieldLine}\n}`;
181
+ : `model ${fromCap} {${content}\n${fieldLine}\n}`;
172
182
  },
173
183
  );
174
184
 
175
185
  // "Many" side (target): adds the relation and the foreign key
176
186
  schemaContent = schemaContent.replace(
177
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
187
+ new RegExp(`model ${toCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
178
188
  (match, content) => {
179
- const relationLine = ` ${from} ${from} @relation(fields: [${from}Id], references: [id])`;
180
- const fkLine = ` ${from}Id String`;
189
+ const relationLine = ` ${fromLow} ${fromCap} @relation(fields: [${fromLow}Id], references: [id])`;
190
+ const fkLine = ` ${fromLow}Id String`;
181
191
  let result = match.includes(relationLine)
182
192
  ? content
183
193
  : `${content}\n${relationLine}`;
184
194
  result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
185
- return `model ${to} {${result}\n}`;
195
+ return `model ${toCap} {${result}\n}`;
186
196
  },
187
197
  );
188
198
  }
@@ -192,27 +202,26 @@ async function setupPrisma(inputs) {
192
202
 
193
203
  // "Many" side (source = from): adds the relation and the foreign key
194
204
  schemaContent = schemaContent.replace(
195
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
205
+ new RegExp(`model ${fromCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
196
206
  (match, content) => {
197
- const relationLine = ` ${to} ${to} @relation(fields: [${to}Id], references: [id])`;
198
- const fkLine = ` ${to}Id String`;
207
+ const relationLine = ` ${toLow} ${toCap} @relation(fields: [${toLow}Id], references: [id])`;
208
+ const fkLine = ` ${toLow}Id String`;
199
209
  let result = match.includes(relationLine)
200
210
  ? content
201
211
  : `${content}\n${relationLine}`;
202
212
  result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
203
- return `model ${from} {${result}\n}`;
213
+ return `model ${fromCap} {${result}\n}`;
204
214
  },
205
215
  );
206
216
 
207
217
  // "One" side (target = to): adds the list (from[])
208
218
  schemaContent = schemaContent.replace(
209
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
219
+ new RegExp(`model ${toCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
210
220
  (match, content) => {
211
- const fromCapitalized = capitalize(from);
212
- const fieldLine = ` ${from}s ${from}[]`;
221
+ const fieldLine = ` ${fromLow}s ${fromCap}[]`;
213
222
  return match.includes(fieldLine)
214
223
  ? match
215
- : `model ${to} {${content}\n${fieldLine}\n}`;
224
+ : `model ${toCap} {${content}\n${fieldLine}\n}`;
216
225
  },
217
226
  );
218
227
  }
@@ -220,28 +229,28 @@ async function setupPrisma(inputs) {
220
229
  if (type === "1-1") {
221
230
  // 'from' side (source): adds the relation, foreign key, and @unique attribute
222
231
  schemaContent = schemaContent.replace(
223
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
232
+ new RegExp(`model ${fromCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
224
233
  (match, content) => {
225
- const relationLine = ` ${to} ${to}? @relation(fields: [${to}Id], references: [id])`; // The foreign key must be unique in a 1-1 relationship, and optional for flexibility
226
- const fkLine = ` ${to}Id String? @unique`;
234
+ const relationLine = ` ${toLow} ${toCap}? @relation(fields: [${toLow}Id], references: [id])`; // The foreign key must be unique in a 1-1 relationship, and optional for flexibility
235
+ const fkLine = ` ${toLow}Id String? @unique`;
227
236
 
228
237
  let result = match.includes(relationLine)
229
238
  ? content
230
239
  : `${content}\n${relationLine}`;
231
240
  result = result.includes(fkLine) ? result : `${result}\n${fkLine}`;
232
- return `model ${from} {${result}\n}`;
241
+ return `model ${fromCap} {${result}\n}`;
233
242
  },
234
243
  );
235
244
 
236
245
  // 'to' side (target): adds the inverse relation (optional)
237
246
  schemaContent = schemaContent.replace(
238
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
247
+ new RegExp(`model ${toCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
239
248
  (match, content) => {
240
249
  // Inverse relation (optional because 'from' holds the FK)
241
- const fieldLine = ` ${from} ${from}?`;
250
+ const fieldLine = ` ${fromLow} ${fromCap}?`;
242
251
  return match.includes(fieldLine)
243
252
  ? match
244
- : `model ${to} {${content}\n${fieldLine}\n}`;
253
+ : `model ${toCap} {${content}\n${fieldLine}\n}`;
245
254
  },
246
255
  );
247
256
  }
@@ -249,23 +258,23 @@ async function setupPrisma(inputs) {
249
258
  if (type === "n-n") {
250
259
  // 'from' side (source): adds the list (to[])
251
260
  schemaContent = schemaContent.replace(
252
- new RegExp(`model ${from} \{([\\s\\S]*?)\n\\}`, "m"),
261
+ new RegExp(`model ${fromCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
253
262
  (match, content) => {
254
- const fieldLine = ` ${to}s ${to}[]`;
263
+ const fieldLine = ` ${toLow}s ${toCap}[]`;
255
264
  return match.includes(fieldLine)
256
265
  ? match
257
- : `model ${from} {${content}\n${fieldLine}\n}`;
266
+ : `model ${fromCap} {${content}\n${fieldLine}\n}`;
258
267
  },
259
268
  );
260
269
 
261
270
  // 'to' side (target): adds the list (from[])
262
271
  schemaContent = schemaContent.replace(
263
- new RegExp(`model ${to} \{([\\s\\S]*?)\n\\}`, "m"),
272
+ new RegExp(`model ${toCap} \\{([\\s\\S]*?)\\n\\}`, "m"),
264
273
  (match, content) => {
265
- const fieldLine = ` ${from}s ${from}[]`;
274
+ const fieldLine = ` ${fromLow}s ${fromCap}[]`;
266
275
  return match.includes(fieldLine)
267
276
  ? match
268
- : `model ${to} {${content}\n${fieldLine}\n}`;
277
+ : `model ${toCap} {${content}\n${fieldLine}\n}`;
269
278
  },
270
279
  );
271
280
  }
@@ -289,7 +298,7 @@ async function setupPrisma(inputs) {
289
298
  path: schemaPath,
290
299
  contente: baseSchema,
291
300
  });
292
- await runCommand(`npx prisma format`, "❌ Failed to format prisma schema");
301
+ await runCommand(`npx prisma format`, "❌ Failed to format prisma schema — run 'npx prisma format' manually", { critical: false });
293
302
 
294
303
  // 📁 Step 6: Creating the `src/prisma` structure
295
304
  const defaultPatch = "src/prisma";
@@ -344,6 +353,7 @@ async function setupPrisma(inputs) {
344
353
  await runCommand(
345
354
  `${inputs.packageManager} add dotenv`,
346
355
  "❌ Failed to install dotenv",
356
+ { critical: false },
347
357
  );
348
358
 
349
359
  // Creating prisma.config.ts file to load environment variables
@@ -362,7 +372,7 @@ async function setupPrisma(inputs) {
362
372
  }
363
373
 
364
374
  // Step 7: Generating the Prisma client
365
- await runCommand("npx prisma generate", "❌ Prisma generation failed");
375
+ await runCommand("npx prisma generate", "❌ Prisma client generation failed — run 'npx prisma generate' manually", { critical: false });
366
376
 
367
377
  // Step 8: Migration (ONLY in 'new' mode)
368
378
  if (inputs.isDemo) {
@@ -421,6 +431,44 @@ function mapTypeToPrisma(type) {
421
431
  }
422
432
  }
423
433
 
434
+ /**
435
+ * Formats a field definition for the Prisma schema including constraints.
436
+ * @param {object} field
437
+ * @returns {string} Formatted field line content (e.g. "String? @unique")
438
+ */
439
+ function formatPrismaField(field) {
440
+ let prismaType = mapTypeToPrisma(field.type);
441
+ if (field.nullable) {
442
+ if (!prismaType.endsWith("?") && !prismaType.endsWith("[]")) {
443
+ prismaType += "?";
444
+ }
445
+ }
446
+ let attributes = "";
447
+ if (field.unique) {
448
+ attributes += " @unique";
449
+ }
450
+ if (field.default !== undefined && field.default !== null && field.default !== "") {
451
+ let defaultVal = field.default;
452
+ if (typeof defaultVal === "string") {
453
+ if (defaultVal === "true") defaultVal = true;
454
+ else if (defaultVal === "false") defaultVal = false;
455
+ else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
456
+ defaultVal = "now()";
457
+ } else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
458
+ defaultVal = Number(defaultVal);
459
+ }
460
+ }
461
+ if (defaultVal === "now()") {
462
+ attributes += ` @default(now())`;
463
+ } else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
464
+ attributes += ` @default(${defaultVal})`;
465
+ } else {
466
+ attributes += ` @default("${defaultVal}")`;
467
+ }
468
+ }
469
+ return `${prismaType}${attributes}`;
470
+ }
471
+
424
472
  async function setupPrismaSeeding(inputs) {
425
473
  logInfo("⚙️ Configuring seeding for Prisma...");
426
474
 
@@ -645,18 +693,18 @@ async function updatePrismaSchema(entityData) {
645
693
 
646
694
  switch (relation.type) {
647
695
  case "n-1": // "Many to One" : Le nouveau module appartient à une cible
648
- relationFields += `\n ${targetLow} ${targetLow} @relation(fields: [${targetLow}Id], references: [id])`;
696
+ relationFields += `\n ${targetLow} ${targetCap} @relation(fields: [${targetLow}Id], references: [id])`;
649
697
  relationFields += `\n ${targetLow}Id String`;
650
698
  break;
651
699
  case "1-n": // "One to Many" : Le nouveau module possède plusieurs cibles
652
- relationFields += `\n ${targetLow}s ${targetLow}[]`;
700
+ relationFields += `\n ${targetLow}s ${targetCap}[]`;
653
701
  break;
654
702
  case "1-1":
655
- relationFields += `\n ${targetLow} ${targetLow}? @relation(fields: [${targetLow}Id], references: [id])`;
703
+ relationFields += `\n ${targetLow} ${targetCap}? @relation(fields: [${targetLow}Id], references: [id])`;
656
704
  relationFields += `\n ${targetLow}Id String? @unique`;
657
705
  break;
658
706
  case "n-n":
659
- relationFields += `\n ${targetLow}s ${targetLow}[]`;
707
+ relationFields += `\n ${targetLow}s ${targetCap}[]`;
660
708
  break;
661
709
  }
662
710
  }
@@ -665,13 +713,13 @@ async function updatePrismaSchema(entityData) {
665
713
  let modelBlock = `\n/**
666
714
  * ${capitalizedName} Model
667
715
  */
668
- model ${lowercasedName} {
716
+ model ${capitalizedName} {
669
717
  id String @id @default(uuid())
670
718
  createdAt DateTime @default(now())
671
719
  updatedAt DateTime @updatedAt${relationFields}`;
672
720
 
673
721
  fields.forEach((field) => {
674
- modelBlock += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
722
+ modelBlock += `\n ${field.name} ${formatPrismaField(field)}`;
675
723
  });
676
724
 
677
725
  modelBlock += `\n}\n`;
@@ -686,117 +734,53 @@ model ${lowercasedName} {
686
734
  // 4️⃣ Injection de l'INVERSE dans le modèle cible (Target)
687
735
  if (relation) {
688
736
  const targetCap = capitalize(relation.target);
689
- const targetLow = capitalize(relation.target);
737
+ const targetLow = relation.target.toLowerCase();
690
738
  const newCap = capitalizedName;
691
739
  const newLow = name.toLowerCase();
692
740
 
693
741
  let inverseField = "";
694
742
  switch (relation.type) {
695
743
  case "n-1": // Inverse d'un Many-to-One est un One-to-Many
696
- inverseField = ` ${newLow}s ${newLow}[]`;
744
+ inverseField = ` ${newLow}s ${newCap}[]`;
697
745
  break;
698
746
  case "1-n": // Inverse d'un One-to-Many est un Many-to-One
699
- inverseField = ` ${newLow} ${newLow} @relation(fields: [${newLow}Id], references: [id])\n ${newLow}Id String`;
747
+ inverseField = ` ${newLow} ${newCap} @relation(fields: [${newLow}Id], references: [id])\n ${newLow}Id String`;
700
748
  break;
701
749
  case "1-1":
702
- inverseField = ` ${newLow} ${newLow}?`;
750
+ inverseField = ` ${newLow} ${newCap}?`;
703
751
  break;
704
752
  case "n-n":
705
- inverseField = ` ${newLow}s ${newLow}[]`;
753
+ inverseField = ` ${newLow}s ${newCap}[]`;
706
754
  break;
707
755
  }
708
756
 
709
757
  // On utilise une RegEx pour trouver le bloc du modèle cible et insérer avant la fermeture "}"
710
758
  await updateFile({
711
759
  path: schemaPath,
712
- pattern: new RegExp(`model ${targetLow} \\{([\\s\\S]*?)\\}`, "g"),
713
- replacement: `model ${targetLow} {$1\n${inverseField}\n}`,
760
+ pattern: new RegExp(`model ${targetCap} \\{([\\s\\S]*?)\\}`, "g"),
761
+ replacement: `model ${targetCap} {$1\n${inverseField}\n}`,
714
762
  });
715
763
  }
716
764
 
717
765
  // 3️⃣ Prisma Commands Sync (meilleur sur Windows)
718
- try {
719
- console.log(info("Running: prisma format..."));
720
- execSync("npx prisma format", { stdio: "pipe" });
721
-
722
- console.log(info("Running: prisma generate..."));
723
- execSync("npx prisma generate", { stdio: "pipe" });
724
-
725
- console.log(info("Running: prisma migrate dev..."));
726
- execSync(
727
- `npx prisma migrate dev --name add_module_${entityData.name} --force`,
728
- {
729
- stdio: "pipe",
730
- },
731
- );
732
-
733
- // console.log(success("🎉 Prisma migration completed successfully!"));
734
- } catch (err) {
735
- // 4️⃣ Gestion spécifique Windows EPERM
736
- if (
737
- err.message.includes("operation not permitted") ||
738
- err.message.includes("EPERM") ||
739
- err.message.includes("EBUSY")
740
- ) {
741
- logWarning("System Lock Detected!");
742
- console.log(
743
- `${info("Prisma Client binary is locked because your server is running.")}`,
744
- );
745
- console.log(
746
- `${info("ACTION:")} Stop NestJS (Ctrl+C) and run manually:\n`,
747
- );
748
- console.log(`${success("npx prisma generate")}`);
749
- console.log(`${success("npx prisma migrate dev")}\n`);
750
- } else {
751
- console.log(error("❌ Prisma error: " + err.message));
752
- }
753
- }
754
- }
755
-
756
- /* async function updatePrismaSchema(entityData) {
757
- const schemaPath = "prisma/schema.prisma";
758
-
759
- // Construction du bloc modèle
760
- let modelBlock = `\nmodel ${capitalize(entityData.name)} {
761
- id String @id @default(uuid())
762
- createdAt DateTime @default(now())
763
- updatedAt DateTime @updatedAt`;
764
-
765
- entityData.fields.forEach((field) => {
766
- // Note: On réutilise ta fonction mapTypeToPrisma ici
767
- modelBlock += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
768
- });
769
-
770
- modelBlock += `\n}\n`;
771
-
772
- // Injection à la fin du fichier
773
- await updateFile({
774
- path: schemaPath,
775
- pattern: /$/, // On ajoute à la fin
776
- replacement: modelBlock,
777
- });
766
+ await runCommand(
767
+ "npx prisma format",
768
+ "❌ Failed to format prisma schema — run 'npx prisma format' manually",
769
+ { critical: false }
770
+ );
778
771
 
779
- // ⚠️ IMPORTANT : On formate et on régénère le client pour supprimer les erreurs TS
780
- try {
781
- await runCommand("npx prisma format");
782
- await runCommand("npx prisma generate");
783
- await runCommand(
784
- `npx prisma migrate dev --name add_module_${entityData.name}`,
785
- );
786
- } catch (err) {
787
- if (err.message.includes("EPERM")) {
788
- console.log(
789
- `\n${warning("⚠️ System Lock:")} Could not update Prisma Client binary because your app is currently running.`,
790
- );
791
- console.log(
792
- `${info("ACTION REQUIRED:")} Stop your dev server (Ctrl+C) and run: ${success("| npx prisma generate | and | npx prisma migrate dev |")}\n`,
793
- );
794
- } else {
795
- logError("Prisma error: " + err.message);
796
- }
797
- }
772
+ await runCommand(
773
+ "npx prisma generate",
774
+ "❌ Failed to generate prisma client — run 'npx prisma generate' manually",
775
+ { critical: false }
776
+ );
798
777
 
799
- throw new Error(err);
800
- } */
778
+ await runCommand(
779
+ `npx prisma migrate dev --name add_module_${entityData.name}`,
780
+ `❌ Failed to run prisma migration — run 'npx prisma migrate dev --name add_module_${entityData.name}' manually`,
781
+ { critical: false }
782
+ );
783
+ }
801
784
 
802
785
  module.exports = { setupPrisma, updatePrismaSchema };
786
+
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { execSync } = require("child_process");
2
+ const { runCommand } = require("../shell");
3
3
  const { logInfo } = require("../loggers/logInfo");
4
4
  const { logSuccess } = require("../loggers/logSuccess");
5
5
 
@@ -7,13 +7,17 @@ async function setupSwagger(inputs) {
7
7
  logInfo("Installation et Configuration de Swagger...");
8
8
 
9
9
  // installation de swagger
10
- try {
11
- execSync("npm install @nestjs/swagger swagger-ui-express", {
12
- stdio: "inherit",
13
- });
14
- } catch (error) {
15
- console.error("❌ Échec de l'installation de Swagger :", error);
16
- process.exit(1);
10
+ await runCommand(
11
+ "npm install @nestjs/swagger swagger-ui-express",
12
+ "Échec de l'installation de Swagger",
13
+ { spinner: "Installing Swagger..." }
14
+ );
15
+
16
+ const isDryRun = process.argv.includes("--dry-run");
17
+ if (isDryRun) {
18
+ console.log("[DRY-RUN] Simulated: write Swagger configuration to src/main.ts");
19
+ logSuccess(" Swagger configuré avec succès !");
20
+ return;
17
21
  }
18
22
 
19
23
  // Modification de main.ts pour intégrer Swagger
package/utils/shell.js CHANGED
@@ -1,32 +1,147 @@
1
1
  const { execSync } = require("child_process");
2
2
  const fs = require("fs");
3
3
  const { logError } = require("./loggers/logError");
4
+ const { logWarning } = require("./loggers/logWarning");
4
5
  const { spinner } = require("./spinner");
6
+ const { warning, info } = require("./colors");
7
+
8
+ /**
9
+ * Accumulated warnings collected during a generation session.
10
+ * Displayed as a summary at the end via printSetupWarnings().
11
+ */
12
+ const _warnings = [];
13
+
14
+ /**
15
+ * Runs a shell command synchronously.
16
+ *
17
+ * @param {string} command - The shell command to execute.
18
+ * @param {string} errorMessage - Human-readable error message shown on failure.
19
+ * @param {object} [options] - Optional configuration.
20
+ * @param {boolean} [options.critical=true] - If true (default), a failure calls process.exit(1).
21
+ * If false, the failure is logged as a warning and execution continues.
22
+ * @param {string|null} [options.spinner] - Optional spinner label shown during execution.
23
+ * @param {number} [options.timeout] - Custom timeout in milliseconds.
24
+ * @returns {Promise<boolean>} Resolves to true on success, false on non-critical failure.
25
+ */
26
+ async function runCommand(command, errorMessage, options = {}) {
27
+ let critical = true;
28
+ let spinnerText = null;
29
+ let customTimeout = undefined;
30
+
31
+ if (typeof options === "string") {
32
+ spinnerText = options;
33
+ } else if (typeof options === "object" && options !== null) {
34
+ critical = options.critical !== false;
35
+ spinnerText = options.spinner || null;
36
+ customTimeout = options.timeout;
37
+ }
38
+
39
+ // Determine Timeout
40
+ let timeoutVal = 30000; // default 30 seconds
41
+ if (
42
+ command.includes("install") ||
43
+ command.includes("add") ||
44
+ command.includes(" new ") ||
45
+ command.includes("prisma migrate")
46
+ ) {
47
+ timeoutVal = 300000; // 5 minutes for heavy setup commands
48
+ }
49
+ if (customTimeout !== undefined) {
50
+ timeoutVal = customTimeout;
51
+ }
52
+
53
+ // Dry-Run Simulation Check
54
+ const isDryRun = process.argv.includes("--dry-run");
55
+ if (isDryRun) {
56
+ if (spinnerText) {
57
+ const spin = spinner(spinnerText);
58
+ spin.start();
59
+ spin.succeed(`[DRY-RUN] Simulated: ${command}`);
60
+ } else {
61
+ console.log(`[DRY-RUN] Simulated: ${command}`);
62
+ }
63
+ return true;
64
+ }
5
65
 
6
- async function runCommand(command, errorMessage, spinnerText = null) {
7
66
  const spin = spinnerText ? spinner(spinnerText) : null;
8
67
 
9
68
  try {
10
69
  if (spin) spin.start();
11
- execSync(command, { stdio: spinnerText ? "pipe" : "inherit" });
70
+ execSync(command, {
71
+ stdio: spinnerText ? "pipe" : "inherit",
72
+ timeout: timeoutVal,
73
+ });
12
74
  if (spin) spin.succeed(spinnerText);
75
+ return true;
13
76
  } catch (error) {
14
77
  if (spin) spin.fail(errorMessage);
15
- logError(errorMessage);
16
- fs.appendFileSync(
17
- "setup.log",
18
- `[Erreur] ${errorMessage}: ${error.message}\n`,
19
- );
20
- process.exit(1);
78
+
79
+ let errorDetail = error.message;
80
+ if (error.code === "ETIMEDOUT" || error.signal === "SIGTERM") {
81
+ errorDetail = `Command timed out after ${timeoutVal / 1000}s`;
82
+ }
83
+
84
+ const logLine = `[${new Date().toISOString()}] ${errorMessage}: ${errorDetail}\n`;
85
+ fs.appendFileSync("setup.log", logLine);
86
+
87
+ if (critical) {
88
+ logError(`${errorMessage}`);
89
+ logError(`Run: ${command}`);
90
+ logError(`Detail: ${errorDetail}`);
91
+ process.exit(1);
92
+ } else {
93
+ logWarning(`${errorMessage} (non-fatal, continuing...)`);
94
+ logWarning(` Command: ${command}`);
95
+ logWarning(` Detail: ${errorDetail}`);
96
+ logWarning(` See setup.log for details`);
97
+ _warnings.push({ command, errorMessage });
98
+ return false;
99
+ }
21
100
  }
22
101
  }
23
102
 
103
+ /**
104
+ * Silent command — returns stdout string or null on failure. Never exits.
105
+ * @param {string} command
106
+ * @returns {string|null}
107
+ */
24
108
  async function runCommandSilent(command) {
25
109
  try {
26
- return execSync(command, { stdio: "pipe" }).toString();
110
+ return execSync(command, { stdio: "pipe", timeout: 10000 }).toString();
27
111
  } catch (error) {
28
112
  return null;
29
113
  }
30
114
  }
31
115
 
32
- module.exports = { runCommand, runCommandSilent };
116
+ /**
117
+ * Prints a summary of all non-critical warnings accumulated during the session.
118
+ * Call this at the very end of the generation pipeline.
119
+ */
120
+ function printSetupWarnings() {
121
+ if (_warnings.length === 0) return;
122
+
123
+ console.log(
124
+ `\n${warning("⚠️ Setup completed with")} ${warning(String(_warnings.length))} ${warning("warning(s):")}`,
125
+ );
126
+ _warnings.forEach((w, i) => {
127
+ console.log(` ${info(`${i + 1}.`)} ${w.errorMessage}`);
128
+ console.log(` ${info("Command:")} ${w.command}`);
129
+ });
130
+ console.log(
131
+ `\n ${info("➜")} Full details in: ${info("setup.log")}\n`,
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Clears the warnings accumulator (useful between test runs).
137
+ */
138
+ function clearSetupWarnings() {
139
+ _warnings.length = 0;
140
+ }
141
+
142
+ module.exports = {
143
+ runCommand,
144
+ runCommandSilent,
145
+ printSetupWarnings,
146
+ clearSetupWarnings,
147
+ };