@ifecodes/backend-template 1.1.9 → 1.4.1

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 (94) hide show
  1. package/README.md +423 -383
  2. package/bin/cli.js +1294 -964
  3. package/bin/lib/microservice-config.js +155 -150
  4. package/bin/lib/prompts.js +277 -241
  5. package/bin/lib/readme-generator.js +364 -329
  6. package/bin/lib/service-setup.js +901 -684
  7. package/package.json +64 -55
  8. package/template/base/js/.eslintrc.json +10 -13
  9. package/template/base/js/.prettierrc +7 -7
  10. package/template/base/js/eslint.config.js +33 -31
  11. package/template/base/js/package.json +29 -28
  12. package/template/base/js/src/app.js +20 -18
  13. package/template/base/js/src/config/db.js +8 -8
  14. package/template/base/js/src/config/env.js +44 -14
  15. package/template/base/js/src/config/index.js +7 -7
  16. package/template/base/js/src/docs/index.js +5 -0
  17. package/template/base/js/src/docs/route-registry.js +63 -0
  18. package/template/base/js/src/middlewares/error-handler.middleware.js +22 -19
  19. package/template/base/js/src/middlewares/index.js +15 -11
  20. package/template/base/js/src/middlewares/method-not-allowed.middleware.js +19 -13
  21. package/template/base/js/src/middlewares/not-found.middleware.js +13 -10
  22. package/template/base/js/src/middlewares/observability.middleware.js +24 -0
  23. package/template/base/js/src/middlewares/root.middleware.js +18 -16
  24. package/template/base/js/src/middlewares/validation.middleware.js +39 -0
  25. package/template/base/js/src/modules/index.js +8 -8
  26. package/template/base/js/src/modules/v1/health/health.controller.auth.js +29 -0
  27. package/template/base/js/src/modules/v1/health/health.controller.js +21 -21
  28. package/template/base/js/src/modules/v1/health/health.route.js +74 -9
  29. package/template/base/js/src/modules/v1/health/index.js +5 -5
  30. package/template/base/js/src/modules/v1/index.js +8 -8
  31. package/template/base/js/src/routes.js +23 -16
  32. package/template/base/js/src/server.js +18 -18
  33. package/template/base/js/src/utils/http-error.js +74 -74
  34. package/template/base/js/src/utils/index.js +28 -28
  35. package/template/base/js/src/utils/logger.js +57 -67
  36. package/template/base/ts/.eslintrc.json +13 -17
  37. package/template/base/ts/.prettierrc +7 -7
  38. package/template/base/ts/eslint.config.js +33 -33
  39. package/template/base/ts/package.json +41 -39
  40. package/template/base/ts/src/app.ts +20 -18
  41. package/template/base/ts/src/config/db.ts +4 -4
  42. package/template/base/ts/src/config/env.ts +40 -10
  43. package/template/base/ts/src/config/index.ts +2 -2
  44. package/template/base/ts/src/docs/index.ts +3 -0
  45. package/template/base/ts/src/docs/route-registry.ts +98 -0
  46. package/template/base/ts/src/middlewares/error-handler.middleware.ts +4 -1
  47. package/template/base/ts/src/middlewares/index.ts +6 -4
  48. package/template/base/ts/src/middlewares/method-not-allowed.middleware.ts +23 -18
  49. package/template/base/ts/src/middlewares/not-found.middleware.ts +10 -8
  50. package/template/base/ts/src/middlewares/observability.middleware.ts +25 -0
  51. package/template/base/ts/src/middlewares/root.middleware.ts +16 -14
  52. package/template/base/ts/src/middlewares/validation.middleware.ts +46 -0
  53. package/template/base/ts/src/modules/index.ts +8 -8
  54. package/template/base/ts/src/modules/v1/health/health.controller.auth.ts +26 -0
  55. package/template/base/ts/src/modules/v1/health/health.controller.ts +18 -18
  56. package/template/base/ts/src/modules/v1/health/health.route.ts +68 -9
  57. package/template/base/ts/src/modules/v1/health/index.ts +1 -1
  58. package/template/base/ts/src/modules/v1/index.ts +8 -8
  59. package/template/base/ts/src/routes.ts +23 -15
  60. package/template/base/ts/src/server.ts +19 -19
  61. package/template/base/ts/src/utils/http-error.ts +63 -63
  62. package/template/base/ts/src/utils/index.ts +14 -14
  63. package/template/base/ts/src/utils/logger.ts +58 -68
  64. package/template/base/ts/tsconfig.json +21 -21
  65. package/template/features/auth/argon2/inject.js +50 -50
  66. package/template/features/auth/base/health-openapi.ts +62 -0
  67. package/template/features/auth/base/inject.js +174 -172
  68. package/template/features/auth/bcrypt/inject.js +40 -40
  69. package/template/features/auth/models/index.ts +1 -1
  70. package/template/features/auth/models/user.model.js +24 -24
  71. package/template/features/auth/models/user.model.ts +28 -28
  72. package/template/features/auth/modules/auth.controller.js +21 -21
  73. package/template/features/auth/modules/auth.controller.ts +28 -20
  74. package/template/features/auth/modules/auth.routes.js +89 -10
  75. package/template/features/auth/modules/auth.routes.ts +86 -11
  76. package/template/features/auth/modules/auth.service.js +29 -29
  77. package/template/features/auth/modules/auth.service.ts +38 -38
  78. package/template/features/auth/modules/index.js +1 -1
  79. package/template/features/auth/modules/index.ts +1 -1
  80. package/template/features/auth/utils/hash.ts +20 -20
  81. package/template/features/auth/utils/jwt.js +12 -12
  82. package/template/features/auth/utils/jwt.ts +15 -15
  83. package/template/features/cors/inject.js +14 -13
  84. package/template/features/helmet/inject.js +7 -6
  85. package/template/features/morgan/inject.js +8 -7
  86. package/template/features/rate-limit/inject.js +7 -6
  87. package/template/gateway/js/app.js +42 -42
  88. package/template/gateway/js/inject.js +33 -33
  89. package/template/gateway/js/server.js +19 -19
  90. package/template/gateway/ts/app.ts +43 -43
  91. package/template/gateway/ts/inject.js +33 -33
  92. package/template/gateway/ts/server.ts +19 -19
  93. package/template/microservice/docker/docker-compose.yml +5 -5
  94. package/template/microservice/nodocker/pm2.config.js +3 -3
