nestcraftx 0.3.0 → 1.0.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 (62) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/CHANGELOG.fr.md +74 -0
  3. package/CHANGELOG.md +74 -0
  4. package/PROGRESS.md +70 -67
  5. package/README.fr.md +60 -69
  6. package/bin/nestcraft.js +8 -0
  7. package/commands/demo.js +12 -46
  8. package/commands/generate.js +551 -2
  9. package/commands/generateConf.js +4 -4
  10. package/commands/help.js +11 -0
  11. package/commands/info.js +2 -3
  12. package/commands/list.js +93 -0
  13. package/commands/new.js +28 -56
  14. package/jest.config.js +9 -0
  15. package/package.json +7 -2
  16. package/readme.md +59 -68
  17. package/tests/e2e/generator.spec.js +71 -0
  18. package/tests/unit/appModuleUpdater.spec.js +111 -0
  19. package/tests/unit/cliParser.spec.js +74 -0
  20. package/tests/unit/controllerGenerator.spec.js +145 -0
  21. package/tests/unit/dtoGenerator.spec.js +87 -0
  22. package/tests/unit/entityGenerator.spec.js +65 -0
  23. package/tests/unit/generateCommand.spec.js +100 -0
  24. package/tests/unit/health.spec.js +69 -0
  25. package/tests/unit/listCommand.spec.js +91 -0
  26. package/tests/unit/middlewareGenerator.spec.js +64 -0
  27. package/tests/unit/newCommand.spec.js +88 -0
  28. package/tests/unit/relationCommand.spec.js +202 -0
  29. package/tests/unit/throttler.spec.js +117 -0
  30. package/utils/app-module.updater.js +6 -0
  31. package/utils/configs/setupCleanArchitecture.js +1 -1
  32. package/utils/configs/setupLightArchitecture.js +98 -26
  33. package/utils/envGenerator.js +9 -1
  34. package/utils/file-system.js +15 -0
  35. package/utils/file-utils/packageJsonUtils.js +6 -0
  36. package/utils/file-utils/saveProjectConfig.js +9 -0
  37. package/utils/fullModeInput.js +12 -229
  38. package/utils/generators/application/dtoGenerator.js +26 -8
  39. package/utils/generators/application/dtoUpdater.js +5 -0
  40. package/utils/generators/cleanModuleGenerator.js +40 -11
  41. package/utils/generators/domain/entityUpdater.js +5 -0
  42. package/utils/generators/infrastructure/mapperUpdater.js +5 -0
  43. package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
  44. package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
  45. package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
  46. package/utils/generators/lightModuleGenerator.js +14 -0
  47. package/utils/generators/presentation/controllerGenerator.js +70 -19
  48. package/utils/helpers.js +8 -2
  49. package/utils/interactive/askEntityInputs.js +5 -68
  50. package/utils/interactive/askRelationCommand.js +98 -0
  51. package/utils/interactive/entityBuilder.js +411 -0
  52. package/utils/lightModeInput.js +22 -241
  53. package/utils/setups/orms/typeOrmSetup.js +42 -8
  54. package/utils/setups/projectSetup.js +88 -10
  55. package/utils/setups/setupAuth.js +335 -20
  56. package/utils/setups/setupDatabase.js +4 -1
  57. package/utils/setups/setupHealth.js +74 -0
  58. package/utils/setups/setupMongoose.js +84 -32
  59. package/utils/setups/setupPrisma.js +151 -4
  60. package/utils/setups/setupSwagger.js +15 -8
  61. package/utils/setups/setupThrottler.js +92 -0
  62. package/utils/shell.js +61 -9
