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
@@ -6,6 +6,7 @@ const { capitalize } = require("./userInput");
6
6
  const { logWarning } = require("./loggers/logWarning");
7
7
  const { logInfo } = require("./loggers/logInfo");
8
8
  const { getPackageManager } = require("./utils");
9
+ const { buildEntityInteractive, buildRelationsInteractive } = require("./interactive/entityBuilder");
9
10
  const actualInquirer = inquirer.default || inquirer;
10
11
 
11
12
  async function getLightModeInputs(projectName, flags) {
@@ -93,12 +94,16 @@ async function getLightModeInputs(projectName, flags) {
93
94
  // JWT Auth config
94
95
  const useAuth = getAuthChoice(flags);
95
96
 
97
+ // Throttler config
98
+ const useThrottler = getThrottlerChoice(flags);
99
+
96
100
  const packageManager = await getPackageManager(flags);
97
101
  inputs.packageManager = packageManager;
98
102
 
99
103
  inputs.useAuth = useAuth;
100
104
  inputs.useSwagger = useSwagger;
101
105
  inputs.useDocker = useDocker;
106
+ inputs.useThrottler = useThrottler;
102
107
 
103
108
  if (useAuth) {
104
109
  console.log(
@@ -139,7 +144,17 @@ async function getLightModeInputs(projectName, flags) {
139
144
  );
140
145
  if (addEntities) {
141
146
  console.log(`\n${info("[INFO]")} Entity input (simplified mode)`);
142
- await addCustomEntities(inputs.entitiesData);
147
+ while (true) {
148
+ const entity = await buildEntityInteractive();
149
+ if (entity) {
150
+ inputs.entitiesData.entities.push(entity);
151
+ console.log(
152
+ `${info("[INFO]")} Entity "${entity.name}" added with ${entity.fields.length} field(s)`
153
+ );
154
+ }
155
+ const addMore = readline.keyInYNStrict("Add another entity?");
156
+ if (!addMore) break;
157
+ }
143
158
  }
144
159
 
145
160
  // Demander les relations entre entités
@@ -149,7 +164,7 @@ async function getLightModeInputs(projectName, flags) {
149
164
  );
150
165
  if (wantsRelation) {
151
166
  console.log(`\n${info("[INFO]")} Configuring relationships`);
152
- await addRelations(inputs.entitiesData);
167
+ await buildRelationsInteractive(inputs.entitiesData);
153
168
  }
154
169
  }
155
170
 
@@ -213,248 +228,14 @@ function getDockerChoice(flags) {
213
228
  return readline.keyInYNStrict(`${info("[?]")} Generate Docker files ?`);
214
229
  }
215
230
 
216
- async function addCustomEntities(entitiesData) {
217
- while (true) {
218
- let name;
219
- while (true) {
220
- name = readline.question("\nEntity name (empty to finish) : ");
221
- if (!name) return;
222
- if (/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) break;
223
- logWarning("Invalid name. Use letters, numbers, and _ only.");
224
- }
225
-
226
- const fields = [];
227
- console.log(` Fields for entity "${name}" :`);
228
-
229
- while (true) {
230
- const fieldName = readline.question(" Field name (empty to finish) : ");
231
- if (!fieldName) break;
232
-
233
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fieldName)) {
234
- logWarning("Invalid field name.");
235
- continue;
236
- }
237
-
238
- const baseTypeChoices = [
239
- "string",
240
- "text",
241
- "number",
242
- "decimal",
243
- "boolean",
244
- "Date",
245
- "uuid",
246
- "json",
247
- "enum",
248
- "array",
249
- "object",
250
- ];
251
-
252
- const typeQuestion = {
253
- type: "list",
254
- name: "ftype",
255
- message: `Type of "${fieldName}"`,
256
- default: "string",
257
- choices: baseTypeChoices,
258
- };
259
-
260
- const typeAnswer = await actualInquirer.prompt([typeQuestion]);
261
- let fieldType = typeAnswer.ftype; // --- ADVANCED LOGIC FOR ARRAY, ENUM, AND OBJECT ---
262
- // Déplace le curseur d'une ligne vers le haut :
263
- process.stdout.write("\x1B[1A");
264
- // Efface la ligne (où se trouvait le "√ Type for...") :
265
- process.stdout.write("\x1B[K");
266
-
267
- if (fieldType === "array") {
268
- // Prompt spécifique pour le type interne du tableau
269
- const arrayInnerQuestion = {
270
- type: "list",
271
- name: "innerType",
272
- message: `Type of elements in "${fieldName}[]"`,
273
- default: "string", // Exclure array et object du sous-type pour simplifier
274
- choices: baseTypeChoices.filter(
275
- (c) => c !== "array" && c !== "object"
276
- ),
277
- };
278
-
279
- const innerAnswer = await actualInquirer.prompt([arrayInnerQuestion]);
280
- fieldType = `${innerAnswer.innerType}[]`;
281
- } else if (fieldType === "enum") {
282
- const enumName = capitalize(fieldName) + "Enum";
283
- logInfo(
284
- `Enum type selected. Remember to define ${enumName} in your code.`
285
- );
286
- fieldType = enumName;
287
- } else if (fieldType === "object") {
288
- // Prompt pour nommer l'objet (ou 'json' par défaut)
289
- const objectNameQuestion = {
290
- type: "input",
291
- name: "objectName",
292
- message: `Complex type name (DTO/Class or leave 'json') :`,
293
- default: "json",
294
- };
295
-
296
- const objectAnswer = await actualInquirer.prompt([objectNameQuestion]);
297
- fieldType = capitalize(objectAnswer.objectName.trim() || "json");
298
- }
299
-
300
- console.log(
301
- ` Field type for "${fieldName}" : ${fieldType} ${success("[✓]")}`
302
- );
303
-
304
- fields.push({ name: fieldName, type: fieldType });
305
- }
306
-
307
- if (fields.length > 0) {
308
- entitiesData.entities.push({ name, fields });
309
- console.log(
310
- `${info("[INFO]")} Entity "${name}" added with ${
311
- fields.length
312
- } field(s)`
313
- );
314
- }
315
-
316
- const addMore = readline.keyInYNStrict("Add another entity?");
317
- if (!addMore) break;
318
- }
319
- }
320
-
321
- /**
322
- * Manages adding relationships between entities via an interactive interface.
323
- * All prompts and logs are in English.
324
- * @param {Object} entitiesData - The object containing entities and relations.
325
- */
326
- async function addRelations(entitiesData) {
327
- if (entitiesData.entities.length < 2) {
328
- logInfo("At least two entities are required to create a relationship.");
329
- return;
330
- }
331
-
332
- let configuring = true;
333
-
334
- while (configuring) {
335
- const entityNames = entitiesData.entities.map((e) => e.name);
336
-
337
- // 1. Interactive selection of entities
338
- const answers = await actualInquirer.prompt([
339
- {
340
- type: "list",
341
- name: "fromName",
342
- message: "Select the source entity (From) :",
343
- choices: entityNames,
344
- },
345
- {
346
- type: "list",
347
- name: "toName",
348
- message: (prev) =>
349
- `To which entity do you want to link ${prev.fromName} ?`,
350
- choices: (prev) => entityNames.filter((name) => name !== prev.fromName),
351
- },
352
- ]);
353
-
354
- // --- CHECK FOR EXISTING RELATIONSHIPS ---
355
- const alreadyExists = entitiesData.relations.find(
356
- (rel) =>
357
- (rel.from === answers.fromName && rel.to === answers.toName) ||
358
- (rel.from === answers.toName && rel.to === answers.fromName)
359
- );
360
-
361
- if (alreadyExists) {
362
- logWarning(
363
- `A relationship already exists between ${answers.fromName} and ${answers.toName} (${alreadyExists.type}).`
364
- );
365
-
366
- const { tryAgain } = await actualInquirer.prompt([
367
- {
368
- type: "confirm",
369
- name: "tryAgain",
370
- message: "Would you like to choose different entities ?",
371
- default: true,
372
- },
373
- ]);
374
-
375
- if (!tryAgain) break;
376
- // Restart the loop
377
- continue;
378
- }
379
-
380
- // 2. Select Relationship Type
381
- const typeAnswer = await actualInquirer.prompt([
382
- {
383
- type: "list",
384
- name: "relType",
385
- message: "What is the relationship type?",
386
- choices: [
387
- {
388
- name: `1-1 (One-to-One) : ${answers.fromName} is linked to exactly one ${answers.toName}`,
389
- value: "1-1",
390
- },
391
- {
392
- name: `1-n (One-to-Many) : ${answers.fromName} owns multiple ${answers.toName}s`,
393
- value: "1-n",
394
- },
395
- {
396
- name: `n-1 (Many-to-One) : Multiple ${answers.fromName}s belong to one ${answers.toName}`,
397
- value: "n-1",
398
- },
399
- {
400
- name: `n-n (Many-to-Many) : Multiple ${answers.fromName}s are linked to multiple ${answers.toName}s`,
401
- value: "n-n",
402
- },
403
- ],
404
- },
405
- ]);
406
-
407
- const from = entitiesData.entities.find((e) => e.name === answers.fromName);
408
- const to = entitiesData.entities.find((e) => e.name === answers.toName);
409
- const relType = typeAnswer.relType;
410
-
411
- // 3. Register relationship
412
- entitiesData.relations.push({
413
- from: from.name,
414
- to: to.name,
415
- type: relType,
416
- });
417
-
418
- // 4. Automatic field injection
419
- const fromLow = from.name.toLowerCase();
420
- const toLow = to.name.toLowerCase();
421
- if (relType === "1-1") {
422
- from.fields.push(
423
- { name: `${toLow}Id`, type: "string" },
424
- { name: toLow, type: to.name }
425
- );
426
- } else if (relType === "1-n") {
427
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
428
- to.fields.push(
429
- { name: `${fromLow}Id`, type: "string" },
430
- { name: fromLow, type: from.name }
431
- );
432
- } else if (relType === "n-1") {
433
- from.fields.push(
434
- { name: `${toLow}Id`, type: "string" },
435
- { name: toLow, type: to.name }
436
- );
437
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
438
- } else if (relType === "n-n") {
439
- from.fields.push({ name: `${toLow}s`, type: `${to.name}[]` });
440
- to.fields.push({ name: `${fromLow}s`, type: `${from.name}[]` });
441
- }
231
+ function getThrottlerChoice(flags) {
232
+ if (flags.throttler !== undefined) {
442
233
  console.log(
443
- `${success("[]")} Relationship added: ${from.name} ${relType} ${to.name}`
234
+ `${info("[INFO]")} Throttler: ${flags.throttler ? "Yes" : "No"} (via flag)`
444
235
  );
445
-
446
- // 5. Ask to continue
447
- const { addMore } = await actualInquirer.prompt([
448
- {
449
- type: "confirm",
450
- name: "addMore",
451
- message: "Do you want to add another relationship ?",
452
- default: false,
453
- },
454
- ]);
455
-
456
- configuring = addMore;
236
+ return flags.throttler === true || flags.throttler === "true";
457
237
  }
238
+ return readline.keyInYNStrict(`${info("[?]")} Enable Rate Limiting (Throttler) ?`);
458
239
  }
459
240
 
460
241
  module.exports = { getLightModeInputs };
@@ -21,8 +21,11 @@ async function setupTypeORM(inputs) {
21
21
  logInfo("📦 Installing TypeORM and PostgreSQL dependencies...");
22
22
 
23
23
  const mode = inputs.mode;
24
+ const pkgManager = inputs.packageManager || "npm";
25
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
26
+
24
27
  await runCommand(
25
- "npm install @nestjs/typeorm typeorm pg reflect-metadata",
28
+ `${installCmd} @nestjs/typeorm@^10.0.2 typeorm@^0.3.20 pg@^8.11.3 reflect-metadata@^0.2.1`,
26
29
  "TypeORM and PostgreSQL dependencies installed successfully"
27
30
  ); // Updating app.module.ts with TypeORM
28
31
 
@@ -162,12 +165,39 @@ async function setupTypeORM(inputs) {
162
165
  columnOptions.push("default: Role.USER");
163
166
  }
164
167
 
165
- if (field.optional) {
168
+ if (field.unique || field.name.toLowerCase() === "email") {
169
+ columnOptions.push("unique: true");
170
+ }
171
+
172
+ const isNullable = field.nullable || field.optional;
173
+ if (isNullable) {
166
174
  columnOptions.push("nullable: true");
167
175
  }
168
176
 
169
- if (field.name.toLowerCase() === "email") {
170
- columnOptions.push("unique: true");
177
+ if (field.default !== undefined && field.default !== null && field.default !== "") {
178
+ let defaultVal = field.default;
179
+ if (typeof defaultVal === "string") {
180
+ if (defaultVal === "true") defaultVal = true;
181
+ else if (defaultVal === "false") defaultVal = false;
182
+ else if (defaultVal.toLowerCase() === "now" || defaultVal.toLowerCase() === "now()") {
183
+ defaultVal = "CURRENT_TIMESTAMP";
184
+ } else if (!isNaN(defaultVal) && defaultVal.trim() !== "") {
185
+ defaultVal = Number(defaultVal);
186
+ }
187
+ }
188
+ if (defaultVal === "CURRENT_TIMESTAMP") {
189
+ columnOptions.push(`default: () => 'CURRENT_TIMESTAMP'`);
190
+ } else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
191
+ columnOptions.push(`default: ${defaultVal}`);
192
+ } else if (mapping.type === "enum") {
193
+ if (mapping.tsType && /^[A-Z]/.test(mapping.tsType)) {
194
+ columnOptions.push(`default: ${mapping.tsType}.${defaultVal.toUpperCase()}`);
195
+ } else {
196
+ columnOptions.push(`default: '${defaultVal}'`);
197
+ }
198
+ } else {
199
+ columnOptions.push(`default: '${defaultVal}'`);
200
+ }
171
201
  }
172
202
 
173
203
  // 2. Gestion des Imports d'Enums Custom
@@ -186,7 +216,7 @@ async function setupTypeORM(inputs) {
186
216
  // 3. Construction du champ
187
217
  fieldsContent += `
188
218
  @Column({ ${columnOptions.join(", ")} })
189
- ${field.name}${field.optional ? "?" : ""}: ${mapping.tsType};
219
+ ${field.name}${isNullable ? "?" : ""}: ${mapping.tsType};
190
220
  `;
191
221
  }
192
222
 
@@ -419,13 +449,17 @@ async function setupTypeOrmSeeding(inputs) {
419
449
  "typeorm-extension",
420
450
  ];
421
451
 
452
+ const pkgManager = inputs.packageManager || "npm";
453
+ const installDevCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add -D` : "npm install -D";
454
+ const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
455
+
422
456
  await runCommand(
423
- `${inputs.packageManager} add -D ${typeOrmDevDeps.join(" ")}`,
457
+ `${installDevCmd} ${typeOrmDevDeps.join(" ")}`,
424
458
  "❌ Failed to install TypeORM seeding dependencies"
425
459
  );
426
460
 
427
461
  await runCommand(
428
- `${inputs.packageManager} install bcrypt`,
462
+ `${installCmd} bcrypt`,
429
463
  "❌ Failed to install bcrypt"
430
464
  );
431
465
 
@@ -435,7 +469,7 @@ async function setupTypeOrmSeeding(inputs) {
435
469
  "typeorm-ts-node-commonjs migration:run -d ./src/database/typeorm.config.ts",
436
470
  "typeorm:seed":
437
471
  "typeorm-extension seed:run -d src/database/typeorm.config.ts",
438
- seed: "npm run typeorm:seed",
472
+ seed: `${pkgManager} run typeorm:seed`,
439
473
  };
440
474
 
441
475
  await updatePackageJson(inputs, typeOrmScripts);
@@ -1,8 +1,24 @@
1
1
  const readline = require("readline-sync");
2
- const { logInfo } = require("../loggers/logInfo");
3
- const { runCommand } = require("../shell");
4
2
  const fs = require("fs");
3
+ const path = require("path");
5
4
  const { info } = require("../colors");
5
+ const { logInfo } = require("../loggers/logInfo");
6
+ const { logError } = require("../loggers/logError");
7
+ const { runCommand } = require("../shell");
8
+
9
+ // Pipeline imports
10
+ const { setupCleanArchitecture } = require("../configs/setupCleanArchitecture");
11
+ const { setupLightArchitecture } = require("../configs/setupLightArchitecture");
12
+ const { setupAuth } = require("./setupAuth");
13
+ const { setupSwagger } = require("./setupSwagger");
14
+ const { setupThrottler } = require("./setupThrottler");
15
+ const { setupHealth } = require("./setupHealth");
16
+ const { setupDatabase } = require("./setupDatabase");
17
+ const { configureDocker } = require("../configs/configureDocker");
18
+ const { setupBootstrapLogger } = require("./setupLogger");
19
+ const { generateEnvFile, writeEnvFile } = require("../envGenerator");
20
+ const { saveProjectConfig } = require("../file-utils/saveProjectConfig");
21
+ const { printSetupWarnings } = require("../shell");
6
22
 
7
23
  async function createProject(inputs) {
8
24
  if (fs.existsSync(inputs.projectName)) {
@@ -13,34 +29,96 @@ async function createProject(inputs) {
13
29
  );
14
30
 
15
31
  if (confirmation) {
16
- // confirmation is true if the user presses 'y'
17
32
  console.log("Deleting existing project...");
18
33
  fs.rmSync(inputs.projectName, { recursive: true, force: true });
19
34
  } else {
20
35
  console.log("Operation cancelled by user. Exiting.");
21
- return; // Stop function execution if the user cancels
36
+ throw new Error("Operation cancelled by user.");
22
37
  }
23
38
  }
24
39
 
25
40
  logInfo(`Creating NestJS project: ${inputs.projectName}`);
26
41
 
27
- // Remains asynchronous as runCommand likely involves external processes (npm/npx)
28
42
  await runCommand(
29
- `npx @nestjs/cli new ${inputs.projectName} --package-manager ${inputs.packageManager}`,
43
+ `npx @nestjs/cli new ${inputs.projectName} --package-manager ${inputs.packageManager} --skip-install`,
30
44
  "Failed to create NestJS project"
31
45
  );
32
46
 
33
47
  // Changing the current process directory to the project root
34
- process.chdir(inputs.projectName);
48
+ const isDryRun = process.argv.includes("--dry-run");
49
+ if (!isDryRun) {
50
+ process.chdir(inputs.projectName);
51
+ } else {
52
+ console.log(`[DRY-RUN] Simulated: change directory to ${inputs.projectName}`);
53
+ }
35
54
 
36
55
  // Installing dependencies
37
56
  logInfo("Installing dependencies...");
38
57
 
39
- // Remains asynchronous as runCommand likely involves external processes (npm/npx)
40
58
  await runCommand(
41
- `${inputs.packageManager} add @nestjs/config class-validator class-transformer dotenv`,
59
+ `${inputs.packageManager} add @nestjs/config@^3.2.0 class-validator@^0.14.1 class-transformer@^0.5.1 dotenv@^16.4.5`,
42
60
  "Failed to install dependencies"
43
61
  );
44
62
  }
45
63
 
46
- module.exports = { createProject };
64
+ /**
65
+ * Runs the complete project scaffolding and setup pipeline in sequence.
66
+ * Handles CWD tracking and restores the working directory on failure.
67
+ * @param {object} inputs
68
+ */
69
+ async function runProjectSetupPipeline(inputs) {
70
+ const originalCwd = process.cwd();
71
+ try {
72
+ logInfo("Starting project generation...");
73
+
74
+ await createProject(inputs);
75
+
76
+ if (inputs.mode === "light") {
77
+ await setupLightArchitecture(inputs);
78
+ } else {
79
+ await setupCleanArchitecture(inputs);
80
+ }
81
+
82
+ if (inputs.useThrottler) {
83
+ await setupThrottler(inputs);
84
+ }
85
+
86
+ await setupHealth();
87
+
88
+ if (inputs.useAuth) {
89
+ await setupAuth(inputs);
90
+ }
91
+
92
+ if (inputs.useSwagger) {
93
+ await setupSwagger(inputs.swaggerInputs);
94
+ } else {
95
+ setupBootstrapLogger();
96
+ }
97
+
98
+ if (inputs.useDocker) {
99
+ await configureDocker(inputs);
100
+ }
101
+
102
+ await setupDatabase(inputs);
103
+
104
+ const envContent = await generateEnvFile(inputs);
105
+ writeEnvFile(envContent);
106
+
107
+ await saveProjectConfig(inputs);
108
+
109
+ // Show warnings
110
+ printSetupWarnings();
111
+
112
+ } catch (error) {
113
+ logError(`Error during project creation: ${error.message}`);
114
+ if (process.cwd() !== originalCwd) {
115
+ process.chdir(originalCwd);
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ createProject,
123
+ runProjectSetupPipeline,
124
+ };