package/bin/cli.js CHANGED
@@ -1,964 +1,1294 @@
1
- #!/usr/bin/env node
2
- import fs from "fs";
3
- import path from "path";
4
- import { execSync } from "child_process";
5
- import { fileURLToPath } from "url";
6
- import pc from "picocolors";
7
- import { getProjectConfig } from "./lib/prompts.js";
8
- import prompts from "prompts";
9
- import { setupService } from "./lib/service-setup.js";
10
- import { generateReadme } from "./lib/readme-generator.js";
11
- // No TS->JS transform: templates contain language-specific folders (base/js, base/ts)
12
- import {
13
- generateDockerCompose,
14
- generatePm2Config,
15
- copyDockerfile,
16
- copyDockerignore,
17
- } from "./lib/microservice-config.js";
18
-
19
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
-
21
- // Get project configuration from user
22
- const config = await getProjectConfig();
23
- const {
24
- sanitizedName,
25
- target,
26
- isExistingProject,
27
- mode,
28
- isInMicroserviceProject,
29
- } = config;
30
-
31
- const baseRoot =
32
- config.language === "javascript"
33
- ? path.join(__dirname, "../template/base/js")
34
- : path.join(__dirname, "../template/base/ts");
35
- const base = baseRoot;
36
-
37
- // Determine which services to create
38
- const servicesToCreate = [];
39
- const servicesToSetup = [];
40
- if (isInMicroserviceProject) {
41
- const newServiceName = config.serviceName.replace(/\s+/g, "-");
42
- servicesToCreate.push(newServiceName);
43
- } else if (config.projectType === "microservice") {
44
- servicesToCreate.push("gateway");
45
- servicesToCreate.push("health-service");
46
- if (config.auth) {
47
- servicesToCreate.push("auth-service");
48
- }
49
- }
50
-
51
- // Validate and prepare project
52
- if (!isInMicroserviceProject && config.projectType === "microservice") {
53
- if (isExistingProject) {
54
- console.error(
55
- `\n${pc.red("āŒ Error:")} Project ${pc.bold(sanitizedName)} already exists!`,
56
- );
57
- process.exit(1);
58
- }
59
- console.log(
60
- `\n${pc.cyan("šŸ—ļø Creating microservices:")} ${pc.bold(servicesToCreate.join(", "))}...\n`,
61
- );
62
- } else if (!isInMicroserviceProject && config.projectType === "monolith") {
63
- if (isExistingProject) {
64
- console.error(
65
- `\n${pc.red("āŒ Error:")} Project ${pc.bold(sanitizedName)} already exists!`,
66
- );
67
- process.exit(1);
68
- }
69
- fs.cpSync(base, target, { recursive: true });
70
-
71
- // Remove db file and remove connectDB export/import if auth is not enabled
72
- if (!config.auth) {
73
- const ext = config.language === "javascript" ? "js" : "ts";
74
- const dbPath = path.join(target, `src/config/db.${ext}`);
75
- if (fs.existsSync(dbPath)) {
76
- fs.rmSync(dbPath);
77
- }
78
-
79
- // Update index.(js|ts) to not export or require connectDB
80
- const indexPath = path.join(target, `src/config/index.${ext}`);
81
- if (fs.existsSync(indexPath)) {
82
- let indexContent = fs.readFileSync(indexPath, "utf8");
83
- if (ext === "ts") {
84
- indexContent = indexContent.replace(
85
- 'export { connectDB } from "./db";\n',
86
- "",
87
- );
88
- // also remove any trailing references like `connectDB,` in exported objects
89
- indexContent = indexContent.replace(/connectDB,?/g, "");
90
- } else {
91
- indexContent = indexContent
92
- .replace('const { connectDB } = require("./db");', "")
93
- .replace(/connectDB,?/g, "");
94
- }
95
- fs.writeFileSync(indexPath, indexContent);
96
- }
97
- }
98
-
99
- // No TypeScript-to-JavaScript conversion — templates include language-specific variants
100
- } else if (isInMicroserviceProject) {
101
- console.log(
102
- `\n${pc.cyan("šŸ—ļø Adding service:")} ${pc.bold(servicesToCreate[0])}...\n`,
103
- );
104
- }
105
-
106
- // Process services
107
- if (isInMicroserviceProject || config.projectType === "microservice") {
108
- // Create shared folder for config and utils (only once)
109
- if (!isInMicroserviceProject) {
110
- const sharedDir = path.join(target, "shared");
111
- if (!fs.existsSync(sharedDir)) {
112
- console.log(
113
- `\n${pc.cyan("šŸ“¦ Creating shared folder for config and utils...")}`,
114
- );
115
- fs.mkdirSync(sharedDir, { recursive: true });
116
-
117
- // Copy config and utils from base template
118
- const baseConfigDir = path.join(base, "src", "config");
119
- const baseUtilsDir = path.join(base, "src", "utils");
120
- const sharedConfigDir = path.join(sharedDir, "config");
121
- const sharedUtilsDir = path.join(sharedDir, "utils");
122
-
123
- fs.cpSync(baseConfigDir, sharedConfigDir, { recursive: true });
124
- fs.cpSync(baseUtilsDir, sharedUtilsDir, { recursive: true });
125
-
126
- // Remove db files and strip connectDB exports/imports when auth is not enabled
127
- if (!config.auth) {
128
- for (const ext of ["ts", "js"]) {
129
- const sharedDbPath = path.join(sharedConfigDir, `db.${ext}`);
130
- if (fs.existsSync(sharedDbPath)) fs.rmSync(sharedDbPath);
131
-
132
- const sharedIndexPath = path.join(sharedConfigDir, `index.${ext}`);
133
- if (fs.existsSync(sharedIndexPath)) {
134
- let idx = fs.readFileSync(sharedIndexPath, "utf8");
135
- // Remove various export/import patterns referencing connectDB
136
- idx = idx.replace(
137
- /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
138
- "",
139
- );
140
- idx = idx.replace(
141
- /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
142
- "",
143
- );
144
- idx = idx.replace(
145
- /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
146
- "",
147
- );
148
- idx = idx.replace(/\bconnectDB,?\b/g, "");
149
- idx = idx.replace(/\n{3,}/g, "\n\n");
150
- fs.writeFileSync(sharedIndexPath, idx);
151
- }
152
- }
153
- }
154
- const ext = config.language === "javascript" ? "js" : "ts";
155
-
156
- // Update shared env.ts to include all service port environment variables
157
- const sharedEnvPath = path.join(sharedConfigDir, `env.${ext}`);
158
- if (fs.existsSync(sharedEnvPath)) {
159
- let envContent = fs.readFileSync(sharedEnvPath, "utf8");
160
- console.log(`\n${pc.cyan("šŸ”§ Updating shared env configuration...")}`);
161
-
162
- // Build port environment variables for all services
163
- const allServices = ["gateway", "health-service"];
164
- if (config.auth) allServices.push("auth-service");
165
-
166
- const portEnvVars = allServices
167
- .map((service) => {
168
- const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
169
- // Don't add ! for JavaScript projects - it will cause syntax errors
170
- const assertion = config.language === "javascript" ? "" : "!";
171
- return ` ${envVarName}: process.env.${envVarName}${assertion},`;
172
- })
173
- .join("\n");
174
-
175
- // Replace PORT with service-specific ports
176
- envContent = envContent.replace(
177
- " PORT: process.env.PORT!,",
178
- portEnvVars,
179
- );
180
-
181
- // Add ALLOWED_ORIGIN if CORS is selected
182
- if (config.features && config.features.includes("cors")) {
183
- const assertion = config.language === "javascript" ? "" : "!";
184
- envContent = envContent.replace(
185
- "/*__ALLOWED_ORIGIN__*/",
186
- `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
187
- );
188
- } else {
189
- envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
190
- }
191
-
192
- // Add MONGO_URI and JWT_SECRET if auth is enabled
193
- if (config.auth) {
194
- const assertion = config.language === "javascript" ? "" : "!";
195
- envContent = envContent.replace(
196
- "/*__MONGO_URI__*/",
197
- `MONGO_URI: process.env.MONGO_URI${assertion},`,
198
- );
199
- envContent = envContent.replace(
200
- "/*__JWT_SECRET__*/",
201
- `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
202
- );
203
- } else {
204
- envContent = envContent.replace("/*__MONGO_URI__*/", "");
205
- envContent = envContent.replace("/*__JWT_SECRET__*/", "");
206
- }
207
-
208
- fs.writeFileSync(sharedEnvPath, envContent);
209
- }
210
-
211
- // Update shared config/index to conditionally export connectDB
212
- const sharedConfigIndexPath = path.join(sharedConfigDir, `index.${ext}`);
213
- if (fs.existsSync(sharedConfigIndexPath)) {
214
- let indexContent = fs.readFileSync(sharedConfigIndexPath, "utf8");
215
- if (!config.auth) {
216
- if (ext === "ts") {
217
- indexContent = indexContent.replace(
218
- 'export { connectDB } from "./db";\n',
219
- "",
220
- );
221
- }
222
- indexContent = indexContent
223
- .replace('const { connectDB } = require("./db");', "")
224
- .replace("connectDB,", "");
225
- fs.writeFileSync(sharedConfigIndexPath, indexContent);
226
- }
227
- }
228
-
229
- // Update shared utils/logger to use shared config
230
- const sharedLoggerPath = path.join(sharedUtilsDir, `logger.${ext}`);
231
- if (fs.existsSync(sharedLoggerPath)) {
232
- console.log(
233
- `\n${pc.cyan("šŸ”§ Updating shared logger to use shared config...")}`,
234
- );
235
- let loggerContent = fs.readFileSync(sharedLoggerPath, "utf8");
236
- // Replace imports like: from '@/config'; or from "@/config" with relative import to shared config
237
- loggerContent = loggerContent.replace(
238
- /from\s+["']@\/config["'];?/g,
239
- "from '../config';",
240
- );
241
- fs.writeFileSync(sharedLoggerPath, loggerContent);
242
- }
243
-
244
- // Create shared package.json
245
- const sharedPackageJson = {
246
- name: "@shared/common",
247
- version: "1.0.0",
248
- type: "commonjs",
249
- exports: {
250
- "./config/*": "./config/*",
251
- "./utils/*": "./utils/*",
252
- },
253
- };
254
- fs.writeFileSync(
255
- path.join(sharedDir, "package.json"),
256
- JSON.stringify(sharedPackageJson, null, 2),
257
- );
258
- }
259
- }
260
-
261
- for (const serviceName of servicesToCreate) {
262
- const serviceRoot = path.join(target, "services", serviceName);
263
-
264
- // Check for exact or alternate folder conflicts (e.g., 'order' vs 'order-service')
265
- const altName = serviceName.endsWith("-service")
266
- ? serviceName.replace(/-service$/, "")
267
- : `${serviceName}-service`;
268
- const altPath = path.join(target, "services", altName);
269
- let conflictPath = null;
270
- if (fs.existsSync(serviceRoot)) conflictPath = serviceRoot;
271
- else if (fs.existsSync(altPath)) conflictPath = altPath;
272
-
273
- if (conflictPath) {
274
- const rel = path.relative(target, conflictPath);
275
- const resp = await prompts({
276
- type: "select",
277
- name: "action",
278
- message: `Service directory '${rel}' already exists and conflicts with requested '${serviceName}'. Choose action:`,
279
- choices: [
280
- { title: "Abort generation", value: "abort" },
281
- { title: `Skip creating '${serviceName}'`, value: "skip" },
282
- {
283
- title: `Overwrite '${rel}' with '${serviceName}'`,
284
- value: "overwrite",
285
- },
286
- ],
287
- initial: 0,
288
- });
289
-
290
- if (!resp.action || resp.action === "abort") {
291
- console.log(pc.red("Aborting."));
292
- process.exit(1);
293
- }
294
- if (resp.action === "skip") {
295
- console.log(pc.yellow(`Skipping ${serviceName}`));
296
- continue;
297
- }
298
- if (resp.action === "overwrite") {
299
- try {
300
- fs.rmSync(conflictPath, { recursive: true, force: true });
301
- } catch (e) {
302
- console.error(
303
- pc.red(`Failed to remove existing path: ${conflictPath}`),
304
- );
305
- process.exit(1);
306
- }
307
- }
308
- }
309
-
310
- console.log(`\nšŸ”Ø Setting up ${serviceName}...`);
311
- fs.cpSync(base, serviceRoot, { recursive: true });
312
- // track which services we actually created/overwrote so we can run setupService for them
313
- servicesToSetup.push(serviceName);
314
-
315
- // Remove .env and .env.example from microservices (environment variables come from docker-compose/pm2)
316
- const envPath = path.join(serviceRoot, ".env");
317
- const envExamplePath = path.join(serviceRoot, ".env.example");
318
- if (fs.existsSync(envPath)) fs.rmSync(envPath);
319
- if (fs.existsSync(envExamplePath)) fs.rmSync(envExamplePath);
320
-
321
- // Remove config and utils from service (they'll use shared) - except gateway handles it differently
322
- if (serviceName !== "gateway") {
323
- const serviceConfigDir = path.join(serviceRoot, "src", "config");
324
- const serviceUtilsDir = path.join(serviceRoot, "src", "utils");
325
- if (fs.existsSync(serviceConfigDir))
326
- fs.rmSync(serviceConfigDir, { recursive: true });
327
- if (fs.existsSync(serviceUtilsDir))
328
- fs.rmSync(serviceUtilsDir, { recursive: true });
329
- }
330
- }
331
-
332
- // Get all services first (needed for gateway routing)
333
- const servicesDir = path.join(target, "services");
334
- const existingServices = fs.existsSync(servicesDir)
335
- ? fs
336
- .readdirSync(servicesDir)
337
- .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
338
- : [];
339
- // Include services we're about to create so port computation and gateway routing
340
- // are aware of newly added services when setting up files.
341
- const allServices = Array.from(
342
- new Set([...existingServices, ...servicesToSetup]),
343
- );
344
-
345
- // Step 1: Setup all service files first (without installing dependencies)
346
- console.log(pc.cyan("\nāš™ļø Setting up service files...\n"));
347
- const serviceConfigs = [];
348
-
349
- for (const serviceName of servicesToSetup) {
350
- const serviceRoot = path.join(target, "services", serviceName);
351
- const shouldIncludeAuth = isInMicroserviceProject
352
- ? config.auth
353
- : serviceName === "auth-service";
354
- const result = await setupService(
355
- config,
356
- serviceName,
357
- serviceRoot,
358
- shouldIncludeAuth,
359
- allServices,
360
- true, // Skip install for now
361
- );
362
- serviceConfigs.push({
363
- serviceName,
364
- serviceRoot,
365
- deps: result.deps,
366
- devDeps: result.devDeps,
367
- });
368
- }
369
-
370
- // Remove per-service husky hooks and ensure a single root pre-commit hook
371
- try {
372
- const servicesDirPath = path.join(target, "services");
373
- const allServicesList = fs.existsSync(servicesDirPath)
374
- ? fs
375
- .readdirSync(servicesDirPath)
376
- .filter((f) =>
377
- fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
378
- )
379
- : [];
380
-
381
- // Remove `.husky` folders from each service
382
- for (const svc of allServicesList) {
383
- const svcHusky = path.join(servicesDirPath, svc, ".husky");
384
- if (fs.existsSync(svcHusky)) {
385
- fs.rmSync(svcHusky, { recursive: true, force: true });
386
- }
387
- }
388
-
389
- // Ensure root .husky/pre-commit exists at target
390
- const rootHuskyDir = path.join(target, ".husky");
391
- if (!fs.existsSync(rootHuskyDir))
392
- fs.mkdirSync(rootHuskyDir, { recursive: true });
393
- const preCommitPath = path.join(rootHuskyDir, "pre-commit");
394
- const preCommitContent =
395
- config.language === "typescript"
396
- ? 'set -e\n\necho "Checking format (prettier)..."\nnpm run check-format\n\necho "Running TypeScript type-check..."\nnpx tsc --noEmit\n\necho "Checking lint..."\nnpm run lint -- --max-warnings=0\n'
397
- : 'set -e\n\necho "Checking format (prettier)..."\nnpm run check-format\n\necho "Checking lint..."\nnpm run lint -- --max-warnings=0\n';
398
- fs.writeFileSync(preCommitPath, preCommitContent);
399
- } catch (err) {
400
- // Non-fatal; continue setup even if husky files couldn't be created/removed
401
- }
402
-
403
- // Step 2: Generate docker-compose/pm2 config and root files
404
- if (mode === "docker") {
405
- generateDockerCompose(target, allServices, config.sanitizedName);
406
- copyDockerfile(target, servicesToSetup);
407
- copyDockerignore(target, servicesToSetup);
408
- } else {
409
- generatePm2Config(target, allServices);
410
- }
411
-
412
- // Create root package.json for microservice monorepo if it doesn't exist
413
- const rootPackageJsonPath = path.join(target, "package.json");
414
- if (!fs.existsSync(rootPackageJsonPath)) {
415
- const rootPackageJson = {
416
- name: sanitizedName,
417
- version: config.version || "1.0.0",
418
- description: config.description || "",
419
- private: true,
420
- scripts: {
421
- dev:
422
- mode === "docker"
423
- ? "docker-compose up"
424
- : "npx pm2 start pm2.config.js && npx pm2 logs",
425
- stop: mode === "docker" ? "docker-compose down" : "npx pm2 kill",
426
- restart:
427
- mode === "docker"
428
- ? "docker-compose restart"
429
- : "npx pm2 restart all && npx pm2 logs",
430
- lint: 'eslint "services/**/*.{js,ts,tsx}" "shared/**/*.{js,ts,tsx}"',
431
- format:
432
- 'prettier --write "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
433
- "check-format":
434
- 'prettier --check "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
435
- prepare: "husky install",
436
- },
437
- devDependencies: {
438
- husky: "^9.1.7",
439
- prettier: "^3.7.4",
440
- "@typescript-eslint/eslint-plugin": "^8.50.1",
441
- "@typescript-eslint/parser": "^8.50.1",
442
- eslint: "^9.39.2",
443
- "eslint-config-prettier": "^10.1.8",
444
- },
445
- };
446
-
447
- // Add runtime dependencies for non-Docker (PM2) mode
448
- if (mode !== "docker") {
449
- rootPackageJson.dependencies = {
450
- dotenv: "^17.2.3",
451
- pm2: "^6.0.14",
452
- "ts-node": "^10.9.2",
453
- "tsconfig-paths": "^4.2.0",
454
- };
455
- }
456
- fs.writeFileSync(
457
- rootPackageJsonPath,
458
- JSON.stringify(rootPackageJson, null, 2) + "\n",
459
- );
460
- }
461
-
462
- // Ensure root lint/format config files exist (copy from template base if available), and remove any per-service copies
463
- try {
464
- const rootFiles = [".prettierrc", ".prettierignore", ".eslintrc.json"];
465
- for (const f of rootFiles) {
466
- const src = path.join(base, f);
467
- const dest = path.join(target, f);
468
- if (fs.existsSync(src)) {
469
- fs.copyFileSync(src, dest);
470
- } else if (!fs.existsSync(dest)) {
471
- // create minimal defaults
472
- if (f === ".prettierignore")
473
- fs.writeFileSync(dest, "node_modules\n" + "dist\n");
474
- else if (f === ".eslintrc.json")
475
- fs.writeFileSync(dest, JSON.stringify({ root: true }, null, 2));
476
- else fs.writeFileSync(dest, "{}");
477
- }
478
- }
479
-
480
- // Write eslint.config.js with recommended workspace config (overwrite)
481
- const eslintConfigPath = path.join(target, "eslint.config.js");
482
-
483
- // Build dynamic project list for TypeScript projects based on the services present
484
- const projectPaths = ["./tsconfig.json"];
485
- try {
486
- if (typeof allServices !== "undefined" && Array.isArray(allServices)) {
487
- for (const svc of allServices) {
488
- const svcTsPath = `./services/${svc}/tsconfig.json`;
489
- if (fs.existsSync(path.join(target, svcTsPath))) {
490
- projectPaths.push(svcTsPath);
491
- }
492
- }
493
- }
494
- } catch (e) {
495
- // non-fatal; fall back to default projectPaths containing only root tsconfig
496
- }
497
-
498
- const projectEntries = projectPaths
499
- .map((p) => ` "${p}",`)
500
- .join("\n");
501
-
502
- const eslintConfigContent = `const tsParser = require("@typescript-eslint/parser");\nconst tsPlugin = require("@typescript-eslint/eslint-plugin");\n\nmodule.exports = [\n // Files/paths to ignore (replaces .eslintignore usage in flat config)\n {\n ignores: ["node_modules/**", "dist/**"],\n },\n\n // TypeScript rules for source files\n {\n files: ["services/**/*.{js,ts,tsx}", "shared/**/*.{js,ts,tsx}"],\n languageOptions: {\n parser: tsParser,\n parserOptions: {\n project: [\n${projectEntries}\n ],\n tsconfigRootDir: __dirname,\n ecmaVersion: 2020,\n sourceType: "module",\n },\n },\n plugins: {\n "@typescript-eslint": tsPlugin,\n },\n rules: {\n // Disallow explicit 'any'\n "@typescript-eslint/no-explicit-any": "error",\n\n // You can add or tune more TypeScript rules here\n "@typescript-eslint/explicit-module-boundary-types": "off",\n },\n },\n];\n`;
503
- fs.writeFileSync(eslintConfigPath, eslintConfigContent);
504
-
505
- // Remove per-service copies if they exist (already removed in setupService, but double-check)
506
- const servicesDirPath = path.join(target, "services");
507
- if (fs.existsSync(servicesDirPath)) {
508
- const svcs = fs
509
- .readdirSync(servicesDirPath)
510
- .filter((f) =>
511
- fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
512
- );
513
- for (const svc of svcs) {
514
- for (const f of [...rootFiles, "eslint.config.js"]) {
515
- const p = path.join(servicesDirPath, svc, f);
516
- if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
517
- }
518
- }
519
- }
520
- } catch (err) {
521
- // non-fatal
522
- }
523
-
524
- // Step 3: Generate README and create root configuration files
525
- if (!isInMicroserviceProject) {
526
- console.log(`\n${pc.cyan("šŸ“ Generating README.md...")}\n`);
527
- const readmeContent = generateReadme(config);
528
- fs.writeFileSync(path.join(target, "README.md"), readmeContent);
529
-
530
- // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
531
- for (const service of allServices) {
532
- const gitignorePath = path.join(servicesDir, service, "gitignore");
533
- const dotGitignorePath = path.join(servicesDir, service, ".gitignore");
534
- if (fs.existsSync(gitignorePath)) {
535
- fs.renameSync(gitignorePath, dotGitignorePath);
536
- }
537
- }
538
-
539
- // Create root .gitignore for microservices
540
- const rootGitignoreContent = `.env\nnode_modules\n`;
541
- fs.writeFileSync(path.join(target, ".gitignore"), rootGitignoreContent);
542
-
543
- // Create root .env and .env.example for microservices
544
- let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
545
-
546
- // Add port configuration for each service
547
- allServices.forEach((service, index) => {
548
- const isGateway = service === "gateway";
549
- const port = isGateway
550
- ? 4000
551
- : 4001 +
552
- allServices.filter((s, i) => s !== "gateway" && i < index).length;
553
- const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
554
- const serviceName = service
555
- .split("-")
556
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
557
- .join(" ");
558
- rootENVContent += `# ${serviceName}\n${envVarName}=${port}\n\n`;
559
- });
560
-
561
- fs.writeFileSync(path.join(target, ".env"), rootENVContent);
562
- fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
563
-
564
- // Create root tsconfig.json for microservices workspace
565
- const rootTsConfigContent = {
566
- compilerOptions: {
567
- target: "ES2020",
568
- module: "CommonJS",
569
- lib: ["ES2020"],
570
- moduleResolution: "node",
571
- esModuleInterop: true,
572
- skipLibCheck: true,
573
- strict: true,
574
- baseUrl: ".",
575
- paths: {
576
- "@/*": ["./*"],
577
- },
578
- },
579
- include: [],
580
- exclude: ["node_modules", "dist"],
581
- references: allServices.map((service) => ({
582
- path: `./services/${service}`,
583
- })),
584
- };
585
- fs.writeFileSync(
586
- path.join(target, "tsconfig.json"),
587
- JSON.stringify(rootTsConfigContent, null, 2) + "\n",
588
- );
589
- }
590
-
591
- // If we're adding a service into an existing microservice project,
592
- // ensure shared config and gateway are updated to reference the new service.
593
- if (isInMicroserviceProject) {
594
- try {
595
- const sharedConfigDir = path.join(target, "shared", "config");
596
- const languageExt = config.language === "javascript" ? "js" : "ts";
597
- const sharedEnvPath = path.join(sharedConfigDir, `env.${languageExt}`);
598
-
599
- if (fs.existsSync(sharedEnvPath)) {
600
- let envContent = fs.readFileSync(sharedEnvPath, "utf8");
601
-
602
- // Build port environment variables for all services
603
- const portEnvVars = allServices
604
- .map((service) => {
605
- const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
606
- const assertion = config.language === "javascript" ? "" : "!";
607
- return ` ${envVarName}: process.env.${envVarName}${assertion},`;
608
- })
609
- .join("\n");
610
-
611
- // Remove any existing *_PORT lines to avoid duplication
612
- envContent = envContent.replace(
613
- /^[ \t]*[A-Z0-9_]+_PORT:\s*process\.env\.[A-Z0-9_]+!?\,?\s*$/gim,
614
- "",
615
- );
616
- // Normalize multiple consecutive blank lines
617
- envContent = envContent.replace(/\n{2,}/g, "\n\n");
618
-
619
- // Attempt several fallback strategies to inject port variables:
620
- // 1. Replace explicit placeholder if present in template
621
- // 2. Insert right after the first object opening brace (or replace placeholder)
622
- if (envContent.includes("/*__PORTS__*/")) {
623
- envContent = envContent.replace(
624
- "/*__PORTS__*/",
625
- "/*__PORTS__*/\n" + portEnvVars,
626
- );
627
- } else {
628
- // Fallback: find the opening brace of the exported ENV object and insert after it
629
- const braceIndex = envContent.indexOf("{");
630
- if (braceIndex !== -1) {
631
- const insertPos =
632
- envContent.indexOf("\n", braceIndex) + 1 || braceIndex + 1;
633
- // insert a stable placeholder comment followed by the ports block
634
- envContent =
635
- envContent.slice(0, insertPos) +
636
- " /*__PORTS__*/\n" +
637
- portEnvVars +
638
- "\n" +
639
- envContent.slice(insertPos);
640
- } else {
641
- // Final fallback: append a placeholder and the ports to the end
642
- envContent = envContent + "\n/*__PORTS__*/\n" + portEnvVars;
643
- }
644
- }
645
-
646
- fs.writeFileSync(sharedEnvPath, envContent);
647
- }
648
-
649
- // Re-generate gateway routes if gateway exists (so new service gets proxied)
650
- const gatewayRoot = path.join(target, "services", "gateway");
651
- if (fs.existsSync(gatewayRoot)) {
652
- // Re-run setupService for gateway to rewrite app/server/env files
653
- await setupService(
654
- config,
655
- "gateway",
656
- gatewayRoot,
657
- config.auth,
658
- allServices,
659
- true,
660
- );
661
- }
662
-
663
- // Update root .env and .env.example so newly added services have port entries
664
- try {
665
- const servicesDirPath = path.join(target, "services");
666
- const svcList = fs.existsSync(servicesDirPath)
667
- ? fs
668
- .readdirSync(servicesDirPath)
669
- .filter((f) =>
670
- fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
671
- )
672
- : allServices;
673
-
674
- // Update only .env.example: preserve runtime .env (don't overwrite user changes)
675
- try {
676
- const envExamplePath = path.join(target, ".env.example");
677
- const servicesPorts = svcList.map((service, index) => {
678
- const isGateway = service === "gateway";
679
- const port = isGateway
680
- ? 4000
681
- : 4001 +
682
- svcList.filter((s, i) => s !== "gateway" && i < index).length;
683
- const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
684
- const serviceNamePretty = service
685
- .split("-")
686
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
687
- .join(" ");
688
- return `# ${serviceNamePretty}\n${envVarName}=${port}\n`;
689
- });
690
-
691
- const portsBlock = servicesPorts.join("\n");
692
-
693
- let exampleContent = "";
694
- if (fs.existsSync(envExamplePath)) {
695
- exampleContent = fs.readFileSync(envExamplePath, "utf8");
696
-
697
- // Remove existing *_PORT lines and any immediate preceding single-line comment
698
- const lines = exampleContent.split(/\r?\n/);
699
- const filtered = [];
700
- for (let i = 0; i < lines.length; i++) {
701
- const line = lines[i];
702
- const next = lines[i + 1];
703
- if (next && /^[A-Z0-9_]+_PORT=/.test(next.trim())) {
704
- // skip this line if it's a comment immediately preceding a _PORT assignment
705
- i++; // skip next as well
706
- continue;
707
- }
708
- if (/^[A-Z0-9_]+_PORT=/.test(line.trim())) {
709
- // skip existing port assignment
710
- continue;
711
- }
712
- filtered.push(line);
713
- }
714
-
715
- exampleContent = filtered.join("\n");
716
- } else {
717
- // create minimal header if example file doesn't exist
718
- exampleContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
719
- }
720
-
721
- // Ensure NODE_ENV line exists and insert portsBlock after it
722
- const nodeEnvRegex = /^NODE_ENV=.*$/m;
723
- if (nodeEnvRegex.test(exampleContent)) {
724
- exampleContent = exampleContent.replace(
725
- nodeEnvRegex,
726
- (m) => `${m}\n\n${portsBlock}\n`,
727
- );
728
- } else {
729
- // Prepend header and ports
730
- exampleContent =
731
- `# Environment Configuration\nNODE_ENV=development\n\n${portsBlock}\n` +
732
- exampleContent;
733
- }
734
-
735
- fs.writeFileSync(envExamplePath, exampleContent);
736
- // Inform the user about the new ports and remind them to update their runtime .env if needed
737
- try {
738
- console.log(
739
- pc.cyan("\nšŸ”§ Updated .env.example with service port entries:\n"),
740
- );
741
- console.log(pc.green(portsBlock));
742
- console.log(
743
- pc.dim(
744
- "If you keep a runtime .env with custom overrides, do NOT overwrite it.\nPlease copy any new *_PORT entries from .env.example into .env as appropriate.",
745
- ),
746
- );
747
- } catch (e) {
748
- // non-fatal if logging fails
749
- }
750
- } catch (e) {
751
- // non-fatal
752
- }
753
- } catch (e) {
754
- // non-fatal
755
- }
756
- } catch (e) {
757
- // non-fatal; continue even if updating shared/gateway fails
758
- }
759
- }
760
-
761
- // Step 5: Install dependencies for all services
762
-
763
- console.log(pc.cyan("\nšŸ“¦ Installing dependencies for all services...\n"));
764
- let allInstallsSucceeded = true;
765
-
766
- for (const { serviceName, serviceRoot, deps, devDeps } of serviceConfigs) {
767
- console.log(
768
- pc.cyan(`\nšŸ“¦ Installing dependencies for ${serviceName}...\n`),
769
- );
770
-
771
- try {
772
- if (deps.length) {
773
- execSync(`npm install ${deps.join(" ")}`, {
774
- cwd: serviceRoot,
775
- stdio: "inherit",
776
- });
777
- }
778
- if (devDeps.length) {
779
- execSync(`npm install -D ${devDeps.join(" ")}`, {
780
- cwd: serviceRoot,
781
- stdio: "inherit",
782
- });
783
- }
784
- execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
785
- } catch (error) {
786
- allInstallsSucceeded = false;
787
- console.error(
788
- pc.red(`\nāŒ Failed to install dependencies for ${serviceName}`),
789
- );
790
- console.error(pc.dim(`\nYou can install them later by running:`));
791
- console.error(pc.cyan(` cd services/${serviceName} && npm install\n`));
792
- }
793
- }
794
-
795
- // Store for later use
796
- config.allInstallsSucceeded = allInstallsSucceeded;
797
- } else {
798
- const result = await setupService(config, null, target, true);
799
- config.installSucceeded = result.installSucceeded;
800
-
801
- // Safety net: ensure eslint.config.js exists in generated monolith projects
802
- const templateEslintConfig = path.join(base, "eslint.config.js");
803
- const generatedEslintConfig = path.join(target, "eslint.config.js");
804
- if (
805
- config.projectType === "monolith" &&
806
- fs.existsSync(templateEslintConfig) &&
807
- !fs.existsSync(generatedEslintConfig)
808
- ) {
809
- fs.copyFileSync(templateEslintConfig, generatedEslintConfig);
810
- }
811
- }
812
-
813
- // Generate README.md for monolith (microservices already done above)
814
- if (!isInMicroserviceProject && config.projectType === "monolith") {
815
- console.log(`\n${pc.cyan("šŸ“ Generating README.md...")}\n`);
816
- const readmeContent = generateReadme(config);
817
- fs.writeFileSync(path.join(target, "README.md"), readmeContent);
818
-
819
- // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
820
- const gitignorePath = path.join(target, "gitignore");
821
- const dotGitignorePath = path.join(target, ".gitignore");
822
- if (fs.existsSync(gitignorePath)) {
823
- fs.renameSync(gitignorePath, dotGitignorePath);
824
- }
825
-
826
- // Generate .env from .env.example for monolith only
827
- console.log(`${pc.cyan("šŸ“„ Setting up environment files...")}\n`);
828
- try {
829
- const rootEnvExamplePath = path.join(target, ".env.example");
830
- const rootEnvPath = path.join(target, ".env");
831
- if (fs.existsSync(rootEnvExamplePath) && !fs.existsSync(rootEnvPath)) {
832
- fs.copyFileSync(rootEnvExamplePath, rootEnvPath);
833
- }
834
- } catch (err) {
835
- // Non-fatal; proceed even if we fail to write env files
836
- }
837
- }
838
-
839
- // Initialize git and Husky
840
- if (!isInMicroserviceProject) {
841
- execSync("git init", { cwd: target, stdio: "inherit" });
842
-
843
- // Install husky and other devDeps and setup at root level
844
- if (config.projectType === "microservice") {
845
- console.log("\nšŸ“¦ Installing dependencies at root level...\n");
846
- if (config.allInstallsSucceeded) {
847
- try {
848
- execSync("npm install", { cwd: target, stdio: "inherit" });
849
- console.log("\nšŸ”§ Setting up Husky...\n");
850
- execSync("npm run prepare", { cwd: target, stdio: "inherit" });
851
- } catch (error) {
852
- console.log("\nāš ļø Husky setup failed\n");
853
- }
854
- // Run format after successful install
855
- console.log(pc.cyan("\nšŸŽØ Formatting code...\n"));
856
- try {
857
- execSync("npm run format", { cwd: target, stdio: "inherit" });
858
- } catch (formatError) {
859
- console.warn(
860
- pc.yellow(
861
- "āš ļø Warning: Code formatting failed. You can run it manually later with: npm run format\n",
862
- ),
863
- );
864
- }
865
- } else {
866
- console.log(
867
- "\nāš ļø Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n",
868
- );
869
- }
870
- } else if (config.projectType === "monolith") {
871
- // Only setup Husky if installation succeeded
872
- if (config.installSucceeded) {
873
- console.log(`\n${pc.cyan("šŸ”§ Setting up Husky...")}\n`);
874
- try {
875
- execSync("npm run prepare", { cwd: target, stdio: "inherit" });
876
- } catch (error) {
877
- console.log(
878
- `\n${pc.yellow("āš ļø Husky setup failed")} ${pc.dim("(run 'npm run prepare' manually after fixing dependencies)")}\n`,
879
- );
880
- }
881
- } else {
882
- console.log(
883
- `\n${pc.yellow("āš ļø Husky setup skipped")} ${pc.dim("(run 'npm install && npm run prepare' to set up git hooks)")}\n`,
884
- );
885
- }
886
- }
887
- }
888
-
889
- // Success messages
890
- const servicesDir = path.join(target, "services");
891
- const allServices = fs.existsSync(servicesDir)
892
- ? fs
893
- .readdirSync(servicesDir)
894
- .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
895
- : servicesToCreate;
896
- // Update root README when adding services to an existing microservice project
897
- if (isInMicroserviceProject) {
898
- try {
899
- const readmeContent = generateReadme({ ...config, allServices });
900
- fs.writeFileSync(path.join(target, "README.md"), readmeContent);
901
- console.log(`\n${pc.cyan("šŸ“ Updated README.md with new services")}`);
902
- } catch (e) {
903
- // non-fatal
904
- }
905
- }
906
-
907
- if (isInMicroserviceProject) {
908
- if (servicesToSetup.length > 0) {
909
- console.log(
910
- `\n${pc.green("āœ… Service")} ${pc.bold(servicesToSetup[0])} ${pc.green("added successfully!")}`,
911
- );
912
- } else {
913
- console.log(pc.yellow("\nāš ļø No new service was created (skipped by your selection)."));
914
- }
915
- console.log(`\n${pc.cyan("šŸ“¦ All services:")} ${allServices.join(", ")}`);
916
- console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
917
- console.log(` ${pc.dim("1.")} Start services: ${pc.bold("npm run dev")}`);
918
- } else if (config.projectType === "microservice") {
919
- console.log(`\n${pc.green("āœ… Microservice Backend created successfully!")}`);
920
- console.log(
921
- `\n${pc.cyan("šŸ“¦ Created services:")} ${servicesToCreate.join(", ")}`,
922
- );
923
- console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
924
- console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
925
- console.log(` ${pc.dim("2.")} Start services: ${pc.bold("npm run dev")}`);
926
- } else {
927
- console.log(`\n${pc.green("āœ… Monolith Backend created successfully!")}`);
928
- console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
929
- console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
930
- console.log(` ${pc.dim("2.")} npm run dev`);
931
- }
932
- // Post-processing: ensure shared config does not export/connect to DB when auth is disabled
933
- try {
934
- if (!config.auth) {
935
- const sharedConfigDir = path.join(target, "shared", "config");
936
- if (fs.existsSync(sharedConfigDir)) {
937
- for (const ext of ["ts", "js"]) {
938
- const idxPath = path.join(sharedConfigDir, `index.${ext}`);
939
- if (!fs.existsSync(idxPath)) continue;
940
- let idxContent = fs.readFileSync(idxPath, "utf8");
941
- // Remove various connectDB export/import patterns
942
- idxContent = idxContent.replace(
943
- /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
944
- "",
945
- );
946
- idxContent = idxContent.replace(
947
- /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
948
- "",
949
- );
950
- idxContent = idxContent.replace(
951
- /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
952
- "",
953
- );
954
- // Remove any remaining connectDB identifiers (commas/newlines)
955
- idxContent = idxContent.replace(/connectDB,?/g, "");
956
- // Normalize multiple blank lines
957
- idxContent = idxContent.replace(/\n{3,}/g, "\n\n");
958
- fs.writeFileSync(idxPath, idxContent);
959
- }
960
- }
961
- }
962
- } catch (e) {
963
- // non-fatal
964
- }
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ import { fileURLToPath } from "url";
6
+ import pc from "picocolors";
7
+ import { getProjectConfig } from "./lib/prompts.js";
8
+ import prompts from "prompts";
9
+ import { setupService } from "./lib/service-setup.js";
10
+ import { generateReadme } from "./lib/readme-generator.js";
11
+ // No TS->JS transform: templates contain language-specific folders (base/js, base/ts)
12
+ import {
13
+ generateDockerCompose,
14
+ generatePm2Config,
15
+ copyDockerfile,
16
+ copyDockerignore,
17
+ } from "./lib/microservice-config.js";
18
+
19
+ function writeStarterWorkflow(target, config) {
20
+ if (config.isInMicroserviceProject || !config.cicd) {
21
+ return;
22
+ }
23
+
24
+ const workflowsDir = path.join(target, ".github", "workflows");
25
+ fs.mkdirSync(workflowsDir, { recursive: true });
26
+
27
+ const workflowName = config.sanitizedName
28
+ ? `${config.sanitizedName} CI/CD`
29
+ : "Build CI/CD";
30
+
31
+ const workflowContent = `name: ${workflowName}
32
+
33
+ on:
34
+ workflow_dispatch:
35
+ push:
36
+ branches:
37
+ - main
38
+ pull_request:
39
+ branches:
40
+ - main
41
+ - dev
42
+
43
+ jobs:
44
+ # ------------------------
45
+ # 1ļøāƒ£ BUILD & CHECK JOB
46
+ # ------------------------
47
+ build:
48
+ name: Build & Check
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - name: Checkout code
52
+ uses: actions/checkout@v4
53
+
54
+ - name: Setup Node.js
55
+ uses: actions/setup-node@v4
56
+ with:
57
+ node-version: 20.x
58
+
59
+ - name: Install dependencies
60
+ run: npm ci
61
+
62
+ - name: Check code format
63
+ run: npm run check-format
64
+
65
+ - name: Run linter
66
+ run: npm run lint
67
+
68
+ - name: Run build
69
+ run: npm run build
70
+ `;
71
+
72
+ fs.writeFileSync(path.join(workflowsDir, "ci-cd.yml"), workflowContent);
73
+ }
74
+
75
+ function writePullRequestTemplate(target, config) {
76
+ if (config.isInMicroserviceProject || !config.cicd) {
77
+ return;
78
+ }
79
+
80
+ const githubDir = path.join(target, ".github");
81
+ fs.mkdirSync(githubDir, { recursive: true });
82
+
83
+ const prTemplateContent = `# Description
84
+
85
+ <!--- Describe your changes in detail -->
86
+
87
+ This PR ...
88
+
89
+ # Changes Proposed
90
+
91
+ ## What were you told to do?
92
+
93
+ ## What did you do?
94
+
95
+ ## Types of changes
96
+
97
+ <!--- What types of changes does your code introduce? Put an x in all the boxes that apply: -->
98
+
99
+ - [ ] Bug fix (non-breaking change which fixes an issue)
100
+ - [ ] New feature (non-breaking change which adds functionality)
101
+ - [ ] Breaking change (fix or feature that would cause existing functionality to change)
102
+ - [ ] Chore (changes that do not relate to a fix or feature and don't modify src or test files)
103
+
104
+ # Check List
105
+
106
+ <!--- Go over all the following points, and put an x in all the boxes that apply. -->
107
+ <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
108
+
109
+ - [x] My code follows the code style of this project.
110
+ - [x] This PR does not contain plagiarized content.
111
+ - [x] The title and description of the PR are clear and explain the approach.
112
+ - [x] I am making a pull request against the dev branch (left side).
113
+ - [x] My commit message style matches our requested structure.
114
+ - [x] My code additions will not fail code linting checks or unit tests.
115
+ - [x] I am only making changes to files I was requested to.
116
+
117
+ ---
118
+
119
+ # Images
120
+
121
+ <!-- Add Screenshots of: -->
122
+
123
+ - Linting check (run npm lint)
124
+ `;
125
+
126
+ fs.writeFileSync(
127
+ path.join(githubDir, "pull_request_template.md"),
128
+ prTemplateContent,
129
+ );
130
+ }
131
+
132
+ function writeContributingGuide(target, config) {
133
+ if (config.isInMicroserviceProject || !config.cicd) {
134
+ return;
135
+ }
136
+
137
+ const contributingContent = `# Contributing
138
+
139
+ Thank you for your interest in contributing to **${config.sanitizedName}**! We appreciate your help in making this project better.
140
+
141
+ ## Getting Started
142
+
143
+ 1. **Fork the repository** — Click the fork button on GitHub to create your own copy.
144
+ 2. **Clone your fork** — Clone your forked repository to your local machine:
145
+ \`\`\`bash
146
+ git clone https://github.com/YOUR_USERNAME/${config.sanitizedName}.git
147
+ cd ${config.sanitizedName}
148
+ \`\`\`
149
+ 3. **Add upstream remote** — Keep a reference to the original repository:
150
+ \`\`\`bash
151
+ git remote add upstream https://github.com/ORIGINAL_OWNER/${config.sanitizedName}.git
152
+ \`\`\`
153
+
154
+ ## Development Setup
155
+
156
+ 1. **Install dependencies**:
157
+ \`\`\`bash
158
+ npm install
159
+ \`\`\`
160
+
161
+ 2. **Create a feature branch**:
162
+ \`\`\`bash
163
+ git checkout -b feature/your-feature-name
164
+ \`\`\`
165
+
166
+ 3. **Make your changes** and commit with clear, descriptive messages:
167
+ \`\`\`bash
168
+ git add .
169
+ git commit -m "feat: add your feature description"
170
+ \`\`\`
171
+
172
+ ## Code Quality
173
+
174
+ Before submitting a pull request, ensure your code meets our standards:
175
+
176
+ ### Linting
177
+
178
+ Run the linter to check for code style issues:
179
+ \`\`\`bash
180
+ npm run lint
181
+ \`\`\`
182
+
183
+ Fix any issues automatically (when possible):
184
+ \`\`\`bash
185
+ npm run lint -- --fix
186
+ \`\`\`
187
+
188
+ ### Formatting
189
+
190
+ We use Prettier for code formatting. Format your code before committing:
191
+ \`\`\`bash
192
+ npm run format
193
+ \`\`\`
194
+
195
+ ### Building
196
+
197
+ Ensure your changes build successfully:
198
+ \`\`\`bash
199
+ npm run build
200
+ \`\`\`
201
+
202
+ ### Testing
203
+
204
+ Run tests to verify your changes:
205
+ \`\`\`bash
206
+ npm run test
207
+ \`\`\`
208
+
209
+ ## Commit Message Guidelines
210
+
211
+ Follow these conventions for commit messages:
212
+
213
+ - \`feat:\` for new features
214
+ - \`fix:\` for bug fixes
215
+ - \`docs:\` for documentation changes
216
+ - \`style:\` for code style changes (formatting, missing semicolons, etc.)
217
+ - \`refactor:\` for code refactoring
218
+ - \`test:\` for testing changes
219
+ - \`chore:\` for dependency updates, build changes, etc.
220
+
221
+ **Example:**
222
+ \`\`\`
223
+ feat: add user authentication endpoint
224
+ fix: resolve null pointer exception in service
225
+ docs: update API documentation
226
+ \`\`\`
227
+
228
+ ## Pull Request Process
229
+
230
+ 1. **Sync your branch** with the main repository:
231
+ \`\`\`bash
232
+ git fetch upstream
233
+ git rebase upstream/main
234
+ \`\`\`
235
+
236
+ 2. **Push your changes**:
237
+ \`\`\`bash
238
+ git push origin feature/your-feature-name
239
+ \`\`\`
240
+
241
+ 3. **Create a Pull Request** on GitHub with:
242
+ - A clear, concise title describing your changes
243
+ - A detailed description using our PR template
244
+ - Reference to any related issues (e.g., "Closes #123")
245
+
246
+ 4. **Address feedback** — Respond to code review comments and make requested changes
247
+
248
+ 5. **Merge** — Once approved and all checks pass, your PR will be merged
249
+
250
+ ## Code Style
251
+
252
+ - Use **consistent indentation** (2 spaces)
253
+ - Follow **ESLint** and **Prettier** configurations
254
+ - Write **descriptive variable and function names**
255
+ - Add **comments** for complex logic
256
+ - Keep **functions small** and focused
257
+
258
+ ## Reporting Issues
259
+
260
+ Found a bug? Please open an issue on GitHub with:
261
+ - A clear, descriptive title
262
+ - Steps to reproduce the issue
263
+ - Expected vs. actual behavior
264
+ - Your environment (OS, Node version, etc.)
265
+ - Any relevant error messages or logs
266
+
267
+ ## Questions or Need Help?
268
+
269
+ - Check the [README.md](../README.md) for project overview
270
+ - Review existing issues and documentation
271
+ - Open a discussion if you have questions
272
+
273
+ ## Code of Conduct
274
+
275
+ Please be respectful and welcoming to all contributors. We are committed to providing a harassment-free environment.
276
+
277
+ ---
278
+
279
+ Thank you for contributing! Your efforts help make this project better for everyone. šŸ™
280
+ `;
281
+
282
+ fs.writeFileSync(path.join(target, "CONTRIBUTING.md"), contributingContent);
283
+ }
284
+
285
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
286
+
287
+ // Get project configuration from user
288
+ const config = await getProjectConfig();
289
+ const {
290
+ sanitizedName,
291
+ target,
292
+ isExistingProject,
293
+ mode,
294
+ isInMicroserviceProject,
295
+ } = config;
296
+
297
+ const baseRoot =
298
+ config.language === "javascript"
299
+ ? path.join(__dirname, "../template/base/js")
300
+ : path.join(__dirname, "../template/base/ts");
301
+ const base = baseRoot;
302
+
303
+ // Determine which services to create
304
+ const servicesToCreate = [];
305
+ const servicesToSetup = [];
306
+ if (isInMicroserviceProject) {
307
+ const newServiceName = config.serviceName.replace(/\s+/g, "-");
308
+ servicesToCreate.push(newServiceName);
309
+ } else if (config.projectType === "microservice") {
310
+ servicesToCreate.push("gateway");
311
+ servicesToCreate.push("health-service");
312
+ if (config.auth) {
313
+ servicesToCreate.push("auth-service");
314
+ }
315
+ }
316
+
317
+ // Validate and prepare project
318
+ if (!isInMicroserviceProject && config.projectType === "microservice") {
319
+ if (isExistingProject) {
320
+ console.error(
321
+ `\n${pc.red("āŒ Error:")} Project ${pc.bold(
322
+ sanitizedName,
323
+ )} already exists!`,
324
+ );
325
+ process.exit(1);
326
+ }
327
+ console.log(
328
+ `\n${pc.cyan("šŸ—ļø Creating microservices:")} ${pc.bold(
329
+ servicesToCreate.join(", "),
330
+ )}...\n`,
331
+ );
332
+ } else if (!isInMicroserviceProject && config.projectType === "monolith") {
333
+ if (isExistingProject) {
334
+ console.error(
335
+ `\n${pc.red("āŒ Error:")} Project ${pc.bold(
336
+ sanitizedName,
337
+ )} already exists!`,
338
+ );
339
+ process.exit(1);
340
+ }
341
+ fs.cpSync(base, target, { recursive: true });
342
+
343
+ // Remove db file and remove connectDB export/import if auth is not enabled
344
+ if (!config.auth) {
345
+ const ext = config.language === "javascript" ? "js" : "ts";
346
+ const dbPath = path.join(target, `src/config/db.${ext}`);
347
+ if (fs.existsSync(dbPath)) {
348
+ fs.rmSync(dbPath);
349
+ }
350
+
351
+ // Update index.(js|ts) to not export or require connectDB
352
+ const indexPath = path.join(target, `src/config/index.${ext}`);
353
+ if (fs.existsSync(indexPath)) {
354
+ let indexContent = fs.readFileSync(indexPath, "utf8");
355
+ if (ext === "ts") {
356
+ indexContent = indexContent.replace(
357
+ 'export { connectDB } from "./db";\n',
358
+ "",
359
+ );
360
+ // also remove any trailing references like `connectDB,` in exported objects
361
+ indexContent = indexContent.replace(/connectDB,?/g, "");
362
+ } else {
363
+ indexContent = indexContent
364
+ .replace('const { connectDB } = require("./db");', "")
365
+ .replace(/connectDB,?/g, "");
366
+ }
367
+ fs.writeFileSync(indexPath, indexContent);
368
+ }
369
+ }
370
+
371
+ // No TypeScript-to-JavaScript conversion — templates include language-specific variants
372
+ } else if (isInMicroserviceProject) {
373
+ console.log(
374
+ `\n${pc.cyan("šŸ—ļø Adding service:")} ${pc.bold(servicesToCreate[0])}...\n`,
375
+ );
376
+ }
377
+
378
+ // Process services
379
+ if (isInMicroserviceProject || config.projectType === "microservice") {
380
+ // Create shared folder for config and utils (only once)
381
+ if (!isInMicroserviceProject) {
382
+ const sharedDir = path.join(target, "shared");
383
+ if (!fs.existsSync(sharedDir)) {
384
+ console.log(
385
+ `\n${pc.cyan("šŸ“¦ Creating shared folder for config and utils...")}`,
386
+ );
387
+ fs.mkdirSync(sharedDir, { recursive: true });
388
+
389
+ // Copy config and utils from base template
390
+ const baseConfigDir = path.join(base, "src", "config");
391
+ const baseUtilsDir = path.join(base, "src", "utils");
392
+ const sharedConfigDir = path.join(sharedDir, "config");
393
+ const sharedUtilsDir = path.join(sharedDir, "utils");
394
+
395
+ fs.cpSync(baseConfigDir, sharedConfigDir, { recursive: true });
396
+ fs.cpSync(baseUtilsDir, sharedUtilsDir, { recursive: true });
397
+
398
+ // Remove db files and strip connectDB exports/imports when auth is not enabled
399
+ if (!config.auth) {
400
+ for (const ext of ["ts", "js"]) {
401
+ const sharedDbPath = path.join(sharedConfigDir, `db.${ext}`);
402
+ if (fs.existsSync(sharedDbPath)) fs.rmSync(sharedDbPath);
403
+
404
+ const sharedIndexPath = path.join(sharedConfigDir, `index.${ext}`);
405
+ if (fs.existsSync(sharedIndexPath)) {
406
+ let idx = fs.readFileSync(sharedIndexPath, "utf8");
407
+ // Remove various export/import patterns referencing connectDB
408
+ idx = idx.replace(
409
+ /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
410
+ "",
411
+ );
412
+ idx = idx.replace(
413
+ /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
414
+ "",
415
+ );
416
+ idx = idx.replace(
417
+ /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
418
+ "",
419
+ );
420
+ idx = idx.replace(/\bconnectDB,?\b/g, "");
421
+ idx = idx.replace(/\n{3,}/g, "\n\n");
422
+ fs.writeFileSync(sharedIndexPath, idx);
423
+ }
424
+ }
425
+ }
426
+ const ext = config.language === "javascript" ? "js" : "ts";
427
+
428
+ // Update shared env.ts to include all service port environment variables
429
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${ext}`);
430
+ if (fs.existsSync(sharedEnvPath)) {
431
+ let envContent = fs.readFileSync(sharedEnvPath, "utf8");
432
+ console.log(`\n${pc.cyan("šŸ”§ Updating shared env configuration...")}`);
433
+
434
+ // Build port environment variables for all services
435
+ const allServices = ["gateway", "health-service"];
436
+ if (config.auth) allServices.push("auth-service");
437
+
438
+ const portEnvVars = allServices
439
+ .map((service) => {
440
+ const envVarName = `${service
441
+ .toUpperCase()
442
+ .replace(/-/g, "_")}_PORT`;
443
+ // Don't add ! for JavaScript projects - it will cause syntax errors
444
+ const assertion = config.language === "javascript" ? "" : "!";
445
+ return ` ${envVarName}: process.env.${envVarName}${assertion},`;
446
+ })
447
+ .join("\n");
448
+
449
+ // Replace PORT with service-specific ports
450
+ envContent = envContent.replace(
451
+ " PORT: process.env.PORT!,",
452
+ portEnvVars,
453
+ );
454
+
455
+ // Add ALLOWED_ORIGIN if CORS is selected
456
+ if (config.features && config.features.includes("cors")) {
457
+ const assertion = config.language === "javascript" ? "" : "!";
458
+ envContent = envContent.replace(
459
+ "/*__ALLOWED_ORIGIN__*/",
460
+ `ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN${assertion},`,
461
+ );
462
+ } else {
463
+ envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
464
+ }
465
+
466
+ // Add MONGO_URI and JWT_SECRET if auth is enabled
467
+ if (config.auth) {
468
+ const assertion = config.language === "javascript" ? "" : "!";
469
+ envContent = envContent.replace(
470
+ "/*__MONGO_URI__*/",
471
+ `MONGO_URI: process.env.MONGO_URI${assertion},`,
472
+ );
473
+ envContent = envContent.replace(
474
+ "/*__JWT_SECRET__*/",
475
+ `JWT_SECRET: process.env.JWT_SECRET${assertion},`,
476
+ );
477
+ } else {
478
+ envContent = envContent.replace("/*__MONGO_URI__*/", "");
479
+ envContent = envContent.replace("/*__JWT_SECRET__*/", "");
480
+ }
481
+
482
+ fs.writeFileSync(sharedEnvPath, envContent);
483
+ }
484
+
485
+ // Update shared config/index to conditionally export connectDB
486
+ const sharedConfigIndexPath = path.join(sharedConfigDir, `index.${ext}`);
487
+ if (fs.existsSync(sharedConfigIndexPath)) {
488
+ let indexContent = fs.readFileSync(sharedConfigIndexPath, "utf8");
489
+ if (!config.auth) {
490
+ if (ext === "ts") {
491
+ indexContent = indexContent.replace(
492
+ 'export { connectDB } from "./db";\n',
493
+ "",
494
+ );
495
+ }
496
+ indexContent = indexContent
497
+ .replace('const { connectDB } = require("./db");', "")
498
+ .replace("connectDB,", "");
499
+ fs.writeFileSync(sharedConfigIndexPath, indexContent);
500
+ }
501
+ }
502
+
503
+ // Update shared utils/logger to use shared config
504
+ const sharedLoggerPath = path.join(sharedUtilsDir, `logger.${ext}`);
505
+ if (fs.existsSync(sharedLoggerPath)) {
506
+ console.log(
507
+ `\n${pc.cyan("šŸ”§ Updating shared logger to use shared config...")}`,
508
+ );
509
+ let loggerContent = fs.readFileSync(sharedLoggerPath, "utf8");
510
+ // Replace imports like: from '@/config'; or from "@/config" with relative import to shared config
511
+ loggerContent = loggerContent.replace(
512
+ /from\s+["']@\/config["'];?/g,
513
+ "from '../config';",
514
+ );
515
+ fs.writeFileSync(sharedLoggerPath, loggerContent);
516
+ }
517
+
518
+ // Create shared package.json
519
+ const sharedPackageJson = {
520
+ name: "@shared/common",
521
+ version: "1.0.0",
522
+ type: "commonjs",
523
+ exports: {
524
+ "./config/*": "./config/*",
525
+ "./utils/*": "./utils/*",
526
+ },
527
+ };
528
+ fs.writeFileSync(
529
+ path.join(sharedDir, "package.json"),
530
+ JSON.stringify(sharedPackageJson, null, 2),
531
+ );
532
+ }
533
+ }
534
+
535
+ for (const serviceName of servicesToCreate) {
536
+ const serviceRoot = path.join(target, "services", serviceName);
537
+
538
+ // Check for exact or alternate folder conflicts (e.g., 'order' vs 'order-service')
539
+ const altName = serviceName.endsWith("-service")
540
+ ? serviceName.replace(/-service$/, "")
541
+ : `${serviceName}-service`;
542
+ const altPath = path.join(target, "services", altName);
543
+ let conflictPath = null;
544
+ if (fs.existsSync(serviceRoot)) conflictPath = serviceRoot;
545
+ else if (fs.existsSync(altPath)) conflictPath = altPath;
546
+
547
+ if (conflictPath) {
548
+ const rel = path.relative(target, conflictPath);
549
+ const resp = await prompts({
550
+ type: "select",
551
+ name: "action",
552
+ message: `Service directory '${rel}' already exists and conflicts with requested '${serviceName}'. Choose action:`,
553
+ choices: [
554
+ { title: "Abort generation", value: "abort" },
555
+ { title: `Skip creating '${serviceName}'`, value: "skip" },
556
+ {
557
+ title: `Overwrite '${rel}' with '${serviceName}'`,
558
+ value: "overwrite",
559
+ },
560
+ ],
561
+ initial: 0,
562
+ });
563
+
564
+ if (!resp.action || resp.action === "abort") {
565
+ console.log(pc.red("Aborting."));
566
+ process.exit(1);
567
+ }
568
+ if (resp.action === "skip") {
569
+ console.log(pc.yellow(`Skipping ${serviceName}`));
570
+ continue;
571
+ }
572
+ if (resp.action === "overwrite") {
573
+ try {
574
+ fs.rmSync(conflictPath, { recursive: true, force: true });
575
+ } catch (e) {
576
+ console.error(
577
+ pc.red(`Failed to remove existing path: ${conflictPath}`),
578
+ );
579
+ process.exit(1);
580
+ }
581
+ }
582
+ }
583
+
584
+ console.log(`\nšŸ”Ø Setting up ${serviceName}...`);
585
+ fs.cpSync(base, serviceRoot, { recursive: true });
586
+ // track which services we actually created/overwrote so we can run setupService for them
587
+ servicesToSetup.push(serviceName);
588
+
589
+ // Remove .env and .env.example from microservices (environment variables come from docker-compose/pm2)
590
+ const envPath = path.join(serviceRoot, ".env");
591
+ const envExamplePath = path.join(serviceRoot, ".env.example");
592
+ if (fs.existsSync(envPath)) fs.rmSync(envPath);
593
+ if (fs.existsSync(envExamplePath)) fs.rmSync(envExamplePath);
594
+
595
+ // Remove config and utils from service (they'll use shared) - except gateway handles it differently
596
+ if (serviceName !== "gateway") {
597
+ const serviceConfigDir = path.join(serviceRoot, "src", "config");
598
+ const serviceUtilsDir = path.join(serviceRoot, "src", "utils");
599
+ if (fs.existsSync(serviceConfigDir))
600
+ fs.rmSync(serviceConfigDir, { recursive: true });
601
+ if (fs.existsSync(serviceUtilsDir))
602
+ fs.rmSync(serviceUtilsDir, { recursive: true });
603
+ }
604
+ }
605
+
606
+ // Get all services first (needed for gateway routing)
607
+ const servicesDir = path.join(target, "services");
608
+ const existingServices = fs.existsSync(servicesDir)
609
+ ? fs
610
+ .readdirSync(servicesDir)
611
+ .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
612
+ : [];
613
+ // Include services we're about to create so port computation and gateway routing
614
+ // are aware of newly added services when setting up files.
615
+ const allServices = Array.from(
616
+ new Set([...existingServices, ...servicesToSetup]),
617
+ );
618
+
619
+ // Step 1: Setup all service files first (without installing dependencies)
620
+ console.log(pc.cyan("\nāš™ļø Setting up service files...\n"));
621
+ const serviceConfigs = [];
622
+
623
+ for (const serviceName of servicesToSetup) {
624
+ const serviceRoot = path.join(target, "services", serviceName);
625
+ const shouldIncludeAuth = isInMicroserviceProject
626
+ ? config.auth
627
+ : serviceName === "auth-service";
628
+ const result = await setupService(
629
+ config,
630
+ serviceName,
631
+ serviceRoot,
632
+ shouldIncludeAuth,
633
+ allServices,
634
+ true, // Skip install for now
635
+ );
636
+ serviceConfigs.push({
637
+ serviceName,
638
+ serviceRoot,
639
+ deps: result.deps,
640
+ devDeps: result.devDeps,
641
+ });
642
+ }
643
+
644
+ // Remove per-service husky hooks and ensure a single root pre-commit hook
645
+ try {
646
+ const servicesDirPath = path.join(target, "services");
647
+ const allServicesList = fs.existsSync(servicesDirPath)
648
+ ? fs
649
+ .readdirSync(servicesDirPath)
650
+ .filter((f) =>
651
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
652
+ )
653
+ : [];
654
+
655
+ // Remove `.husky` folders from each service
656
+ for (const svc of allServicesList) {
657
+ const svcHusky = path.join(servicesDirPath, svc, ".husky");
658
+ if (fs.existsSync(svcHusky)) {
659
+ fs.rmSync(svcHusky, { recursive: true, force: true });
660
+ }
661
+ }
662
+
663
+ // Ensure root .husky/pre-commit exists at target
664
+ const rootHuskyDir = path.join(target, ".husky");
665
+ if (!fs.existsSync(rootHuskyDir))
666
+ fs.mkdirSync(rootHuskyDir, { recursive: true });
667
+ const preCommitPath = path.join(rootHuskyDir, "pre-commit");
668
+ const preCommitContent =
669
+ config.language === "typescript"
670
+ ? 'set -e\n\necho "Checking format (prettier)..."\nnpm run check-format\n\necho "Running TypeScript type-check..."\nnpx tsc --noEmit\n\necho "Checking lint..."\nnpm run lint -- --max-warnings=0\n'
671
+ : 'set -e\n\necho "Checking format (prettier)..."\nnpm run check-format\n\necho "Checking lint..."\nnpm run lint -- --max-warnings=0\n';
672
+ fs.writeFileSync(preCommitPath, preCommitContent);
673
+ } catch (err) {
674
+ // Non-fatal; continue setup even if husky files couldn't be created/removed
675
+ }
676
+
677
+ // Step 2: Generate docker-compose/pm2 config and root files
678
+ if (mode === "docker") {
679
+ generateDockerCompose(target, allServices, config.sanitizedName);
680
+ copyDockerfile(target, servicesToSetup);
681
+ copyDockerignore(target, servicesToSetup);
682
+ } else {
683
+ generatePm2Config(target, allServices);
684
+ }
685
+
686
+ // Create root package.json for microservice monorepo if it doesn't exist
687
+ const rootPackageJsonPath = path.join(target, "package.json");
688
+ if (!fs.existsSync(rootPackageJsonPath)) {
689
+ const rootPackageJson = {
690
+ name: sanitizedName,
691
+ version: config.version || "1.0.0",
692
+ description: config.description || "",
693
+ private: true,
694
+ scripts: {
695
+ dev:
696
+ mode === "docker"
697
+ ? "docker-compose up"
698
+ : "npx pm2 start pm2.config.js && npx pm2 logs",
699
+ stop: mode === "docker" ? "docker-compose down" : "npx pm2 kill",
700
+ restart:
701
+ mode === "docker"
702
+ ? "docker-compose restart"
703
+ : "npx pm2 restart all && npx pm2 logs",
704
+ lint: 'eslint "services/**/*.{js,ts,tsx}" "shared/**/*.{js,ts,tsx}"',
705
+ format:
706
+ 'prettier --write "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
707
+ "check-format":
708
+ 'prettier --check "services/**/*.{js,ts,json}" "shared/**/*.{js,ts,json}"',
709
+ prepare: "husky install",
710
+ },
711
+ devDependencies: {
712
+ husky: "^9.1.7",
713
+ prettier: "^3.7.4",
714
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
715
+ "@typescript-eslint/parser": "^8.50.1",
716
+ eslint: "^9.39.2",
717
+ "eslint-config-prettier": "^10.1.8",
718
+ },
719
+ };
720
+
721
+ // Add runtime dependencies for non-Docker (PM2) mode
722
+ if (mode !== "docker") {
723
+ rootPackageJson.dependencies = {
724
+ dotenv: "^17.2.3",
725
+ pm2: "^6.0.14",
726
+ "ts-node": "^10.9.2",
727
+ "tsconfig-paths": "^4.2.0",
728
+ };
729
+ }
730
+ fs.writeFileSync(
731
+ rootPackageJsonPath,
732
+ JSON.stringify(rootPackageJson, null, 2) + "\n",
733
+ );
734
+ }
735
+
736
+ // Ensure root lint/format config files exist (copy from template base if available), and remove any per-service copies
737
+ try {
738
+ const rootFiles = [".prettierrc", ".prettierignore", ".eslintrc.json"];
739
+ for (const f of rootFiles) {
740
+ const src = path.join(base, f);
741
+ const dest = path.join(target, f);
742
+ if (fs.existsSync(src)) {
743
+ fs.copyFileSync(src, dest);
744
+ } else if (!fs.existsSync(dest)) {
745
+ // create minimal defaults
746
+ if (f === ".prettierignore")
747
+ fs.writeFileSync(dest, "node_modules\n" + "dist\n");
748
+ else if (f === ".eslintrc.json")
749
+ fs.writeFileSync(dest, JSON.stringify({ root: true }, null, 2));
750
+ else fs.writeFileSync(dest, "{}");
751
+ }
752
+ }
753
+
754
+ // Write eslint.config.js with recommended workspace config (overwrite)
755
+ const eslintConfigPath = path.join(target, "eslint.config.js");
756
+
757
+ // Build dynamic project list for TypeScript projects based on the services present
758
+ const projectPaths = ["./tsconfig.json"];
759
+ try {
760
+ if (typeof allServices !== "undefined" && Array.isArray(allServices)) {
761
+ for (const svc of allServices) {
762
+ const svcTsPath = `./services/${svc}/tsconfig.json`;
763
+ if (fs.existsSync(path.join(target, svcTsPath))) {
764
+ projectPaths.push(svcTsPath);
765
+ }
766
+ }
767
+ }
768
+ } catch (e) {
769
+ // non-fatal; fall back to default projectPaths containing only root tsconfig
770
+ }
771
+
772
+ const projectEntries = projectPaths
773
+ .map((p) => ` "${p}",`)
774
+ .join("\n");
775
+
776
+ const eslintConfigContent = `const tsParser = require("@typescript-eslint/parser");\nconst tsPlugin = require("@typescript-eslint/eslint-plugin");\n\nmodule.exports = [\n // Files/paths to ignore (replaces .eslintignore usage in flat config)\n {\n ignores: ["node_modules/**", "dist/**"],\n },\n\n // TypeScript rules for source files\n {\n files: ["services/**/*.{js,ts,tsx}", "shared/**/*.{js,ts,tsx}"],\n languageOptions: {\n parser: tsParser,\n parserOptions: {\n project: [\n${projectEntries}\n ],\n tsconfigRootDir: __dirname,\n ecmaVersion: 2020,\n sourceType: "module",\n },\n },\n plugins: {\n "@typescript-eslint": tsPlugin,\n },\n rules: {\n // Disallow explicit 'any'\n "@typescript-eslint/no-explicit-any": "error",\n\n // You can add or tune more TypeScript rules here\n "@typescript-eslint/explicit-module-boundary-types": "off",\n },\n },\n];\n`;
777
+ fs.writeFileSync(eslintConfigPath, eslintConfigContent);
778
+
779
+ // Remove per-service copies if they exist (already removed in setupService, but double-check)
780
+ const servicesDirPath = path.join(target, "services");
781
+ if (fs.existsSync(servicesDirPath)) {
782
+ const svcs = fs
783
+ .readdirSync(servicesDirPath)
784
+ .filter((f) =>
785
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
786
+ );
787
+ for (const svc of svcs) {
788
+ for (const f of [...rootFiles, "eslint.config.js"]) {
789
+ const p = path.join(servicesDirPath, svc, f);
790
+ if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
791
+ }
792
+ }
793
+ }
794
+ } catch (err) {
795
+ // non-fatal
796
+ }
797
+
798
+ // Step 3: Generate README and create root configuration files
799
+ if (!isInMicroserviceProject) {
800
+ console.log(`\n${pc.cyan("šŸ“ Generating README.md...")}\n`);
801
+ const readmeContent = generateReadme(config);
802
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
803
+ writeStarterWorkflow(target, config);
804
+ writePullRequestTemplate(target, config);
805
+ writeContributingGuide(target, config);
806
+
807
+ // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
808
+ for (const service of allServices) {
809
+ const gitignorePath = path.join(servicesDir, service, "gitignore");
810
+ const dotGitignorePath = path.join(servicesDir, service, ".gitignore");
811
+ if (fs.existsSync(gitignorePath)) {
812
+ fs.renameSync(gitignorePath, dotGitignorePath);
813
+ }
814
+ }
815
+
816
+ // Create root .gitignore for microservices
817
+ const rootGitignoreContent = `.env\nnode_modules\n`;
818
+ fs.writeFileSync(path.join(target, ".gitignore"), rootGitignoreContent);
819
+
820
+ // Create root .env and .env.example for microservices
821
+ let rootENVContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
822
+
823
+ // Add port configuration for each service
824
+ allServices.forEach((service, index) => {
825
+ const isGateway = service === "gateway";
826
+ const port = isGateway
827
+ ? 4000
828
+ : 4001 +
829
+ allServices.filter((s, i) => s !== "gateway" && i < index).length;
830
+ const envVarName = `${service.toUpperCase().replace(/-/g, "_")}_PORT`;
831
+ const serviceName = service
832
+ .split("-")
833
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
834
+ .join(" ");
835
+ rootENVContent += `# ${serviceName}\n${envVarName}=${port}\n\n`;
836
+ });
837
+
838
+ fs.writeFileSync(path.join(target, ".env"), rootENVContent);
839
+ fs.writeFileSync(path.join(target, ".env.example"), rootENVContent);
840
+
841
+ // Create root tsconfig.json for microservices workspace
842
+ const rootTsConfigContent = {
843
+ compilerOptions: {
844
+ target: "ES2020",
845
+ module: "CommonJS",
846
+ lib: ["ES2020"],
847
+ moduleResolution: "node",
848
+ esModuleInterop: true,
849
+ skipLibCheck: true,
850
+ strict: true,
851
+ baseUrl: ".",
852
+ paths: {
853
+ "@/*": ["./*"],
854
+ },
855
+ },
856
+ include: [],
857
+ exclude: ["node_modules", "dist"],
858
+ references: allServices.map((service) => ({
859
+ path: `./services/${service}`,
860
+ })),
861
+ };
862
+ fs.writeFileSync(
863
+ path.join(target, "tsconfig.json"),
864
+ JSON.stringify(rootTsConfigContent, null, 2) + "\n",
865
+ );
866
+ }
867
+
868
+ // If we're adding a service into an existing microservice project,
869
+ // ensure shared config and gateway are updated to reference the new service.
870
+ if (isInMicroserviceProject) {
871
+ try {
872
+ const sharedConfigDir = path.join(target, "shared", "config");
873
+ const languageExt = config.language === "javascript" ? "js" : "ts";
874
+ const sharedEnvPath = path.join(sharedConfigDir, `env.${languageExt}`);
875
+
876
+ if (fs.existsSync(sharedEnvPath)) {
877
+ let envContent = fs.readFileSync(sharedEnvPath, "utf8");
878
+
879
+ // Build port environment variables for all services
880
+ const portEnvVars = allServices
881
+ .map((service) => {
882
+ const envVarName = `${service
883
+ .toUpperCase()
884
+ .replace(/-/g, "_")}_PORT`;
885
+ const assertion = config.language === "javascript" ? "" : "!";
886
+ return ` ${envVarName}: process.env.${envVarName}${assertion},`;
887
+ })
888
+ .join("\n");
889
+
890
+ // Remove any existing *_PORT lines to avoid duplication
891
+ envContent = envContent.replace(
892
+ /^[ \t]*[A-Z0-9_]+_PORT:\s*process\.env\.[A-Z0-9_]+!?\,?\s*$/gim,
893
+ "",
894
+ );
895
+ // Normalize multiple consecutive blank lines
896
+ envContent = envContent.replace(/\n{2,}/g, "\n\n");
897
+
898
+ // Attempt several fallback strategies to inject port variables:
899
+ // 1. Replace explicit placeholder if present in template
900
+ // 2. Insert right after the first object opening brace (or replace placeholder)
901
+ if (envContent.includes("/*__PORTS__*/")) {
902
+ envContent = envContent.replace(
903
+ "/*__PORTS__*/",
904
+ "/*__PORTS__*/\n" + portEnvVars,
905
+ );
906
+ } else {
907
+ // Fallback: find the opening brace of the exported ENV object and insert after it
908
+ const braceIndex = envContent.indexOf("{");
909
+ if (braceIndex !== -1) {
910
+ const insertPos =
911
+ envContent.indexOf("\n", braceIndex) + 1 || braceIndex + 1;
912
+ // insert a stable placeholder comment followed by the ports block
913
+ envContent =
914
+ envContent.slice(0, insertPos) +
915
+ " /*__PORTS__*/\n" +
916
+ portEnvVars +
917
+ "\n" +
918
+ envContent.slice(insertPos);
919
+ } else {
920
+ // Final fallback: append a placeholder and the ports to the end
921
+ envContent = envContent + "\n/*__PORTS__*/\n" + portEnvVars;
922
+ }
923
+ }
924
+
925
+ fs.writeFileSync(sharedEnvPath, envContent);
926
+ }
927
+
928
+ // Re-generate gateway routes if gateway exists (so new service gets proxied)
929
+ const gatewayRoot = path.join(target, "services", "gateway");
930
+ if (fs.existsSync(gatewayRoot)) {
931
+ // Re-run setupService for gateway to rewrite app/server/env files
932
+ await setupService(
933
+ config,
934
+ "gateway",
935
+ gatewayRoot,
936
+ config.auth,
937
+ allServices,
938
+ true,
939
+ );
940
+ }
941
+
942
+ // Update root .env and .env.example so newly added services have port entries
943
+ try {
944
+ const servicesDirPath = path.join(target, "services");
945
+ const svcList = fs.existsSync(servicesDirPath)
946
+ ? fs
947
+ .readdirSync(servicesDirPath)
948
+ .filter((f) =>
949
+ fs.statSync(path.join(servicesDirPath, f)).isDirectory(),
950
+ )
951
+ : allServices;
952
+
953
+ // Update only .env.example: preserve runtime .env (don't overwrite user changes)
954
+ try {
955
+ const envExamplePath = path.join(target, ".env.example");
956
+ const servicesPorts = svcList.map((service, index) => {
957
+ const isGateway = service === "gateway";
958
+ const port = isGateway
959
+ ? 4000
960
+ : 4001 +
961
+ svcList.filter((s, i) => s !== "gateway" && i < index).length;
962
+ const envVarName = `${service
963
+ .toUpperCase()
964
+ .replace(/-/g, "_")}_PORT`;
965
+ const serviceNamePretty = service
966
+ .split("-")
967
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
968
+ .join(" ");
969
+ return `# ${serviceNamePretty}\n${envVarName}=${port}\n`;
970
+ });
971
+
972
+ const portsBlock = servicesPorts.join("\n");
973
+
974
+ let exampleContent = "";
975
+ if (fs.existsSync(envExamplePath)) {
976
+ exampleContent = fs.readFileSync(envExamplePath, "utf8");
977
+
978
+ // Remove existing *_PORT lines and any immediate preceding single-line comment
979
+ const lines = exampleContent.split(/\r?\n/);
980
+ const filtered = [];
981
+ for (let i = 0; i < lines.length; i++) {
982
+ const line = lines[i];
983
+ const next = lines[i + 1];
984
+ if (next && /^[A-Z0-9_]+_PORT=/.test(next.trim())) {
985
+ // skip this line if it's a comment immediately preceding a _PORT assignment
986
+ i++; // skip next as well
987
+ continue;
988
+ }
989
+ if (/^[A-Z0-9_]+_PORT=/.test(line.trim())) {
990
+ // skip existing port assignment
991
+ continue;
992
+ }
993
+ filtered.push(line);
994
+ }
995
+
996
+ exampleContent = filtered.join("\n");
997
+ } else {
998
+ // create minimal header if example file doesn't exist
999
+ exampleContent = `# Environment Configuration\nNODE_ENV=development\n\n`;
1000
+ }
1001
+
1002
+ // Ensure NODE_ENV line exists and insert portsBlock after it
1003
+ const nodeEnvRegex = /^NODE_ENV=.*$/m;
1004
+ if (nodeEnvRegex.test(exampleContent)) {
1005
+ exampleContent = exampleContent.replace(
1006
+ nodeEnvRegex,
1007
+ (m) => `${m}\n\n${portsBlock}\n`,
1008
+ );
1009
+ } else {
1010
+ // Prepend header and ports
1011
+ exampleContent =
1012
+ `# Environment Configuration\nNODE_ENV=development\n\n${portsBlock}\n` +
1013
+ exampleContent;
1014
+ }
1015
+
1016
+ fs.writeFileSync(envExamplePath, exampleContent);
1017
+ // Inform the user about the new ports and remind them to update their runtime .env if needed
1018
+ try {
1019
+ console.log(
1020
+ pc.cyan("\nšŸ”§ Updated .env.example with service port entries:\n"),
1021
+ );
1022
+ console.log(pc.green(portsBlock));
1023
+ console.log(
1024
+ pc.dim(
1025
+ "If you keep a runtime .env with custom overrides, do NOT overwrite it.\nPlease copy any new *_PORT entries from .env.example into .env as appropriate.",
1026
+ ),
1027
+ );
1028
+ } catch (e) {
1029
+ // non-fatal if logging fails
1030
+ }
1031
+ } catch (e) {
1032
+ // non-fatal
1033
+ }
1034
+ } catch (e) {
1035
+ // non-fatal
1036
+ }
1037
+ } catch (e) {
1038
+ // non-fatal; continue even if updating shared/gateway fails
1039
+ }
1040
+ }
1041
+
1042
+ // Step 5: Install dependencies for all services
1043
+
1044
+ console.log(pc.cyan("\nšŸ“¦ Installing dependencies for all services...\n"));
1045
+ let allInstallsSucceeded = true;
1046
+
1047
+ for (const { serviceName, serviceRoot, deps, devDeps } of serviceConfigs) {
1048
+ console.log(
1049
+ pc.cyan(`\nšŸ“¦ Installing dependencies for ${serviceName}...\n`),
1050
+ );
1051
+
1052
+ try {
1053
+ if (deps.length) {
1054
+ execSync(`npm install ${deps.join(" ")}`, {
1055
+ cwd: serviceRoot,
1056
+ stdio: "inherit",
1057
+ });
1058
+ }
1059
+ if (devDeps.length) {
1060
+ execSync(`npm install -D ${devDeps.join(" ")}`, {
1061
+ cwd: serviceRoot,
1062
+ stdio: "inherit",
1063
+ });
1064
+ }
1065
+ execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
1066
+ } catch (error) {
1067
+ allInstallsSucceeded = false;
1068
+ console.error(
1069
+ pc.red(`\nāŒ Failed to install dependencies for ${serviceName}`),
1070
+ );
1071
+ console.error(pc.dim(`\nYou can install them later by running:`));
1072
+ console.error(pc.cyan(` cd services/${serviceName} && npm install\n`));
1073
+ }
1074
+ }
1075
+
1076
+ // Store for later use
1077
+ config.allInstallsSucceeded = allInstallsSucceeded;
1078
+ } else {
1079
+ config.monolithSetupResult = await setupService(config, null, target, true);
1080
+ config.installSucceeded = config.monolithSetupResult.installSucceeded;
1081
+ }
1082
+
1083
+ // Generate README.md for monolith (microservices already done above)
1084
+ if (!isInMicroserviceProject && config.projectType === "monolith") {
1085
+ console.log(`\n${pc.cyan("šŸ“ Generating README.md...")}\n`);
1086
+ const readmeContent = generateReadme(config);
1087
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
1088
+ writeStarterWorkflow(target, config);
1089
+ writePullRequestTemplate(target, config);
1090
+ writeContributingGuide(target, config);
1091
+
1092
+ // Rename gitignore to .gitignore (npm doesn't publish .gitignore files)
1093
+ const gitignorePath = path.join(target, "gitignore");
1094
+ const dotGitignorePath = path.join(target, ".gitignore");
1095
+ if (fs.existsSync(gitignorePath)) {
1096
+ fs.renameSync(gitignorePath, dotGitignorePath);
1097
+ }
1098
+
1099
+ // Generate .env from .env.example for monolith only
1100
+ console.log(`${pc.cyan("šŸ“„ Setting up environment files...")}\n`);
1101
+ try {
1102
+ const rootEnvExamplePath = path.join(target, ".env.example");
1103
+ const rootEnvPath = path.join(target, ".env");
1104
+ if (fs.existsSync(rootEnvExamplePath) && !fs.existsSync(rootEnvPath)) {
1105
+ fs.copyFileSync(rootEnvExamplePath, rootEnvPath);
1106
+ }
1107
+ } catch (err) {
1108
+ // Non-fatal; proceed even if we fail to write env files
1109
+ }
1110
+ }
1111
+
1112
+ // Install monolith dependencies last, after all files have been generated.
1113
+ if (!isInMicroserviceProject && config.projectType === "monolith") {
1114
+ const result = config.monolithSetupResult;
1115
+
1116
+ try {
1117
+ if (result.deps && result.deps.length) {
1118
+ execSync(`npm install ${result.deps.join(" ")}`, {
1119
+ cwd: target,
1120
+ stdio: "inherit",
1121
+ });
1122
+ }
1123
+ if (result.devDeps && result.devDeps.length) {
1124
+ execSync(`npm install -D ${result.devDeps.join(" ")}`, {
1125
+ cwd: target,
1126
+ stdio: "inherit",
1127
+ });
1128
+ }
1129
+ execSync("npm install", { cwd: target, stdio: "inherit" });
1130
+ } catch (error) {
1131
+ console.error(pc.red("\nāŒ Failed to install monolith dependencies"));
1132
+ console.error(pc.dim(`\nYou can install them later by running:`));
1133
+ console.error(pc.cyan(` cd ${target} && npm install\n`));
1134
+
1135
+ if (
1136
+ error?.code === "ENOSPC" ||
1137
+ /no space left on device/i.test(error?.message || "")
1138
+ ) {
1139
+ console.error(
1140
+ pc.red(
1141
+ "\nšŸ›‘ Generation stopped because the disk is full. Free up space and run the generator again.\n",
1142
+ ),
1143
+ );
1144
+ process.exit(1);
1145
+ }
1146
+ }
1147
+
1148
+ // Safety net: ensure eslint.config.js exists in generated monolith projects
1149
+ const templateEslintConfig = path.join(base, "eslint.config.js");
1150
+ const generatedEslintConfig = path.join(target, "eslint.config.js");
1151
+ if (
1152
+ fs.existsSync(templateEslintConfig) &&
1153
+ !fs.existsSync(generatedEslintConfig)
1154
+ ) {
1155
+ fs.copyFileSync(templateEslintConfig, generatedEslintConfig);
1156
+ }
1157
+ }
1158
+
1159
+ // Initialize git and Husky
1160
+ if (!isInMicroserviceProject) {
1161
+ execSync("git init", { cwd: target, stdio: "inherit" });
1162
+
1163
+ // Install husky and other devDeps and setup at root level
1164
+ if (config.projectType === "microservice") {
1165
+ console.log("\nšŸ“¦ Installing dependencies at root level...\n");
1166
+ if (config.allInstallsSucceeded) {
1167
+ try {
1168
+ execSync("npm install", { cwd: target, stdio: "inherit" });
1169
+ console.log("\nšŸ”§ Setting up Husky...\n");
1170
+ execSync("npm run prepare", { cwd: target, stdio: "inherit" });
1171
+ } catch (error) {
1172
+ console.log("\nāš ļø Husky setup failed\n");
1173
+ }
1174
+ // Run format after successful install
1175
+ console.log(pc.cyan("\nšŸŽØ Formatting code...\n"));
1176
+ try {
1177
+ execSync("npm run format", { cwd: target, stdio: "inherit" });
1178
+ } catch (formatError) {
1179
+ console.warn(
1180
+ pc.yellow(
1181
+ "āš ļø Warning: Code formatting failed. You can run it manually later with: npm run format\n",
1182
+ ),
1183
+ );
1184
+ }
1185
+ } else {
1186
+ console.log(
1187
+ "\nāš ļø Husky setup skipped (run 'npm install && npm run prepare' after fixing service dependencies)\n",
1188
+ );
1189
+ }
1190
+ } else if (config.projectType === "monolith") {
1191
+ // Only setup Husky if installation succeeded
1192
+ if (config.installSucceeded) {
1193
+ console.log(`\n${pc.cyan("šŸ”§ Setting up Husky...")}\n`);
1194
+ try {
1195
+ execSync("npm run prepare", { cwd: target, stdio: "inherit" });
1196
+ } catch (error) {
1197
+ console.log(
1198
+ `\n${pc.yellow("āš ļø Husky setup failed")} ${pc.dim(
1199
+ "(run 'npm run prepare' manually after fixing dependencies)",
1200
+ )}\n`,
1201
+ );
1202
+ }
1203
+ } else {
1204
+ console.log(
1205
+ `\n${pc.yellow("āš ļø Husky setup skipped")} ${pc.dim(
1206
+ "(run 'npm install && npm run prepare' to set up git hooks)",
1207
+ )}\n`,
1208
+ );
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ // Success messages
1214
+ const servicesDir = path.join(target, "services");
1215
+ const allServices = fs.existsSync(servicesDir)
1216
+ ? fs
1217
+ .readdirSync(servicesDir)
1218
+ .filter((f) => fs.statSync(path.join(servicesDir, f)).isDirectory())
1219
+ : servicesToCreate;
1220
+ // Update root README when adding services to an existing microservice project
1221
+ if (isInMicroserviceProject) {
1222
+ try {
1223
+ const readmeContent = generateReadme({ ...config, allServices });
1224
+ fs.writeFileSync(path.join(target, "README.md"), readmeContent);
1225
+ console.log(`\n${pc.cyan("šŸ“ Updated README.md with new services")}`);
1226
+ } catch (e) {
1227
+ // non-fatal
1228
+ }
1229
+ }
1230
+
1231
+ if (isInMicroserviceProject) {
1232
+ if (servicesToSetup.length > 0) {
1233
+ console.log(
1234
+ `\n${pc.green("āœ… Service")} ${pc.bold(servicesToSetup[0])} ${pc.green(
1235
+ "added successfully!",
1236
+ )}`,
1237
+ );
1238
+ } else {
1239
+ console.log(
1240
+ pc.yellow(
1241
+ "\nāš ļø No new service was created (skipped by your selection).",
1242
+ ),
1243
+ );
1244
+ }
1245
+ console.log(`\n${pc.cyan("šŸ“¦ All services:")} ${allServices.join(", ")}`);
1246
+ console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
1247
+ console.log(` ${pc.dim("1.")} Start services: ${pc.bold("npm run dev")}`);
1248
+ } else if (config.projectType === "microservice") {
1249
+ console.log(`\n${pc.green("āœ… Microservice Backend created successfully!")}`);
1250
+ console.log(
1251
+ `\n${pc.cyan("šŸ“¦ Created services:")} ${servicesToCreate.join(", ")}`,
1252
+ );
1253
+ console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
1254
+ console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
1255
+ console.log(` ${pc.dim("2.")} Start services: ${pc.bold("npm run dev")}`);
1256
+ } else {
1257
+ console.log(`\n${pc.green("āœ… Monolith Backend created successfully!")}`);
1258
+ console.log(`\n${pc.blue("šŸ’” Next steps:")}`);
1259
+ console.log(` ${pc.dim("1.")} cd ${pc.bold(sanitizedName)}`);
1260
+ console.log(` ${pc.dim("2.")} npm run dev`);
1261
+ }
1262
+ // Post-processing: ensure shared config does not export/connect to DB when auth is disabled
1263
+ try {
1264
+ if (!config.auth) {
1265
+ const sharedConfigDir = path.join(target, "shared", "config");
1266
+ if (fs.existsSync(sharedConfigDir)) {
1267
+ for (const ext of ["ts", "js"]) {
1268
+ const idxPath = path.join(sharedConfigDir, `index.${ext}`);
1269
+ if (!fs.existsSync(idxPath)) continue;
1270
+ let idxContent = fs.readFileSync(idxPath, "utf8");
1271
+ // Remove various connectDB export/import patterns
1272
+ idxContent = idxContent.replace(
1273
+ /export\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
1274
+ "",
1275
+ );
1276
+ idxContent = idxContent.replace(
1277
+ /import\s*\{\s*connectDB\s*\}\s*from\s*["']\.\/db["'];?/g,
1278
+ "",
1279
+ );
1280
+ idxContent = idxContent.replace(
1281
+ /const\s*\{\s*connectDB\s*\}\s*=\s*require\(["']\.\/db["']\);?/g,
1282
+ "",
1283
+ );
1284
+ // Remove any remaining connectDB identifiers (commas/newlines)
1285
+ idxContent = idxContent.replace(/connectDB,?/g, "");
1286
+ // Normalize multiple blank lines
1287
+ idxContent = idxContent.replace(/\n{3,}/g, "\n\n");
1288
+ fs.writeFileSync(idxPath, idxContent);
1289
+ }
1290
+ }
1291
+ }
1292
+ } catch (e) {
1293
+ // non-fatal
1294
+ }