@@ -63,11 +63,23 @@ async function setupMongoose(inputs) {
63
63
 
64
64
  logInfo("📦 Installing Mongoose and @nestjs/mongoose...");
65
65
 
66
+ const pkgManager = inputs.packageManager || "npm";
67
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
68
+
66
69
  await runCommand(
67
- `${inputs.packageManager} install @nestjs/mongoose mongoose`,
70
+ `${installCmd} @nestjs/mongoose@^10.0.6 mongoose@^8.3.1`,
68
71
  "Mongoose and its dependencies successfully installed!"
69
72
  );
70
73
 
74
+ const isDryRun = process.argv.includes("--dry-run");
75
+ if (isDryRun) {
76
+ console.log("[DRY-RUN] Simulated: write Mongoose configuration, app.module.ts config, and schemas");
77
+ logSuccess(
78
+ "Mongoose configuration complete. Schemas are generated in src/schemas!"
79
+ );
80
+ return;
81
+ }
82
+
71
83
  // --- Base Configuration (app.module.ts and .env) --- // Generating the .env file
72
84
  const envPath = ".env";
73
85
  const mongoUriLine = `MONGO_URI=${inputs.dbConfig.MONGO_URI}`;
@@ -78,14 +90,10 @@ async function setupMongoose(inputs) {
78
90
  await createFile({ path: envPath, contente: envContent });
79
91
  } else {
80
92
  let content = fs.readFileSync(envPath, "utf8");
81
- if (content.includes("MONGO_URI=")) {
82
- content = content.replace(/MONGO_URI=.*/g, mongoUriLine);
83
- } else {
93
+ if (!content.includes("MONGO_URI=")) {
84
94
  content += `\n${mongoUriLine}`;
85
95
  }
86
- if (content.includes("MONGO_DB=")) {
87
- content = content.replace(/MONGO_DB=.*/g, mongoDbLine);
88
- } else {
96
+ if (!content.includes("MONGO_DB=")) {
89
97
  content += `\n${mongoDbLine}`;
90
98
  }
91
99
  fs.writeFileSync(envPath, content, "utf8");
@@ -98,21 +106,30 @@ async function setupMongoose(inputs) {
98
106
  dbName: process.env.MONGO_DB,
99
107
  }),`;
100
108
 
101
- // 1. Adding MongooseModule import
102
- await updateFile({
103
- path: appModulePath,
104
- pattern: /import {[\s\S]*?} from '@nestjs\/config';/,
105
- replacement: (match) => `${match}\n${mongooseImport}`,
106
- });
109
+ if (fs.existsSync(appModulePath)) {
110
+ let appModuleContent = fs.readFileSync(appModulePath, "utf8");
111
+
112
+ // 1. Adding MongooseModule import
113
+ if (!appModuleContent.includes("@nestjs/mongoose")) {
114
+ await updateFile({
115
+ path: appModulePath,
116
+ pattern: /import {[\s\S]*?} from '@nestjs\/config';/,
117
+ replacement: (match) => `${match}\n${mongooseImport}`,
118
+ });
119
+ appModuleContent = fs.readFileSync(appModulePath, "utf8");
120
+ }
107
121
 
108
- // 2. Adding MongooseModule.forRoot() configuration
109
- const importsPattern =
110
- /imports:\s*\[[\s\S]*?ConfigModule\.forRoot\([\s\S]*?\),/;
111
- await updateFile({
112
- path: appModulePath,
113
- pattern: importsPattern,
114
- replacement: (match) => `${match}${mongooseForRoot}`,
115
- });
122
+ // 2. Adding MongooseModule.forRoot() configuration
123
+ if (!appModuleContent.includes("MongooseModule.forRoot")) {
124
+ const importsPattern =
125
+ /imports:\s*\[[\s\S]*?ConfigModule\.forRoot\([\s\S]*?\),/;
126
+ await updateFile({
127
+ path: appModulePath,
128
+ pattern: importsPattern,
129
+ replacement: (match) => `${match}${mongooseForRoot}`,
130
+ });
131
+ }
132
+ }
116
133
 
117
134
  // --- Generating Mongoose Entities (Schemas) ---
118
135
  logInfo("📁 Generating Mongoose schemas (src/schemas)...");
@@ -161,10 +178,34 @@ role: Role;
161
178
  const mongooseType = mapTypeToMongoose(field.type);
162
179
  const tsType = field.type;
163
180
 
164
- const isRequired =
165
- field.name.toLowerCase() !== "isactive" &&
166
- field.name.toLowerCase() !== "password";
167
- const requiredOption = isRequired ? ", required: true" : "";
181
+ const isRequired = field.nullable !== undefined ? !field.nullable : (field.name.toLowerCase() !== "isactive" && field.name.toLowerCase() !== "password");
182
+ let propOpts = [`type: ${mongooseType}`];
183
+ if (isRequired) {
184
+ propOpts.push("required: true");
185
+ }
186
+ if (field.unique) {
187
+ propOpts.push("unique: true");
188
+ }
189
+ if (field.default !== undefined && field.default !== null && field.default !== "") {
190
+ let defaultVal = field.default;
191
+ if (typeof defaultVal === "string") {
192
+ if (defaultVal === "true") defaultVal = true;
193
+ else if (defaultVal === "false") defaultVal = false;
194
+ else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
195
+ defaultVal = "Date.now";
196
+ } else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
197
+ defaultVal = Number(defaultVal);
198
+ }
199
+ }
200
+ if (defaultVal === "Date.now") {
201
+ propOpts.push("default: Date.now");
202
+ } else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
203
+ propOpts.push(`default: ${defaultVal}`);
204
+ } else {
205
+ propOpts.push(`default: '${defaultVal}'`);
206
+ }
207
+ }
208
+ const propOptionsStr = propOpts.join(", ");
168
209
 
169
210
  // Import des enums partagés si nécessaire
170
211
  if (
@@ -176,8 +217,8 @@ role: Role;
176
217
  }
177
218
 
178
219
  fieldsContent += `
179
- @Prop({ type: ${mongooseType}${requiredOption} })
180
- ${field.name}: ${tsType};
220
+ @Prop({ ${propOptionsStr} })
221
+ ${field.name}${field.nullable ? "?" : ""}: ${tsType};
181
222
  `;
182
223
  }
183
224
 
@@ -203,11 +244,22 @@ role: Role;
203
244
  ", "
204
245
  )}]),`;
205
246
 
206
- await updateFile({
207
- path: appModulePath,
208
- pattern: new RegExp(mongooseForRoot.trim().replace(/[\n\r]/g, "\\s*"), "g"),
209
- replacement: (match) => `${match}\n\t${forFeatureBlock}`,
210
- });
247
+ if (fs.existsSync(appModulePath)) {
248
+ const appModuleContent = fs.readFileSync(appModulePath, "utf8");
249
+ if (appModuleContent.includes("MongooseModule.forFeature")) {
250
+ await updateFile({
251
+ path: appModulePath,
252
+ pattern: /MongooseModule\.forFeature\(\[[\s\S]*?\]\),/,
253
+ replacement: forFeatureBlock,
254
+ });
255
+ } else {
256
+ await updateFile({
257
+ path: appModulePath,
258
+ pattern: new RegExp(mongooseForRoot.trim().replace(/[\n\r]/g, "\\s*"), "g"),
259
+ replacement: (match) => `${match}\n\t${forFeatureBlock}`,
260
+ });
261
+ }
262
+ }
211
263
 
212
264
  if (inputs.isDemo) {
213
265
  // The generateSampleData function must be adapted for 'new Date()' usage and Mongoose types
@@ -147,7 +147,7 @@ async function setupPrisma(inputs) {
147
147
  !fieldsToExclude.includes(fieldNameLower) &&
148
148
  !addedFields.has(fieldNameLower)
149
149
  ) {
150
- schemaContent += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
150
+ schemaContent += `\n ${field.name} ${formatPrismaField(field)}`;
151
151
  addedFields.add(fieldNameLower);
152
152
  }
153
153
  }
@@ -281,6 +281,32 @@ async function setupPrisma(inputs) {
281
281
  }
282
282
  }
283
283
 
284
+ if (inputs.useAuth) {
285
+ schemaContent += `
286
+ /**
287
+ * PasswordReset Model
288
+ */
289
+ model PasswordReset {
290
+ id String @id @default(uuid())
291
+ email String
292
+ token String @unique
293
+ expiresAt DateTime
294
+ createdAt DateTime @default(now())
295
+ }
296
+
297
+ /**
298
+ * Otp Model
299
+ */
300
+ model Otp {
301
+ id String @id @default(uuid())
302
+ email String
303
+ otp String
304
+ expiresAt DateTime
305
+ createdAt DateTime @default(now())
306
+ }
307
+ `;
308
+ }
309
+
284
310
  logInfo("Updating schema.prisma");
285
311
  const baseSchema = `
286
312
  generator client {
@@ -431,6 +457,44 @@ function mapTypeToPrisma(type) {
431
457
  }
432
458
  }
433
459
 
460
+ /**
461
+ * Formats a field definition for the Prisma schema including constraints.
462
+ * @param {object} field
463
+ * @returns {string} Formatted field line content (e.g. "String? @unique")
464
+ */
465
+ function formatPrismaField(field) {
466
+ let prismaType = mapTypeToPrisma(field.type);
467
+ if (field.nullable) {
468
+ if (!prismaType.endsWith("?") && !prismaType.endsWith("[]")) {
469
+ prismaType += "?";
470
+ }
471
+ }
472
+ let attributes = "";
473
+ if (field.unique) {
474
+ attributes += " @unique";
475
+ }
476
+ if (field.default !== undefined && field.default !== null && field.default !== "") {
477
+ let defaultVal = field.default;
478
+ if (typeof defaultVal === "string") {
479
+ if (defaultVal === "true") defaultVal = true;
480
+ else if (defaultVal === "false") defaultVal = false;
481
+ else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
482
+ defaultVal = "now()";
483
+ } else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
484
+ defaultVal = Number(defaultVal);
485
+ }
486
+ }
487
+ if (defaultVal === "now()") {
488
+ attributes += ` @default(now())`;
489
+ } else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
490
+ attributes += ` @default(${defaultVal})`;
491
+ } else {
492
+ attributes += ` @default("${defaultVal}")`;
493
+ }
494
+ }
495
+ return `${prismaType}${attributes}`;
496
+ }
497
+
434
498
  async function setupPrismaSeeding(inputs) {
435
499
  logInfo("⚙️ Configuring seeding for Prisma...");
436
500
 
@@ -681,7 +745,7 @@ model ${capitalizedName} {
681
745
  updatedAt DateTime @updatedAt${relationFields}`;
682
746
 
683
747
  fields.forEach((field) => {
684
- modelBlock += `\n ${field.name} ${mapTypeToPrisma(field.type)}`;
748
+ modelBlock += `\n ${field.name} ${formatPrismaField(field)}`;
685
749
  });
686
750
 
687
751
  modelBlock += `\n}\n`;
@@ -738,11 +802,94 @@ model ${capitalizedName} {
738
802
  );
739
803
 
740
804
  await runCommand(
741
- `npx prisma migrate dev --name add_module_${entityData.name} --force`,
805
+ `npx prisma migrate dev --name add_module_${entityData.name}`,
742
806
  `❌ Failed to run prisma migration — run 'npx prisma migrate dev --name add_module_${entityData.name}' manually`,
743
807
  { critical: false }
744
808
  );
745
809
  }
746
810
 
747
- module.exports = { setupPrisma, updatePrismaSchema };
811
+ /**
812
+ * Ajoute une relation entre deux modèles Prisma EXISTANTS.
813
+ * Contrairement à updatePrismaSchema, ne crée PAS un nouveau modèle —
814
+ * patche uniquement les champs FK/inverse dans les deux modèles déjà présents.
815
+ *
816
+ * @param {string} source - Nom de l'entité source (ex: "post")
817
+ * @param {string} target - Nom de l'entité cible (ex: "user")
818
+ * @param {string} relationType - "n-1" | "1-n" | "1-1" | "n-n"
819
+ */
820
+ async function addPrismaRelation(source, target, relationType) {
821
+ const schemaPath = "prisma/schema.prisma";
822
+ const sourceCap = capitalize(source);
823
+ const targetCap = capitalize(target);
824
+ const sourceLow = source.toLowerCase();
825
+ const targetLow = target.toLowerCase();
826
+
827
+ // 1️⃣ Champs à injecter dans le modèle SOURCE
828
+ let sourceFields = "";
829
+ switch (relationType) {
830
+ case "n-1":
831
+ sourceFields = ` ${targetLow} ${targetCap} @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String`;
832
+ break;
833
+ case "1-n":
834
+ sourceFields = ` ${targetLow}s ${targetCap}[]`;
835
+ break;
836
+ case "1-1":
837
+ sourceFields = ` ${targetLow} ${targetCap}? @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String? @unique`;
838
+ break;
839
+ case "n-n":
840
+ sourceFields = ` ${targetLow}s ${targetCap}[]`;
841
+ break;
842
+ default:
843
+ throw new Error(`❌ Type de relation inconnu : ${relationType}`);
844
+ }
845
+
846
+ // 2️⃣ Champs inverses à injecter dans le modèle TARGET
847
+ let targetFields = "";
848
+ switch (relationType) {
849
+ case "n-1":
850
+ targetFields = ` ${sourceLow}s ${sourceCap}[]`;
851
+ break;
852
+ case "1-n":
853
+ targetFields = ` ${sourceLow} ${sourceCap} @relation(fields: [${sourceLow}Id], references: [id])\n ${sourceLow}Id String`;
854
+ break;
855
+ case "1-1":
856
+ targetFields = ` ${sourceLow} ${sourceCap}?`;
857
+ break;
858
+ case "n-n":
859
+ targetFields = ` ${sourceLow}s ${sourceCap}[]`;
860
+ break;
861
+ }
862
+
863
+ // 3️⃣ Patch du modèle SOURCE
864
+ await updateFile({
865
+ path: schemaPath,
866
+ pattern: new RegExp(`model ${sourceCap} \\{([\\s\\S]*?)\\}`, "g"),
867
+ replacement: `model ${sourceCap} {$1\n${sourceFields}\n}`,
868
+ });
869
+
870
+ // 4️⃣ Patch du modèle TARGET (inverse)
871
+ await updateFile({
872
+ path: schemaPath,
873
+ pattern: new RegExp(`model ${targetCap} \\{([\\s\\S]*?)\\}`, "g"),
874
+ replacement: `model ${targetCap} {$1\n${targetFields}\n}`,
875
+ });
876
+
877
+ // 5️⃣ Synchronisation Prisma
878
+ await runCommand(
879
+ "npx prisma format",
880
+ "❌ Failed to format prisma schema — run 'npx prisma format' manually",
881
+ { critical: false },
882
+ );
883
+ await runCommand(
884
+ "npx prisma generate",
885
+ "❌ Failed to generate prisma client — run 'npx prisma generate' manually",
886
+ { critical: false },
887
+ );
888
+ await runCommand(
889
+ `npx prisma migrate dev --name add_relation_${sourceLow}_${targetLow}`,
890
+ `❌ Failed to run prisma migration — run 'npx prisma migrate dev' manually`,
891
+ { critical: false },
892
+ );
893
+ }
748
894
 
895
+ module.exports = { setupPrisma, updatePrismaSchema, addPrismaRelation };
@@ -1,19 +1,26 @@
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
 
6
6
  async function setupSwagger(inputs) {
7
7
  logInfo("Installation et Configuration de Swagger...");
8
8
 
9
+ const pkgManager = inputs.packageManager || "npm";
10
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
11
+
9
12
  // 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);
13
+ await runCommand(
14
+ `${installCmd} @nestjs/swagger@^7.3.0 swagger-ui-express@^5.0.0`,
15
+ "Échec de l'installation de Swagger",
16
+ { spinner: "Installing Swagger..." }
17
+ );
18
+
19
+ const isDryRun = process.argv.includes("--dry-run");
20
+ if (isDryRun) {
21
+ console.log("[DRY-RUN] Simulated: write Swagger configuration to src/main.ts");
22
+ logSuccess(" Swagger configuré avec succès !");
23
+ return;
17
24
  }
18
25
 
19
26
  // Modification de main.ts pour intégrer Swagger
@@ -0,0 +1,92 @@
1
+ // utils/setups/setupThrottler.js
2
+ //
3
+ // Script d'installation et de configuration de @nestjs/throttler.
4
+ // Injecte le module et le guard dans app.module.ts de manière idempotente.
5
+
6
+ const fs = require("fs");
7
+ const { logInfo } = require("../loggers/logInfo");
8
+ const { logSuccess } = require("../loggers/logSuccess");
9
+ const { runCommand } = require("../shell");
10
+ const { updateFile } = require("../userInput");
11
+
12
+ /**
13
+ * Configure le rate limiting global dans le projet.
14
+ *
15
+ * @param {object} inputs - Configuration du projet
16
+ */
17
+ async function setupThrottler(inputs) {
18
+ logInfo("Installation et Configuration du Rate Limiting (Throttler)...");
19
+
20
+ const pkgManager = inputs.packageManager || "npm";
21
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
22
+
23
+ // Installation de @nestjs/throttler
24
+ await runCommand(
25
+ `${installCmd} @nestjs/throttler@^6.0.0`,
26
+ "Échec de l'installation de @nestjs/throttler",
27
+ { spinner: "Installing @nestjs/throttler..." }
28
+ );
29
+
30
+ const isDryRun = process.argv.includes("--dry-run");
31
+ if (isDryRun) {
32
+ console.log("[DRY-RUN] Simulated: write Throttler configuration to src/app.module.ts");
33
+ logSuccess(" Throttler configuré avec succès !");
34
+ return;
35
+ }
36
+
37
+ const appModulePath = "src/app.module.ts";
38
+ if (!fs.existsSync(appModulePath)) {
39
+ console.warn(`⚠️ Fichier ${appModulePath} introuvable. Skip configuration.`);
40
+ return;
41
+ }
42
+
43
+ let content = fs.readFileSync(appModulePath, "utf8");
44
+
45
+ if (!content.includes("ThrottlerModule")) {
46
+ // 1. Ajouter les imports
47
+ content = content.replace(
48
+ "import { Module } from '@nestjs/common';",
49
+ "import { Module } from '@nestjs/common';\nimport { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';\nimport { APP_GUARD } from '@nestjs/core';"
50
+ );
51
+
52
+ // 2. Ajouter ThrottlerModule.forRoot dans imports: [
53
+ content = content.replace(
54
+ "imports: [",
55
+ `imports: [
56
+ ThrottlerModule.forRoot([{
57
+ ttl: 60000,
58
+ limit: 10,
59
+ }]),`
60
+ );
61
+
62
+ // 3. Ajouter ThrottlerGuard dans providers: [
63
+ if (content.includes("providers: [")) {
64
+ content = content.replace(
65
+ "providers: [",
66
+ `providers: [
67
+ {
68
+ provide: APP_GUARD,
69
+ useClass: ThrottlerGuard,
70
+ },`
71
+ );
72
+ } else {
73
+ content = content.replace(
74
+ "@Module({",
75
+ `@Module({
76
+ providers: [
77
+ {
78
+ provide: APP_GUARD,
79
+ useClass: ThrottlerGuard,
80
+ },
81
+ ],`
82
+ );
83
+ }
84
+
85
+ fs.writeFileSync(appModulePath, content, "utf8");
86
+ logSuccess(" Throttler configuré avec succès dans app.module.ts !");
87
+ } else {
88
+ logInfo(" Throttler déjà présent dans app.module.ts. Skip.");
89
+ }
90
+ }
91
+
92
+ module.exports = { setupThrottler };
package/utils/shell.js CHANGED
@@ -20,44 +20,96 @@ const _warnings = [];
20
20
  * @param {boolean} [options.critical=true] - If true (default), a failure calls process.exit(1).
21
21
  * If false, the failure is logged as a warning and execution continues.
22
22
  * @param {string|null} [options.spinner] - Optional spinner label shown during execution.
23
+ * @param {number} [options.timeout] - Custom timeout in milliseconds.
23
24
  * @returns {Promise<boolean>} Resolves to true on success, false on non-critical failure.
24
25
  */
25
26
  async function runCommand(command, errorMessage, options = {}) {
26
- // Support old call signature: runCommand(cmd, msg, spinnerText)
27
- // New signature: runCommand(cmd, msg, { critical, spinner })
28
27
  let critical = true;
29
28
  let spinnerText = null;
29
+ let customTimeout = undefined;
30
30
 
31
31
  if (typeof options === "string") {
32
- // Legacy: third arg was spinnerText string
33
32
  spinnerText = options;
34
33
  } else if (typeof options === "object" && options !== null) {
35
- critical = options.critical !== false; // default true
34
+ critical = options.critical !== false;
36
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 = 600000; // 10 minutes for heavy setup commands
48
+ }
49
+ if (customTimeout !== undefined) {
50
+ timeoutVal = customTimeout;
51
+ }
52
+
53
+ // Intercept npm installation commands and append --legacy-peer-deps, --no-audit, --no-fund if missing
54
+ let finalCommand = command;
55
+ if (
56
+ command.startsWith("npm ") &&
57
+ (command.includes("install") || command.includes("add") || command.includes(" i "))
58
+ ) {
59
+ if (!command.includes("--legacy-peer-deps")) {
60
+ finalCommand = `${finalCommand} --legacy-peer-deps`;
61
+ }
62
+ if (!command.includes("--no-audit")) {
63
+ finalCommand = `${finalCommand} --no-audit`;
64
+ }
65
+ if (!command.includes("--no-fund")) {
66
+ finalCommand = `${finalCommand} --no-fund`;
67
+ }
68
+ }
69
+
70
+ // Dry-Run Simulation Check
71
+ const isDryRun = process.argv.includes("--dry-run");
72
+ if (isDryRun) {
73
+ if (spinnerText) {
74
+ const spin = spinner(spinnerText);
75
+ spin.start();
76
+ spin.succeed(`[DRY-RUN] Simulated: ${finalCommand}`);
77
+ } else {
78
+ console.log(`[DRY-RUN] Simulated: ${finalCommand}`);
79
+ }
80
+ return true;
37
81
  }
38
82
 
39
83
  const spin = spinnerText ? spinner(spinnerText) : null;
40
84
 
41
85
  try {
42
86
  if (spin) spin.start();
43
- execSync(command, { stdio: spinnerText ? "pipe" : "inherit" });
87
+ execSync(finalCommand, {
88
+ stdio: spinnerText ? "pipe" : "inherit",
89
+ timeout: timeoutVal,
90
+ });
44
91
  if (spin) spin.succeed(spinnerText);
45
92
  return true;
46
93
  } catch (error) {
47
94
  if (spin) spin.fail(errorMessage);
48
95
 
49
- const logLine = `[${new Date().toISOString()}] ${errorMessage}: ${error.message}\n`;
96
+ let errorDetail = error.message;
97
+ if (error.code === "ETIMEDOUT" || error.signal === "SIGTERM") {
98
+ errorDetail = `Command timed out after ${timeoutVal / 1000}s`;
99
+ }
100
+
101
+ const logLine = `[${new Date().toISOString()}] ${errorMessage}: ${errorDetail}\n`;
50
102
  fs.appendFileSync("setup.log", logLine);
51
103
 
52
104
  if (critical) {
53
- // Fatal — stop everything
54
105
  logError(`${errorMessage}`);
55
106
  logError(`Run: ${command}`);
107
+ logError(`Detail: ${errorDetail}`);
56
108
  process.exit(1);
57
109
  } else {
58
- // Non-critical — warn and continue
59
110
  logWarning(`${errorMessage} (non-fatal, continuing...)`);
60
111
  logWarning(` Command: ${command}`);
112
+ logWarning(` Detail: ${errorDetail}`);
61
113
  logWarning(` See setup.log for details`);
62
114
  _warnings.push({ command, errorMessage });
63
115
  return false;
@@ -72,7 +124,7 @@ async function runCommand(command, errorMessage, options = {}) {
72
124
  */
73
125
  async function runCommandSilent(command) {
74
126
  try {
75
- return execSync(command, { stdio: "pipe" }).toString();
127
+ return execSync(command, { stdio: "pipe", timeout: 10000 }).toString();
76
128
  } catch (error) {
77
129
  return null;
78
130
  }