@ifecodes/backend-template 1.1.9 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +423 -383
  2. package/bin/cli.js +1276 -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
@@ -1,241 +1,277 @@
1
- import prompts from "prompts";
2
- import pc from "picocolors";
3
- import fs from "fs";
4
- import path from "path";
5
-
6
- export const getProjectConfig = async () => {
7
- // Check if running in CI or non-interactive mode
8
- const isCI = process.env.CI === 'true' || !process.stdin.isTTY;
9
-
10
- // Check if we're in an existing microservice project
11
- const isInMicroserviceProject = fs.existsSync(
12
- path.join(process.cwd(), "services")
13
- );
14
-
15
- // Parse command line arguments
16
- const args = process.argv.slice(2);
17
-
18
- // Separate project name parts from project type
19
- // Look for "microservice", "monolith", "micro", or "mono" in args
20
- let cliName = null;
21
- let cliProjectType = null;
22
-
23
- const projectTypeKeywords = ["microservice", "monolith", "micro", "mono"];
24
- const projectTypeIndex = args.findIndex(arg =>
25
- projectTypeKeywords.includes(arg.toLowerCase())
26
- );
27
-
28
- if (projectTypeIndex !== -1) {
29
- // Everything before the type keyword is the project name
30
- cliName = args.slice(0, projectTypeIndex).join("-");
31
- const typeArg = args[projectTypeIndex].toLowerCase();
32
- cliProjectType = typeArg === "micro" || typeArg === "microservice"
33
- ? "microservice"
34
- : "monolith";
35
- } else if (args.length > 0) {
36
- // If no type keyword found, treat all args as project name
37
- cliName = args.join("-");
38
- }
39
-
40
- // Pre-fill values from CLI args if provided
41
- const hasCliArgs = cliName && !isInMicroserviceProject;
42
-
43
- const res = await prompts(
44
- [
45
- {
46
- type: isInMicroserviceProject || hasCliArgs || isCI ? null : "text",
47
- name: "name",
48
- message: pc.cyan("Project name"),
49
- initial: "my-backend",
50
- },
51
- {
52
- type: isInMicroserviceProject || isCI ? null : "select",
53
- name: "language",
54
- message: pc.cyan("Select language"),
55
- choices: [
56
- { title: pc.green("TypeScript"), value: "typescript" },
57
- { title: pc.yellow("JavaScript"), value: "javascript" },
58
- ],
59
- initial: 0,
60
- },
61
- {
62
- type: isInMicroserviceProject || isCI ? null : "text",
63
- name: "description",
64
- message: pc.cyan("Project description") + pc.dim(" (optional)"),
65
- initial: "",
66
- },
67
- {
68
- type: isInMicroserviceProject || isCI ? null : "text",
69
- name: "author",
70
- message: pc.cyan("Author") + pc.dim(" (optional)"),
71
- initial: "",
72
- },
73
- {
74
- type: isInMicroserviceProject || isCI ? null : "text",
75
- name: "keywords",
76
- message: pc.cyan("Keywords") + pc.dim(" (comma-separated, optional)"),
77
- initial: "",
78
- },
79
- {
80
- type: isInMicroserviceProject || (hasCliArgs && cliProjectType) || isCI ? null : "select",
81
- name: "projectType",
82
- message: pc.cyan("Project type"),
83
- choices: [
84
- { title: pc.blue("Monolith API"), value: "monolith" },
85
- { title: pc.magenta("Microservice"), value: "microservice" },
86
- ],
87
- },
88
- {
89
- type: (prev, values) => {
90
- // Skip if in existing microservice project or CI mode
91
- if (isInMicroserviceProject || isCI) return null;
92
-
93
- // Get projectType from previous answers or CLI args
94
- const projectType = prev || cliProjectType || values.projectType;
95
-
96
- // Show prompt only if projectType is microservice
97
- return projectType === "microservice" ? "select" : null;
98
- },
99
- name: "mode",
100
- message: pc.cyan("Microservice setup"),
101
- choices: [
102
- { title: pc.blue("With Docker 🐳"), value: "docker" },
103
- { title: pc.yellow("Without Docker (PM2)"), value: "nodocker" },
104
- ],
105
- },
106
- {
107
- type: isInMicroserviceProject ? "text" : isCI ? null : "multiselect",
108
- name: isInMicroserviceProject ? "serviceName" : "features",
109
- message: isInMicroserviceProject
110
- ? pc.cyan("New service name") + pc.dim(" (e.g., user-service, order-service)")
111
- : pc.cyan("Select features"),
112
- choices: isInMicroserviceProject
113
- ? undefined
114
- : [
115
- { title: pc.blue("CORS"), value: "cors" },
116
- { title: pc.yellow("Rate Limiter"), value: "rate-limit" },
117
- { title: pc.green("Helmet"), value: "helmet" },
118
- { title: pc.magenta("Morgan (HTTP logger)"), value: "morgan" },
119
- ],
120
- },
121
- {
122
- type: isCI ? null : "toggle",
123
- name: "auth",
124
- message: isInMicroserviceProject
125
- ? pc.cyan("Include authentication in this service?")
126
- : pc.cyan("Include authentication system?"),
127
- initial: true,
128
- active: pc.green("yes"),
129
- inactive: pc.red("no"),
130
- },
131
- {
132
- type: isInMicroserviceProject && !isCI ? "multiselect" : null,
133
- name: "features",
134
- message: pc.cyan("Select features for this service"),
135
- choices: [
136
- { title: pc.yellow("Rate Limiter"), value: "rate-limit" },
137
- { title: pc.green("Helmet"), value: "helmet" },
138
- { title: pc.magenta("Morgan (HTTP logger)"), value: "morgan" },
139
- { title: pc.blue("CORS"), value: "cors" },
140
- ],
141
- },
142
- ],
143
- {
144
- onCancel: () => {
145
- console.log(pc.yellow("\n❌ Operation cancelled by user."));
146
- process.exit(0);
147
- }
148
- });
149
-
150
- // Handle cancelled prompts (user pressed Ctrl+C or closed the prompt)
151
- // Check if user cancelled the prompt (Ctrl+C) vs just didn't enter anything
152
- if (!res) {
153
- console.log(pc.yellow("\n❌ Operation cancelled by user."));
154
- process.exit(0);
155
- }
156
-
157
- // Check if critical fields are missing (indicates cancellation mid-prompt)
158
- if (!isInMicroserviceProject && !isCI) {
159
- // For new projects, we need language and projectType
160
- if (!res.language || (!res.projectType && !cliProjectType)) {
161
- console.log(pc.yellow("\n❌ Operation cancelled by user."));
162
- process.exit(0);
163
- }
164
- }
165
-
166
- // If the response is empty but we expected prompts, something went wrong
167
- if (Object.keys(res).length === 0 && !isInMicroserviceProject && !hasCliArgs && !isCI) {
168
- console.log(pc.red("\n❌ No inputs received. Please try again."));
169
- process.exit(1);
170
- }
171
-
172
- // Set defaults for CI/non-interactive mode
173
- if (isCI) {
174
- res.features = res.features || [];
175
- res.auth = res.auth ?? false;
176
- res.mode = res.mode || "docker"; // Default to docker in CI
177
- res.language = res.language || "typescript"; // Default to TypeScript in CI
178
- }
179
-
180
- // Merge CLI args with prompted responses
181
- if (hasCliArgs) {
182
- res.name = cliName;
183
- if (cliProjectType) {
184
- res.projectType = cliProjectType;
185
- } else {
186
- // If no project type in CLI, default to monolith
187
- res.projectType = res.projectType || "monolith";
188
- }
189
- }
190
-
191
- // Ensure we have a project name (fallback if somehow missed)
192
- if (!res.name && !isInMicroserviceProject) {
193
- res.name = "my-backend";
194
- }
195
-
196
- let sanitizedName, target, isExistingProject, mode;
197
-
198
- if (isInMicroserviceProject) {
199
- // We're adding to an existing project
200
- target = process.cwd();
201
- sanitizedName = path.basename(target);
202
- isExistingProject = true;
203
-
204
- // Detect mode from existing files
205
- mode = fs.existsSync(path.join(target, "docker-compose.yml"))
206
- ? "docker"
207
- : "nodocker";
208
-
209
- console.log(pc.cyan(`\n📁 Detected existing microservice project: ${sanitizedName}`));
210
- console.log(pc.dim(`Mode: ${mode}\n`));
211
- } else {
212
- sanitizedName = res.name.replace(/\s+/g, "-");
213
- target = path.resolve(process.cwd(), sanitizedName);
214
- isExistingProject = fs.existsSync(target);
215
- mode = res.mode;
216
- }
217
-
218
- // If adding to an existing microservice project, normalize the provided service name
219
- if (isInMicroserviceProject && res.serviceName) {
220
- res.serviceName = normalizeServiceName(res.serviceName);
221
- }
222
-
223
- return {
224
- ...res,
225
- sanitizedName,
226
- target,
227
- isExistingProject,
228
- mode,
229
- isInMicroserviceProject,
230
- };
231
- };
232
-
233
- // Normalize serviceName for existing microservice projects: ensure it ends with '-service'
234
- // and is kebab-cased/lowercase for consistency.
235
- function normalizeServiceName(name) {
236
- if (!name) return name;
237
- let svc = String(name).trim().toLowerCase();
238
- svc = svc.replace(/\s+/g, "-").replace(/[^a-z0-9\-]/g, "");
239
- if (!svc.endsWith("-service")) svc = `${svc}-service`;
240
- return svc;
241
- }
1
+ import prompts from "prompts";
2
+ import pc from "picocolors";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ export const getProjectConfig = async () => {
7
+ // Check if running in CI or non-interactive mode
8
+ const isCI = process.env.CI === "true" || !process.stdin.isTTY;
9
+
10
+ // Check if we're in an existing microservice project
11
+ const isInMicroserviceProject = fs.existsSync(
12
+ path.join(process.cwd(), "services")
13
+ );
14
+
15
+ // Parse command line arguments
16
+ const args = process.argv.slice(2);
17
+
18
+ // Separate project name parts from project type
19
+ // Look for "microservice", "monolith", "micro", or "mono" in args
20
+ let cliName = null;
21
+ let cliProjectType = null;
22
+
23
+ const projectTypeKeywords = ["microservice", "monolith", "micro", "mono"];
24
+ const projectTypeIndex = args.findIndex((arg) =>
25
+ projectTypeKeywords.includes(arg.toLowerCase())
26
+ );
27
+
28
+ if (projectTypeIndex !== -1) {
29
+ // Everything before the type keyword is the project name
30
+ cliName = args.slice(0, projectTypeIndex).join("-");
31
+ const typeArg = args[projectTypeIndex].toLowerCase();
32
+ cliProjectType =
33
+ typeArg === "micro" || typeArg === "microservice"
34
+ ? "microservice"
35
+ : "monolith";
36
+ } else if (args.length > 0) {
37
+ // If no type keyword found, treat all args as project name
38
+ cliName = args.join("-");
39
+ }
40
+
41
+ // Pre-fill values from CLI args if provided
42
+ const hasCliArgs = cliName && !isInMicroserviceProject;
43
+
44
+ const res = await prompts(
45
+ [
46
+ {
47
+ type: isInMicroserviceProject || hasCliArgs || isCI ? null : "text",
48
+ name: "name",
49
+ message: pc.cyan("Project name"),
50
+ initial: "my-backend",
51
+ },
52
+ {
53
+ type: isInMicroserviceProject || isCI ? null : "select",
54
+ name: "language",
55
+ message: pc.cyan("Select language"),
56
+ choices: [
57
+ { title: pc.green("TypeScript"), value: "typescript" },
58
+ { title: pc.yellow("JavaScript"), value: "javascript" },
59
+ ],
60
+ initial: 0,
61
+ },
62
+ {
63
+ type: isInMicroserviceProject || isCI ? null : "text",
64
+ name: "description",
65
+ message: pc.cyan("Project description") + pc.dim(" (optional)"),
66
+ initial: "",
67
+ },
68
+ {
69
+ type: isInMicroserviceProject || isCI ? null : "text",
70
+ name: "author",
71
+ message: pc.cyan("Author") + pc.dim(" (optional)"),
72
+ initial: "",
73
+ },
74
+ {
75
+ type: isInMicroserviceProject || isCI ? null : "text",
76
+ name: "keywords",
77
+ message: pc.cyan("Keywords") + pc.dim(" (comma-separated, optional)"),
78
+ initial: "",
79
+ },
80
+ {
81
+ type:
82
+ isInMicroserviceProject || (hasCliArgs && cliProjectType) || isCI
83
+ ? null
84
+ : "select",
85
+ name: "projectType",
86
+ message: pc.cyan("Project type"),
87
+ choices: [
88
+ { title: pc.blue("Monolith API"), value: "monolith" },
89
+ { title: pc.magenta("Microservice"), value: "microservice" },
90
+ ],
91
+ },
92
+ {
93
+ type: (prev, values) => {
94
+ // Skip if in existing microservice project or CI mode
95
+ if (isInMicroserviceProject || isCI) return null;
96
+
97
+ // Get projectType from previous answers or CLI args
98
+ const projectType = prev || cliProjectType || values.projectType;
99
+
100
+ // Show prompt only if projectType is microservice
101
+ return projectType === "microservice" ? "select" : null;
102
+ },
103
+ name: "mode",
104
+ message: pc.cyan("Microservice setup"),
105
+ choices: [
106
+ { title: pc.blue("With Docker 🐳"), value: "docker" },
107
+ { title: pc.yellow("Without Docker (PM2)"), value: "nodocker" },
108
+ ],
109
+ },
110
+ {
111
+ type: isInMicroserviceProject ? "text" : isCI ? null : "multiselect",
112
+ name: isInMicroserviceProject ? "serviceName" : "features",
113
+ message: isInMicroserviceProject
114
+ ? pc.cyan("New service name") +
115
+ pc.dim(" (e.g., user-service, order-service)")
116
+ : pc.cyan("Select features"),
117
+ choices: isInMicroserviceProject
118
+ ? undefined
119
+ : [
120
+ { title: pc.blue("CORS"), value: "cors" },
121
+ { title: pc.yellow("Rate Limiter"), value: "rate-limit" },
122
+ { title: pc.green("Helmet"), value: "helmet" },
123
+ { title: pc.magenta("Morgan (HTTP logger)"), value: "morgan" },
124
+ ],
125
+ },
126
+ {
127
+ type: isCI ? null : "toggle",
128
+ name: "auth",
129
+ message: isInMicroserviceProject
130
+ ? pc.cyan("Include authentication in this service?")
131
+ : pc.cyan("Include authentication system?"),
132
+ initial: true,
133
+ active: pc.green("yes"),
134
+ inactive: pc.red("no"),
135
+ },
136
+ {
137
+ type: isInMicroserviceProject && !isCI ? "multiselect" : null,
138
+ name: "features",
139
+ message: pc.cyan("Select features for this service"),
140
+ choices: [
141
+ { title: pc.yellow("Rate Limiter"), value: "rate-limit" },
142
+ { title: pc.green("Helmet"), value: "helmet" },
143
+ { title: pc.magenta("Morgan (HTTP logger)"), value: "morgan" },
144
+ { title: pc.blue("CORS"), value: "cors" },
145
+ ],
146
+ },
147
+ {
148
+ type: isInMicroserviceProject || isCI ? null : "select",
149
+ name: "projectScope",
150
+ message: pc.cyan("Is this a team or individual project?"),
151
+ choices: [
152
+ { title: pc.green("Team project"), value: "team" },
153
+ { title: pc.yellow("Individual project"), value: "individual" },
154
+ ],
155
+ initial: 0,
156
+ },
157
+ {
158
+ type: isInMicroserviceProject || isCI ? null : "toggle",
159
+ name: "validation",
160
+ message: pc.cyan("Include request validation with Zod?"),
161
+ initial: false,
162
+ active: pc.green("yes"),
163
+ inactive: pc.red("no"),
164
+ },
165
+ ],
166
+ {
167
+ onCancel: () => {
168
+ console.log(pc.yellow("\n❌ Operation cancelled by user."));
169
+ process.exit(0);
170
+ },
171
+ }
172
+ );
173
+
174
+ // Handle cancelled prompts (user pressed Ctrl+C or closed the prompt)
175
+ // Check if user cancelled the prompt (Ctrl+C) vs just didn't enter anything
176
+ if (!res) {
177
+ console.log(pc.yellow("\n❌ Operation cancelled by user."));
178
+ process.exit(0);
179
+ }
180
+
181
+ // Check if critical fields are missing (indicates cancellation mid-prompt)
182
+ if (!isInMicroserviceProject && !isCI) {
183
+ // For new projects, we need language and projectType
184
+ if (!res.language || (!res.projectType && !cliProjectType)) {
185
+ console.log(pc.yellow("\n❌ Operation cancelled by user."));
186
+ process.exit(0);
187
+ }
188
+ }
189
+
190
+ // If the response is empty but we expected prompts, something went wrong
191
+ if (
192
+ Object.keys(res).length === 0 &&
193
+ !isInMicroserviceProject &&
194
+ !hasCliArgs &&
195
+ !isCI
196
+ ) {
197
+ console.log(pc.red("\n❌ No inputs received. Please try again."));
198
+ process.exit(1);
199
+ }
200
+
201
+ // Set defaults for CI/non-interactive mode
202
+ if (isCI) {
203
+ res.features = res.features || [];
204
+ res.auth = res.auth ?? false;
205
+ res.mode = res.mode || "docker"; // Default to docker in CI
206
+ res.language = res.language || "typescript"; // Default to TypeScript in CI
207
+ res.projectScope = res.projectScope || "team";
208
+ res.validation = res.validation ?? false;
209
+ }
210
+
211
+ // Merge CLI args with prompted responses
212
+ if (hasCliArgs) {
213
+ res.name = cliName;
214
+ if (cliProjectType) {
215
+ res.projectType = cliProjectType;
216
+ } else {
217
+ // If no project type in CLI, default to monolith
218
+ res.projectType = res.projectType || "monolith";
219
+ }
220
+ }
221
+
222
+ // Ensure we have a project name (fallback if somehow missed)
223
+ if (!res.name && !isInMicroserviceProject) {
224
+ res.name = "my-backend";
225
+ }
226
+
227
+ // Derive a CI/CD preference from the project scope so later generators can use it.
228
+ res.cicd = !isInMicroserviceProject && res.projectScope !== "individual";
229
+
230
+ let sanitizedName, target, isExistingProject, mode;
231
+
232
+ if (isInMicroserviceProject) {
233
+ // We're adding to an existing project
234
+ target = process.cwd();
235
+ sanitizedName = path.basename(target);
236
+ isExistingProject = true;
237
+
238
+ // Detect mode from existing files
239
+ mode = fs.existsSync(path.join(target, "docker-compose.yml"))
240
+ ? "docker"
241
+ : "nodocker";
242
+
243
+ console.log(
244
+ pc.cyan(`\n📁 Detected existing microservice project: ${sanitizedName}`)
245
+ );
246
+ console.log(pc.dim(`Mode: ${mode}\n`));
247
+ } else {
248
+ sanitizedName = res.name.replace(/\s+/g, "-");
249
+ target = path.resolve(process.cwd(), sanitizedName);
250
+ isExistingProject = fs.existsSync(target);
251
+ mode = res.mode;
252
+ }
253
+
254
+ // If adding to an existing microservice project, normalize the provided service name
255
+ if (isInMicroserviceProject && res.serviceName) {
256
+ res.serviceName = normalizeServiceName(res.serviceName);
257
+ }
258
+
259
+ return {
260
+ ...res,
261
+ sanitizedName,
262
+ target,
263
+ isExistingProject,
264
+ mode,
265
+ isInMicroserviceProject,
266
+ };
267
+ };
268
+
269
+ // Normalize serviceName for existing microservice projects: ensure it ends with '-service'
270
+ // and is kebab-cased/lowercase for consistency.
271
+ function normalizeServiceName(name) {
272
+ if (!name) return name;
273
+ let svc = String(name).trim().toLowerCase();
274
+ svc = svc.replace(/\s+/g, "-").replace(/[^a-z0-9\-]/g, "");
275
+ if (!svc.endsWith("-service")) svc = `${svc}-service`;
276
+ return svc;
277
+ }