servcraft 0.1.0 → 0.1.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 (216) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command6 } from "commander";
4
+ import { Command as Command5 } from "commander";
5
5
 
6
6
  // src/cli/commands/init.ts
7
7
  import { Command } from "commander";
@@ -73,159 +73,183 @@ function getModulesDir() {
73
73
  }
74
74
 
75
75
  // src/cli/commands/init.ts
76
- var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(async (name, cmdOptions) => {
77
- console.log(chalk2.blue(`
76
+ var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
77
+ async (name, cmdOptions) => {
78
+ console.log(
79
+ chalk2.blue(`
78
80
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
79
81
  \u2551 \u2551
80
82
  \u2551 ${chalk2.bold("\u{1F680} Servcraft Project Generator")} \u2551
81
83
  \u2551 \u2551
82
84
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
83
- `));
84
- let options;
85
- if (cmdOptions?.yes) {
86
- options = {
87
- name: name || "my-servcraft-app",
88
- language: cmdOptions.javascript ? "javascript" : "typescript",
89
- database: cmdOptions.db || "postgresql",
90
- validator: "zod",
91
- features: ["auth", "users", "email"]
92
- };
93
- } else {
94
- const answers = await inquirer.prompt([
95
- {
96
- type: "input",
97
- name: "name",
98
- message: "Project name:",
99
- default: name || "my-servcraft-app",
100
- validate: (input) => {
101
- if (!/^[a-z0-9-_]+$/i.test(input)) {
102
- return "Project name can only contain letters, numbers, hyphens, and underscores";
85
+ `)
86
+ );
87
+ let options;
88
+ if (cmdOptions?.yes) {
89
+ const db = cmdOptions.db || "postgresql";
90
+ options = {
91
+ name: name || "my-servcraft-app",
92
+ language: cmdOptions.javascript ? "javascript" : "typescript",
93
+ database: db,
94
+ orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
95
+ validator: "zod",
96
+ features: ["auth", "users", "email"]
97
+ };
98
+ } else {
99
+ const answers = await inquirer.prompt([
100
+ {
101
+ type: "input",
102
+ name: "name",
103
+ message: "Project name:",
104
+ default: name || "my-servcraft-app",
105
+ validate: (input) => {
106
+ if (!/^[a-z0-9-_]+$/i.test(input)) {
107
+ return "Project name can only contain letters, numbers, hyphens, and underscores";
108
+ }
109
+ return true;
103
110
  }
104
- return true;
111
+ },
112
+ {
113
+ type: "list",
114
+ name: "language",
115
+ message: "Select language:",
116
+ choices: [
117
+ { name: "TypeScript (Recommended)", value: "typescript" },
118
+ { name: "JavaScript", value: "javascript" }
119
+ ],
120
+ default: "typescript"
121
+ },
122
+ {
123
+ type: "list",
124
+ name: "database",
125
+ message: "Select database:",
126
+ choices: [
127
+ { name: "PostgreSQL (Recommended for SQL)", value: "postgresql" },
128
+ { name: "MySQL", value: "mysql" },
129
+ { name: "SQLite (Development)", value: "sqlite" },
130
+ { name: "MongoDB (NoSQL)", value: "mongodb" },
131
+ { name: "None (Add later)", value: "none" }
132
+ ],
133
+ default: "postgresql"
134
+ },
135
+ {
136
+ type: "list",
137
+ name: "validator",
138
+ message: "Select validation library:",
139
+ choices: [
140
+ { name: "Zod (Recommended - TypeScript-first)", value: "zod" },
141
+ { name: "Joi (Battle-tested, feature-rich)", value: "joi" },
142
+ { name: "Yup (Inspired by Joi, lighter)", value: "yup" }
143
+ ],
144
+ default: "zod"
145
+ },
146
+ {
147
+ type: "checkbox",
148
+ name: "features",
149
+ message: "Select features to include:",
150
+ choices: [
151
+ { name: "Authentication (JWT)", value: "auth", checked: true },
152
+ { name: "User Management", value: "users", checked: true },
153
+ { name: "Email Service", value: "email", checked: true },
154
+ { name: "Audit Logs", value: "audit", checked: false },
155
+ { name: "File Upload", value: "upload", checked: false },
156
+ { name: "Redis Cache", value: "redis", checked: false }
157
+ ]
105
158
  }
106
- },
107
- {
108
- type: "list",
109
- name: "language",
110
- message: "Select language:",
111
- choices: [
112
- { name: "TypeScript (Recommended)", value: "typescript" },
113
- { name: "JavaScript", value: "javascript" }
114
- ],
115
- default: "typescript"
116
- },
117
- {
118
- type: "list",
119
- name: "database",
120
- message: "Select database:",
121
- choices: [
122
- { name: "PostgreSQL (Recommended)", value: "postgresql" },
123
- { name: "MySQL", value: "mysql" },
124
- { name: "SQLite (Development)", value: "sqlite" },
125
- { name: "MongoDB", value: "mongodb" },
126
- { name: "None (Add later)", value: "none" }
127
- ],
128
- default: "postgresql"
129
- },
130
- {
131
- type: "list",
132
- name: "validator",
133
- message: "Select validation library:",
134
- choices: [
135
- { name: "Zod (Recommended - TypeScript-first)", value: "zod" },
136
- { name: "Joi (Battle-tested, feature-rich)", value: "joi" },
137
- { name: "Yup (Inspired by Joi, lighter)", value: "yup" }
138
- ],
139
- default: "zod"
140
- },
141
- {
142
- type: "checkbox",
143
- name: "features",
144
- message: "Select features to include:",
145
- choices: [
146
- { name: "Authentication (JWT)", value: "auth", checked: true },
147
- { name: "User Management", value: "users", checked: true },
148
- { name: "Email Service", value: "email", checked: true },
149
- { name: "Audit Logs", value: "audit", checked: false },
150
- { name: "File Upload", value: "upload", checked: false },
151
- { name: "Redis Cache", value: "redis", checked: false }
152
- ]
153
- }
154
- ]);
155
- options = answers;
156
- }
157
- const projectDir = path2.resolve(process.cwd(), options.name);
158
- const spinner = ora("Creating project...").start();
159
- try {
160
- try {
161
- await fs2.access(projectDir);
162
- spinner.stop();
163
- error(`Directory "${options.name}" already exists`);
164
- return;
165
- } catch {
166
- }
167
- await ensureDir(projectDir);
168
- spinner.text = "Generating project files...";
169
- const packageJson = generatePackageJson(options);
170
- await writeFile(path2.join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2));
171
- if (options.language === "typescript") {
172
- await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig());
173
- await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig());
174
- } else {
175
- await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig());
176
- }
177
- await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
178
- await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
179
- await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
180
- await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
181
- await writeFile(path2.join(projectDir, "docker-compose.yml"), generateDockerCompose(options));
182
- const ext = options.language === "typescript" ? "ts" : "js";
183
- const dirs = [
184
- "src/core",
185
- "src/config",
186
- "src/modules",
187
- "src/middleware",
188
- "src/utils",
189
- "src/types",
190
- "tests/unit",
191
- "tests/integration"
192
- ];
193
- if (options.database !== "none" && options.database !== "mongodb") {
194
- dirs.push("prisma");
195
- }
196
- for (const dir of dirs) {
197
- await ensureDir(path2.join(projectDir, dir));
159
+ ]);
160
+ const db = answers.database;
161
+ options = {
162
+ ...answers,
163
+ orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
164
+ };
198
165
  }
199
- await writeFile(
200
- path2.join(projectDir, `src/index.${ext}`),
201
- generateEntryFile(options)
202
- );
203
- await writeFile(
204
- path2.join(projectDir, `src/core/server.${ext}`),
205
- generateServerFile(options)
206
- );
207
- await writeFile(
208
- path2.join(projectDir, `src/core/logger.${ext}`),
209
- generateLoggerFile(options)
210
- );
211
- if (options.database !== "none" && options.database !== "mongodb") {
166
+ const projectDir = path2.resolve(process.cwd(), options.name);
167
+ const spinner = ora("Creating project...").start();
168
+ try {
169
+ try {
170
+ await fs2.access(projectDir);
171
+ spinner.stop();
172
+ error(`Directory "${options.name}" already exists`);
173
+ return;
174
+ } catch {
175
+ }
176
+ await ensureDir(projectDir);
177
+ spinner.text = "Generating project files...";
178
+ const packageJson = generatePackageJson(options);
212
179
  await writeFile(
213
- path2.join(projectDir, "prisma/schema.prisma"),
214
- generatePrismaSchema(options)
180
+ path2.join(projectDir, "package.json"),
181
+ JSON.stringify(packageJson, null, 2)
215
182
  );
216
- }
217
- spinner.succeed("Project files generated!");
218
- const installSpinner = ora("Installing dependencies...").start();
219
- try {
220
- execSync("npm install", { cwd: projectDir, stdio: "pipe" });
221
- installSpinner.succeed("Dependencies installed!");
222
- } catch {
223
- installSpinner.warn("Failed to install dependencies automatically");
224
- warn(' Run "npm install" manually in the project directory');
225
- }
226
- console.log("\n" + chalk2.green("\u2728 Project created successfully!"));
227
- console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
228
- console.log(`
183
+ if (options.language === "typescript") {
184
+ await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig());
185
+ await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig());
186
+ } else {
187
+ await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig());
188
+ }
189
+ await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
190
+ await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
191
+ await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
192
+ await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
193
+ await writeFile(
194
+ path2.join(projectDir, "docker-compose.yml"),
195
+ generateDockerCompose(options)
196
+ );
197
+ const ext = options.language === "typescript" ? "ts" : "js";
198
+ const dirs = [
199
+ "src/core",
200
+ "src/config",
201
+ "src/modules",
202
+ "src/middleware",
203
+ "src/utils",
204
+ "src/types",
205
+ "tests/unit",
206
+ "tests/integration"
207
+ ];
208
+ if (options.orm === "prisma") {
209
+ dirs.push("prisma");
210
+ }
211
+ if (options.orm === "mongoose") {
212
+ dirs.push("src/database/models");
213
+ }
214
+ for (const dir of dirs) {
215
+ await ensureDir(path2.join(projectDir, dir));
216
+ }
217
+ await writeFile(path2.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
218
+ await writeFile(
219
+ path2.join(projectDir, `src/core/server.${ext}`),
220
+ generateServerFile(options)
221
+ );
222
+ await writeFile(
223
+ path2.join(projectDir, `src/core/logger.${ext}`),
224
+ generateLoggerFile(options)
225
+ );
226
+ if (options.orm === "prisma") {
227
+ await writeFile(
228
+ path2.join(projectDir, "prisma/schema.prisma"),
229
+ generatePrismaSchema(options)
230
+ );
231
+ } else if (options.orm === "mongoose") {
232
+ await writeFile(
233
+ path2.join(projectDir, `src/database/connection.${ext}`),
234
+ generateMongooseConnection(options)
235
+ );
236
+ await writeFile(
237
+ path2.join(projectDir, `src/database/models/user.model.${ext}`),
238
+ generateMongooseUserModel(options)
239
+ );
240
+ }
241
+ spinner.succeed("Project files generated!");
242
+ const installSpinner = ora("Installing dependencies...").start();
243
+ try {
244
+ execSync("npm install", { cwd: projectDir, stdio: "pipe" });
245
+ installSpinner.succeed("Dependencies installed!");
246
+ } catch {
247
+ installSpinner.warn("Failed to install dependencies automatically");
248
+ warn(' Run "npm install" manually in the project directory');
249
+ }
250
+ console.log("\n" + chalk2.green("\u2728 Project created successfully!"));
251
+ console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
252
+ console.log(`
229
253
  ${options.name}/
230
254
  \u251C\u2500\u2500 src/
231
255
  \u2502 \u251C\u2500\u2500 core/ # Core server, logger
@@ -239,24 +263,25 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
239
263
  \u251C\u2500\u2500 docker-compose.yml
240
264
  \u2514\u2500\u2500 package.json
241
265
  `);
242
- console.log(chalk2.bold("\u{1F680} Get started:"));
243
- console.log(`
266
+ console.log(chalk2.bold("\u{1F680} Get started:"));
267
+ console.log(`
244
268
  ${chalk2.cyan(`cd ${options.name}`)}
245
269
  ${options.database !== "none" ? chalk2.cyan("npm run db:push # Setup database") : ""}
246
270
  ${chalk2.cyan("npm run dev # Start development server")}
247
271
  `);
248
- console.log(chalk2.bold("\u{1F4DA} Available commands:"));
249
- console.log(`
272
+ console.log(chalk2.bold("\u{1F4DA} Available commands:"));
273
+ console.log(`
250
274
  ${chalk2.yellow("servcraft generate module <name>")} Generate a new module
251
275
  ${chalk2.yellow("servcraft generate controller <name>")} Generate a controller
252
276
  ${chalk2.yellow("servcraft generate service <name>")} Generate a service
253
277
  ${chalk2.yellow("servcraft add auth")} Add authentication module
254
278
  `);
255
- } catch (err) {
256
- spinner.fail("Failed to create project");
257
- error(err instanceof Error ? err.message : String(err));
279
+ } catch (err) {
280
+ spinner.fail("Failed to create project");
281
+ error(err instanceof Error ? err.message : String(err));
282
+ }
258
283
  }
259
- });
284
+ );
260
285
  function generatePackageJson(options) {
261
286
  const isTS = options.language === "typescript";
262
287
  const pkg = {
@@ -306,7 +331,7 @@ function generatePackageJson(options) {
306
331
  pkg.devDependencies["@types/node"] = "^22.10.1";
307
332
  pkg.devDependencies["@types/bcryptjs"] = "^2.4.6";
308
333
  }
309
- if (options.database !== "none" && options.database !== "mongodb") {
334
+ if (options.orm === "prisma") {
310
335
  pkg.dependencies["@prisma/client"] = "^5.22.0";
311
336
  pkg.devDependencies.prisma = "^5.22.0";
312
337
  pkg.scripts["db:generate"] = "prisma generate";
@@ -314,8 +339,11 @@ function generatePackageJson(options) {
314
339
  pkg.scripts["db:push"] = "prisma db push";
315
340
  pkg.scripts["db:studio"] = "prisma studio";
316
341
  }
317
- if (options.database === "mongodb") {
342
+ if (options.orm === "mongoose") {
318
343
  pkg.dependencies.mongoose = "^8.8.4";
344
+ if (isTS) {
345
+ pkg.devDependencies["@types/mongoose"] = "^5.11.97";
346
+ }
319
347
  }
320
348
  if (options.features.includes("email")) {
321
349
  pkg.dependencies.nodemailer = "^6.9.15";
@@ -330,37 +358,45 @@ function generatePackageJson(options) {
330
358
  return pkg;
331
359
  }
332
360
  function generateTsConfig() {
333
- return JSON.stringify({
334
- compilerOptions: {
335
- target: "ES2022",
336
- module: "NodeNext",
337
- moduleResolution: "NodeNext",
338
- lib: ["ES2022"],
339
- outDir: "./dist",
340
- rootDir: "./src",
341
- strict: true,
342
- esModuleInterop: true,
343
- skipLibCheck: true,
344
- forceConsistentCasingInFileNames: true,
345
- resolveJsonModule: true,
346
- declaration: true,
347
- sourceMap: true
361
+ return JSON.stringify(
362
+ {
363
+ compilerOptions: {
364
+ target: "ES2022",
365
+ module: "NodeNext",
366
+ moduleResolution: "NodeNext",
367
+ lib: ["ES2022"],
368
+ outDir: "./dist",
369
+ rootDir: "./src",
370
+ strict: true,
371
+ esModuleInterop: true,
372
+ skipLibCheck: true,
373
+ forceConsistentCasingInFileNames: true,
374
+ resolveJsonModule: true,
375
+ declaration: true,
376
+ sourceMap: true
377
+ },
378
+ include: ["src/**/*"],
379
+ exclude: ["node_modules", "dist"]
348
380
  },
349
- include: ["src/**/*"],
350
- exclude: ["node_modules", "dist"]
351
- }, null, 2);
381
+ null,
382
+ 2
383
+ );
352
384
  }
353
385
  function generateJsConfig() {
354
- return JSON.stringify({
355
- compilerOptions: {
356
- module: "NodeNext",
357
- moduleResolution: "NodeNext",
358
- target: "ES2022",
359
- checkJs: true
386
+ return JSON.stringify(
387
+ {
388
+ compilerOptions: {
389
+ module: "NodeNext",
390
+ moduleResolution: "NodeNext",
391
+ target: "ES2022",
392
+ checkJs: true
393
+ },
394
+ include: ["src/**/*"],
395
+ exclude: ["node_modules"]
360
396
  },
361
- include: ["src/**/*"],
362
- exclude: ["node_modules"]
363
- }, null, 2);
397
+ null,
398
+ 2
399
+ );
364
400
  }
365
401
  function generateTsupConfig() {
366
402
  return `import { defineConfig } from 'tsup';
@@ -376,7 +412,7 @@ export default defineConfig({
376
412
  `;
377
413
  }
378
414
  function generateEnvExample(options) {
379
- let env2 = `# Server
415
+ let env = `# Server
380
416
  NODE_ENV=development
381
417
  PORT=3000
382
418
  HOST=0.0.0.0
@@ -394,31 +430,31 @@ RATE_LIMIT_MAX=100
394
430
  LOG_LEVEL=info
395
431
  `;
396
432
  if (options.database === "postgresql") {
397
- env2 += `
433
+ env += `
398
434
  # Database (PostgreSQL)
399
435
  DATABASE_PROVIDER=postgresql
400
436
  DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
401
437
  `;
402
438
  } else if (options.database === "mysql") {
403
- env2 += `
439
+ env += `
404
440
  # Database (MySQL)
405
441
  DATABASE_PROVIDER=mysql
406
442
  DATABASE_URL="mysql://user:password@localhost:3306/mydb"
407
443
  `;
408
444
  } else if (options.database === "sqlite") {
409
- env2 += `
445
+ env += `
410
446
  # Database (SQLite)
411
447
  DATABASE_PROVIDER=sqlite
412
448
  DATABASE_URL="file:./dev.db"
413
449
  `;
414
450
  } else if (options.database === "mongodb") {
415
- env2 += `
451
+ env += `
416
452
  # Database (MongoDB)
417
453
  MONGODB_URI="mongodb://localhost:27017/mydb"
418
454
  `;
419
455
  }
420
456
  if (options.features.includes("email")) {
421
- env2 += `
457
+ env += `
422
458
  # Email
423
459
  SMTP_HOST=smtp.example.com
424
460
  SMTP_PORT=587
@@ -428,12 +464,12 @@ SMTP_FROM="App <noreply@example.com>"
428
464
  `;
429
465
  }
430
466
  if (options.features.includes("redis")) {
431
- env2 += `
467
+ env += `
432
468
  # Redis
433
469
  REDIS_URL=redis://localhost:6379
434
470
  `;
435
471
  }
436
- return env2;
472
+ return env;
437
473
  }
438
474
  function generateGitignore() {
439
475
  return `node_modules/
@@ -505,6 +541,23 @@ volumes:
505
541
 
506
542
  volumes:
507
543
  mysql-data:
544
+ `;
545
+ } else if (options.database === "mongodb") {
546
+ compose += ` - MONGODB_URI=mongodb://mongodb:27017/mydb
547
+ depends_on:
548
+ - mongodb
549
+
550
+ mongodb:
551
+ image: mongo:7
552
+ environment:
553
+ MONGO_INITDB_DATABASE: mydb
554
+ ports:
555
+ - "27017:27017"
556
+ volumes:
557
+ - mongodb-data:/data/db
558
+
559
+ volumes:
560
+ mongodb-data:
508
561
  `;
509
562
  }
510
563
  if (options.features.includes("redis")) {
@@ -616,11 +669,112 @@ ${isTS ? "export const logger: Logger" : "const logger"} = pino({
616
669
  ${isTS ? "" : "module.exports = { logger };"}
617
670
  `;
618
671
  }
672
+ function generateMongooseConnection(options) {
673
+ const isTS = options.language === "typescript";
674
+ return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
675
+
676
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
677
+
678
+ ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "async function connectDatabase()"} {
679
+ try {
680
+ const conn = await mongoose.connect(MONGODB_URI);
681
+ logger.info(\`MongoDB connected: \${conn.connection.host}\`);
682
+ return conn;
683
+ } catch (error) {
684
+ logger.error({ err: error }, 'MongoDB connection failed');
685
+ process.exit(1);
686
+ }
687
+ }
688
+
689
+ ${isTS ? "export async function disconnectDatabase(): Promise<void>" : "async function disconnectDatabase()"} {
690
+ try {
691
+ await mongoose.disconnect();
692
+ logger.info('MongoDB disconnected');
693
+ } catch (error) {
694
+ logger.error({ err: error }, 'MongoDB disconnect failed');
695
+ }
696
+ }
697
+
698
+ ${isTS ? "export { mongoose };" : "module.exports = { connectDatabase, disconnectDatabase, mongoose };"}
699
+ `;
700
+ }
701
+ function generateMongooseUserModel(options) {
702
+ const isTS = options.language === "typescript";
703
+ return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
704
+
705
+ ${isTS ? `export interface IUser extends Document {
706
+ email: string;
707
+ password: string;
708
+ name?: string;
709
+ role: 'user' | 'admin';
710
+ status: 'active' | 'inactive' | 'suspended';
711
+ emailVerified: boolean;
712
+ createdAt: Date;
713
+ updatedAt: Date;
714
+ comparePassword(candidatePassword: string): Promise<boolean>;
715
+ }` : ""}
716
+
717
+ const userSchema = new Schema${isTS ? "<IUser>" : ""}({
718
+ email: {
719
+ type: String,
720
+ required: true,
721
+ unique: true,
722
+ lowercase: true,
723
+ trim: true,
724
+ },
725
+ password: {
726
+ type: String,
727
+ required: true,
728
+ minlength: 8,
729
+ },
730
+ name: {
731
+ type: String,
732
+ trim: true,
733
+ },
734
+ role: {
735
+ type: String,
736
+ enum: ['user', 'admin'],
737
+ default: 'user',
738
+ },
739
+ status: {
740
+ type: String,
741
+ enum: ['active', 'inactive', 'suspended'],
742
+ default: 'active',
743
+ },
744
+ emailVerified: {
745
+ type: Boolean,
746
+ default: false,
747
+ },
748
+ }, {
749
+ timestamps: true,
750
+ });
751
+
752
+ // Hash password before saving
753
+ userSchema.pre('save', async function(next) {
754
+ if (!this.isModified('password')) return next();
755
+
756
+ try {
757
+ const salt = await bcrypt.genSalt(10);
758
+ this.password = await bcrypt.hash(this.password, salt);
759
+ next();
760
+ } catch (error${isTS ? ": any" : ""}) {
761
+ next(error);
762
+ }
763
+ });
764
+
765
+ // Compare password method
766
+ userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
767
+ return bcrypt.compare(candidatePassword, this.password);
768
+ };
769
+
770
+ ${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
771
+ `;
772
+ }
619
773
 
620
774
  // src/cli/commands/generate.ts
621
775
  import { Command as Command2 } from "commander";
622
- import path4 from "path";
623
- import ora3 from "ora";
776
+ import path3 from "path";
777
+ import ora2 from "ora";
624
778
  import inquirer2 from "inquirer";
625
779
 
626
780
  // src/cli/utils/field-parser.ts
@@ -1058,23 +1212,11 @@ export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
1058
1212
  }
1059
1213
 
1060
1214
  // src/cli/templates/routes.ts
1061
- function routesTemplate(name, pascalName, camelName, pluralName, fields = []) {
1062
- const serializedFields = JSON.stringify(fields, null, 2);
1215
+ function routesTemplate(name, pascalName, camelName, pluralName) {
1063
1216
  return `import type { FastifyInstance } from 'fastify';
1064
1217
  import type { ${pascalName}Controller } from './${name}.controller.js';
1065
1218
  import type { AuthService } from '../auth/auth.service.js';
1066
1219
  import { createAuthMiddleware, createRoleMiddleware } from '../auth/auth.middleware.js';
1067
- import { generateRouteSchema } from '../swagger/schema-builder.js';
1068
- import type { FieldDefinition } from '../cli/utils/field-parser.js';
1069
-
1070
- const ${camelName}Fields: FieldDefinition[] = ${serializedFields};
1071
- const ${camelName}Schemas = {
1072
- list: generateRouteSchema('${pascalName}', ${camelName}Fields, 'list'),
1073
- get: generateRouteSchema('${pascalName}', ${camelName}Fields, 'get'),
1074
- create: generateRouteSchema('${pascalName}', ${camelName}Fields, 'create'),
1075
- update: generateRouteSchema('${pascalName}', ${camelName}Fields, 'update'),
1076
- delete: generateRouteSchema('${pascalName}', ${camelName}Fields, 'delete'),
1077
- };
1078
1220
 
1079
1221
  export function register${pascalName}Routes(
1080
1222
  app: FastifyInstance,
@@ -1090,31 +1232,31 @@ export function register${pascalName}Routes(
1090
1232
  // Protected routes
1091
1233
  app.get(
1092
1234
  '/${pluralName}',
1093
- { preHandler: [authenticate], ...${camelName}Schemas.list },
1235
+ { preHandler: [authenticate] },
1094
1236
  controller.list.bind(controller)
1095
1237
  );
1096
1238
 
1097
1239
  app.get(
1098
1240
  '/${pluralName}/:id',
1099
- { preHandler: [authenticate], ...${camelName}Schemas.get },
1241
+ { preHandler: [authenticate] },
1100
1242
  controller.getById.bind(controller)
1101
1243
  );
1102
1244
 
1103
1245
  app.post(
1104
1246
  '/${pluralName}',
1105
- { preHandler: [authenticate], ...${camelName}Schemas.create },
1247
+ { preHandler: [authenticate] },
1106
1248
  controller.create.bind(controller)
1107
1249
  );
1108
1250
 
1109
1251
  app.patch(
1110
1252
  '/${pluralName}/:id',
1111
- { preHandler: [authenticate], ...${camelName}Schemas.update },
1253
+ { preHandler: [authenticate] },
1112
1254
  controller.update.bind(controller)
1113
1255
  );
1114
1256
 
1115
1257
  app.delete(
1116
1258
  '/${pluralName}/:id',
1117
- { preHandler: [authenticate, isAdmin], ...${camelName}Schemas.delete },
1259
+ { preHandler: [authenticate, isAdmin] },
1118
1260
  controller.delete.bind(controller)
1119
1261
  );
1120
1262
  }
@@ -1465,1735 +1607,74 @@ ${indexLines.join("\n")}
1465
1607
  `;
1466
1608
  }
1467
1609
 
1468
- // src/cli/utils/docs-generator.ts
1469
- import fs3 from "fs/promises";
1470
- import path3 from "path";
1471
- import ora2 from "ora";
1472
-
1473
- // src/core/server.ts
1474
- import Fastify from "fastify";
1475
-
1476
- // src/core/logger.ts
1477
- import pino from "pino";
1478
- var defaultConfig = {
1479
- level: process.env.LOG_LEVEL || "info",
1480
- pretty: process.env.NODE_ENV !== "production",
1481
- name: "servcraft"
1482
- };
1483
- function createLogger(config2 = {}) {
1484
- const mergedConfig = { ...defaultConfig, ...config2 };
1485
- const transport = mergedConfig.pretty ? {
1486
- target: "pino-pretty",
1487
- options: {
1488
- colorize: true,
1489
- translateTime: "SYS:standard",
1490
- ignore: "pid,hostname"
1491
- }
1492
- } : void 0;
1493
- return pino({
1494
- name: mergedConfig.name,
1495
- level: mergedConfig.level,
1496
- transport,
1497
- formatters: {
1498
- level: (label) => ({ level: label })
1499
- },
1500
- timestamp: pino.stdTimeFunctions.isoTime
1501
- });
1502
- }
1503
- var logger = createLogger();
1504
-
1505
- // src/core/server.ts
1506
- var defaultConfig2 = {
1507
- port: parseInt(process.env.PORT || "3000", 10),
1508
- host: process.env.HOST || "0.0.0.0",
1509
- trustProxy: true,
1510
- bodyLimit: 1048576,
1511
- // 1MB
1512
- requestTimeout: 3e4
1513
- // 30s
1514
- };
1515
- var Server = class {
1516
- app;
1517
- config;
1518
- logger;
1519
- isShuttingDown = false;
1520
- constructor(config2 = {}) {
1521
- this.config = { ...defaultConfig2, ...config2 };
1522
- this.logger = this.config.logger || logger;
1523
- const fastifyOptions = {
1524
- logger: this.logger,
1525
- trustProxy: this.config.trustProxy,
1526
- bodyLimit: this.config.bodyLimit,
1527
- requestTimeout: this.config.requestTimeout
1528
- };
1529
- this.app = Fastify(fastifyOptions);
1530
- this.setupHealthCheck();
1531
- this.setupGracefulShutdown();
1532
- }
1533
- get instance() {
1534
- return this.app;
1535
- }
1536
- setupHealthCheck() {
1537
- this.app.get("/health", async (_request, reply) => {
1538
- const healthcheck = {
1539
- status: "ok",
1540
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1541
- uptime: process.uptime(),
1542
- memory: process.memoryUsage(),
1543
- version: process.env.npm_package_version || "0.1.0"
1544
- };
1545
- return reply.status(200).send(healthcheck);
1546
- });
1547
- this.app.get("/ready", async (_request, reply) => {
1548
- if (this.isShuttingDown) {
1549
- return reply.status(503).send({ status: "shutting_down" });
1550
- }
1551
- return reply.status(200).send({ status: "ready" });
1552
- });
1553
- }
1554
- setupGracefulShutdown() {
1555
- const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
1556
- signals.forEach((signal) => {
1557
- process.on(signal, async () => {
1558
- this.logger.info(`Received ${signal}, starting graceful shutdown...`);
1559
- await this.shutdown();
1560
- });
1561
- });
1562
- process.on("uncaughtException", async (error2) => {
1563
- this.logger.error({ err: error2 }, "Uncaught exception");
1564
- await this.shutdown(1);
1565
- });
1566
- process.on("unhandledRejection", async (reason) => {
1567
- this.logger.error({ err: reason }, "Unhandled rejection");
1568
- await this.shutdown(1);
1569
- });
1610
+ // src/cli/commands/generate.ts
1611
+ var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
1612
+ generateCommand.command("module <name> [fields...]").alias("m").description(
1613
+ "Generate a complete module with controller, service, repository, types, schemas, and routes"
1614
+ ).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
1615
+ let fields = [];
1616
+ if (options.interactive) {
1617
+ fields = await promptForFields();
1618
+ } else if (fieldsArgs.length > 0) {
1619
+ fields = parseFields(fieldsArgs.join(" "));
1570
1620
  }
1571
- async shutdown(exitCode = 0) {
1572
- if (this.isShuttingDown) {
1621
+ const spinner = ora2("Generating module...").start();
1622
+ try {
1623
+ const kebabName = toKebabCase(name);
1624
+ const pascalName = toPascalCase(name);
1625
+ const camelName = toCamelCase(name);
1626
+ const pluralName = pluralize(kebabName);
1627
+ const tableName = pluralize(kebabName.replace(/-/g, "_"));
1628
+ const validatorType = options.validator || "zod";
1629
+ const moduleDir = path3.join(getModulesDir(), kebabName);
1630
+ if (await fileExists(moduleDir)) {
1631
+ spinner.stop();
1632
+ error(`Module "${kebabName}" already exists`);
1573
1633
  return;
1574
1634
  }
1575
- this.isShuttingDown = true;
1576
- this.logger.info("Graceful shutdown initiated...");
1577
- const shutdownTimeout = setTimeout(() => {
1578
- this.logger.error("Graceful shutdown timeout, forcing exit");
1579
- process.exit(1);
1580
- }, 3e4);
1581
- try {
1582
- await this.app.close();
1583
- this.logger.info("Server closed successfully");
1584
- clearTimeout(shutdownTimeout);
1585
- process.exit(exitCode);
1586
- } catch (error2) {
1587
- this.logger.error({ err: error2 }, "Error during shutdown");
1588
- clearTimeout(shutdownTimeout);
1589
- process.exit(1);
1635
+ const hasFields = fields.length > 0;
1636
+ const files = [
1637
+ {
1638
+ name: `${kebabName}.types.ts`,
1639
+ content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) : typesTemplate(kebabName, pascalName)
1640
+ },
1641
+ {
1642
+ name: `${kebabName}.schemas.ts`,
1643
+ content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) : schemasTemplate(kebabName, pascalName, camelName)
1644
+ },
1645
+ {
1646
+ name: `${kebabName}.service.ts`,
1647
+ content: serviceTemplate(kebabName, pascalName, camelName)
1648
+ },
1649
+ {
1650
+ name: `${kebabName}.controller.ts`,
1651
+ content: controllerTemplate(kebabName, pascalName, camelName)
1652
+ },
1653
+ { name: "index.ts", content: moduleIndexTemplate(kebabName, pascalName, camelName) }
1654
+ ];
1655
+ if (options.repository !== false) {
1656
+ files.push({
1657
+ name: `${kebabName}.repository.ts`,
1658
+ content: repositoryTemplate(kebabName, pascalName, camelName, pluralName)
1659
+ });
1590
1660
  }
1591
- }
1592
- async start() {
1593
- try {
1594
- await this.app.listen({
1595
- port: this.config.port,
1596
- host: this.config.host
1661
+ if (options.routes !== false) {
1662
+ files.push({
1663
+ name: `${kebabName}.routes.ts`,
1664
+ content: routesTemplate(kebabName, pascalName, camelName, pluralName)
1597
1665
  });
1598
- this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
1599
- } catch (error2) {
1600
- this.logger.error({ err: error2 }, "Failed to start server");
1601
- throw error2;
1602
1666
  }
1603
- }
1604
- };
1605
- function createServer(config2 = {}) {
1606
- return new Server(config2);
1607
- }
1608
-
1609
- // src/utils/errors.ts
1610
- var AppError = class _AppError extends Error {
1611
- statusCode;
1612
- isOperational;
1613
- errors;
1614
- constructor(message, statusCode = 500, isOperational = true, errors) {
1615
- super(message);
1616
- this.statusCode = statusCode;
1617
- this.isOperational = isOperational;
1618
- this.errors = errors;
1619
- Object.setPrototypeOf(this, _AppError.prototype);
1620
- Error.captureStackTrace(this, this.constructor);
1621
- }
1622
- };
1623
- var NotFoundError = class extends AppError {
1624
- constructor(resource = "Resource") {
1625
- super(`${resource} not found`, 404);
1626
- }
1627
- };
1628
- var UnauthorizedError = class extends AppError {
1629
- constructor(message = "Unauthorized") {
1630
- super(message, 401);
1631
- }
1632
- };
1633
- var ForbiddenError = class extends AppError {
1634
- constructor(message = "Forbidden") {
1635
- super(message, 403);
1636
- }
1637
- };
1638
- var BadRequestError = class extends AppError {
1639
- constructor(message = "Bad request", errors) {
1640
- super(message, 400, true, errors);
1641
- }
1642
- };
1643
- var ConflictError = class extends AppError {
1644
- constructor(message = "Resource already exists") {
1645
- super(message, 409);
1646
- }
1647
- };
1648
- var ValidationError = class extends AppError {
1649
- constructor(errors) {
1650
- super("Validation failed", 422, true, errors);
1651
- }
1652
- };
1653
- function isAppError(error2) {
1654
- return error2 instanceof AppError;
1655
- }
1656
-
1657
- // src/config/env.ts
1658
- import { z } from "zod";
1659
- import dotenv from "dotenv";
1660
- dotenv.config();
1661
- var envSchema = z.object({
1662
- // Server
1663
- NODE_ENV: z.enum(["development", "staging", "production", "test"]).default("development"),
1664
- PORT: z.string().transform(Number).default("3000"),
1665
- HOST: z.string().default("0.0.0.0"),
1666
- // Database
1667
- DATABASE_URL: z.string().optional(),
1668
- // JWT
1669
- JWT_SECRET: z.string().min(32).optional(),
1670
- JWT_ACCESS_EXPIRES_IN: z.string().default("15m"),
1671
- JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),
1672
- // Security
1673
- CORS_ORIGIN: z.string().default("*"),
1674
- RATE_LIMIT_MAX: z.string().transform(Number).default("100"),
1675
- RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default("60000"),
1676
- // Email
1677
- SMTP_HOST: z.string().optional(),
1678
- SMTP_PORT: z.string().transform(Number).optional(),
1679
- SMTP_USER: z.string().optional(),
1680
- SMTP_PASS: z.string().optional(),
1681
- SMTP_FROM: z.string().optional(),
1682
- // Redis (optional)
1683
- REDIS_URL: z.string().optional(),
1684
- // Swagger/OpenAPI
1685
- SWAGGER_ENABLED: z.union([z.literal("true"), z.literal("false")]).default("true").transform((val) => val === "true"),
1686
- SWAGGER_ROUTE: z.string().default("/docs"),
1687
- SWAGGER_TITLE: z.string().default("Servcraft API"),
1688
- SWAGGER_DESCRIPTION: z.string().default("API documentation"),
1689
- SWAGGER_VERSION: z.string().default("1.0.0"),
1690
- // Logging
1691
- LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
1692
- });
1693
- function validateEnv() {
1694
- const parsed = envSchema.safeParse(process.env);
1695
- if (!parsed.success) {
1696
- logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
1697
- throw new Error("Invalid environment variables");
1698
- }
1699
- return parsed.data;
1700
- }
1701
- var env = validateEnv();
1702
- function isProduction() {
1703
- return env.NODE_ENV === "production";
1704
- }
1705
-
1706
- // src/config/index.ts
1707
- function parseCorsOrigin(origin) {
1708
- if (origin === "*") return "*";
1709
- if (origin.includes(",")) {
1710
- return origin.split(",").map((o) => o.trim());
1711
- }
1712
- return origin;
1713
- }
1714
- function createConfig() {
1715
- return {
1716
- env,
1717
- server: {
1718
- port: env.PORT,
1719
- host: env.HOST
1720
- },
1721
- jwt: {
1722
- secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
1723
- accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
1724
- refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
1725
- },
1726
- security: {
1727
- corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
1728
- rateLimit: {
1729
- max: env.RATE_LIMIT_MAX,
1730
- windowMs: env.RATE_LIMIT_WINDOW_MS
1731
- }
1732
- },
1733
- email: {
1734
- host: env.SMTP_HOST,
1735
- port: env.SMTP_PORT,
1736
- user: env.SMTP_USER,
1737
- pass: env.SMTP_PASS,
1738
- from: env.SMTP_FROM
1739
- },
1740
- database: {
1741
- url: env.DATABASE_URL
1742
- },
1743
- redis: {
1744
- url: env.REDIS_URL
1745
- },
1746
- swagger: {
1747
- enabled: env.SWAGGER_ENABLED,
1748
- route: env.SWAGGER_ROUTE,
1749
- title: env.SWAGGER_TITLE,
1750
- description: env.SWAGGER_DESCRIPTION,
1751
- version: env.SWAGGER_VERSION
1667
+ for (const file of files) {
1668
+ await writeFile(path3.join(moduleDir, file.name), file.content);
1752
1669
  }
1753
- };
1754
- }
1755
- var config = createConfig();
1756
-
1757
- // src/middleware/error-handler.ts
1758
- function registerErrorHandler(app) {
1759
- app.setErrorHandler(
1760
- (error2, request, reply) => {
1761
- logger.error(
1762
- {
1763
- err: error2,
1764
- requestId: request.id,
1765
- method: request.method,
1766
- url: request.url
1767
- },
1768
- "Request error"
1769
- );
1770
- if (isAppError(error2)) {
1771
- return reply.status(error2.statusCode).send({
1772
- success: false,
1773
- message: error2.message,
1774
- errors: error2.errors,
1775
- ...isProduction() ? {} : { stack: error2.stack }
1776
- });
1777
- }
1778
- if ("validation" in error2 && error2.validation) {
1779
- const errors = {};
1780
- for (const err of error2.validation) {
1781
- const field = err.instancePath?.replace("/", "") || "body";
1782
- if (!errors[field]) {
1783
- errors[field] = [];
1784
- }
1785
- errors[field].push(err.message || "Invalid value");
1786
- }
1787
- return reply.status(400).send({
1788
- success: false,
1789
- message: "Validation failed",
1790
- errors
1791
- });
1792
- }
1793
- if ("statusCode" in error2 && typeof error2.statusCode === "number") {
1794
- return reply.status(error2.statusCode).send({
1795
- success: false,
1796
- message: error2.message,
1797
- ...isProduction() ? {} : { stack: error2.stack }
1798
- });
1799
- }
1800
- return reply.status(500).send({
1801
- success: false,
1802
- message: isProduction() ? "Internal server error" : error2.message,
1803
- ...isProduction() ? {} : { stack: error2.stack }
1804
- });
1805
- }
1806
- );
1807
- app.setNotFoundHandler((request, reply) => {
1808
- return reply.status(404).send({
1809
- success: false,
1810
- message: `Route ${request.method} ${request.url} not found`
1811
- });
1812
- });
1813
- }
1814
-
1815
- // src/middleware/security.ts
1816
- import helmet from "@fastify/helmet";
1817
- import cors from "@fastify/cors";
1818
- import rateLimit from "@fastify/rate-limit";
1819
- var defaultOptions = {
1820
- helmet: true,
1821
- cors: true,
1822
- rateLimit: true
1823
- };
1824
- async function registerSecurity(app, options = {}) {
1825
- const opts = { ...defaultOptions, ...options };
1826
- if (opts.helmet) {
1827
- await app.register(helmet, {
1828
- contentSecurityPolicy: {
1829
- directives: {
1830
- defaultSrc: ["'self'"],
1831
- styleSrc: ["'self'", "'unsafe-inline'"],
1832
- scriptSrc: ["'self'"],
1833
- imgSrc: ["'self'", "data:", "https:"]
1834
- }
1835
- },
1836
- crossOriginEmbedderPolicy: false
1837
- });
1838
- logger.debug("Helmet security headers enabled");
1839
- }
1840
- if (opts.cors) {
1841
- await app.register(cors, {
1842
- origin: config.security.corsOrigin,
1843
- credentials: true,
1844
- methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
1845
- allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
1846
- exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
1847
- maxAge: 86400
1848
- // 24 hours
1849
- });
1850
- logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
1851
- }
1852
- if (opts.rateLimit) {
1853
- await app.register(rateLimit, {
1854
- max: config.security.rateLimit.max,
1855
- timeWindow: config.security.rateLimit.windowMs,
1856
- errorResponseBuilder: (_request, context) => ({
1857
- success: false,
1858
- message: "Too many requests, please try again later",
1859
- retryAfter: context.after
1860
- }),
1861
- keyGenerator: (request) => {
1862
- return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
1863
- }
1864
- });
1865
- logger.debug(
1866
- {
1867
- max: config.security.rateLimit.max,
1868
- windowMs: config.security.rateLimit.windowMs
1869
- },
1870
- "Rate limiting enabled"
1871
- );
1872
- }
1873
- }
1874
-
1875
- // src/modules/swagger/swagger.service.ts
1876
- import swagger from "@fastify/swagger";
1877
- import swaggerUi from "@fastify/swagger-ui";
1878
- var defaultConfig3 = {
1879
- enabled: true,
1880
- route: "/docs",
1881
- title: "Servcraft API",
1882
- description: "API documentation generated by Servcraft",
1883
- version: "1.0.0",
1884
- tags: [
1885
- { name: "Auth", description: "Authentication endpoints" },
1886
- { name: "Users", description: "User management endpoints" },
1887
- { name: "Health", description: "Health check endpoints" }
1888
- ]
1889
- };
1890
- async function registerSwagger(app, customConfig) {
1891
- const swaggerConfig = { ...defaultConfig3, ...customConfig };
1892
- if (swaggerConfig.enabled === false) {
1893
- logger.info("Swagger documentation disabled");
1894
- return;
1895
- }
1896
- await app.register(swagger, {
1897
- openapi: {
1898
- openapi: "3.0.3",
1899
- info: {
1900
- title: swaggerConfig.title,
1901
- description: swaggerConfig.description,
1902
- version: swaggerConfig.version,
1903
- contact: swaggerConfig.contact,
1904
- license: swaggerConfig.license
1905
- },
1906
- servers: swaggerConfig.servers || [
1907
- {
1908
- url: `http://localhost:${config.server.port}`,
1909
- description: "Development server"
1910
- }
1911
- ],
1912
- tags: swaggerConfig.tags,
1913
- components: {
1914
- securitySchemes: {
1915
- bearerAuth: {
1916
- type: "http",
1917
- scheme: "bearer",
1918
- bearerFormat: "JWT",
1919
- description: "Enter your JWT token"
1920
- }
1921
- }
1922
- }
1923
- }
1924
- });
1925
- await app.register(swaggerUi, {
1926
- routePrefix: swaggerConfig.route || "/docs",
1927
- uiConfig: {
1928
- docExpansion: "list",
1929
- deepLinking: true,
1930
- displayRequestDuration: true,
1931
- filter: true,
1932
- showExtensions: true,
1933
- showCommonExtensions: true
1934
- },
1935
- staticCSP: true,
1936
- transformStaticCSP: (header) => header
1937
- });
1938
- logger.info("Swagger documentation registered at /docs");
1939
- }
1940
- var commonResponses = {
1941
- success: {
1942
- type: "object",
1943
- properties: {
1944
- success: { type: "boolean", example: true },
1945
- data: { type: "object" }
1946
- }
1947
- },
1948
- error: {
1949
- type: "object",
1950
- properties: {
1951
- success: { type: "boolean", example: false },
1952
- message: { type: "string" },
1953
- errors: {
1954
- type: "object",
1955
- additionalProperties: {
1956
- type: "array",
1957
- items: { type: "string" }
1958
- }
1959
- }
1960
- }
1961
- },
1962
- unauthorized: {
1963
- type: "object",
1964
- properties: {
1965
- success: { type: "boolean", example: false },
1966
- message: { type: "string", example: "Unauthorized" }
1967
- }
1968
- },
1969
- notFound: {
1970
- type: "object",
1971
- properties: {
1972
- success: { type: "boolean", example: false },
1973
- message: { type: "string", example: "Resource not found" }
1974
- }
1975
- },
1976
- paginated: {
1977
- type: "object",
1978
- properties: {
1979
- success: { type: "boolean", example: true },
1980
- data: {
1981
- type: "object",
1982
- properties: {
1983
- data: { type: "array", items: { type: "object" } },
1984
- meta: {
1985
- type: "object",
1986
- properties: {
1987
- total: { type: "number" },
1988
- page: { type: "number" },
1989
- limit: { type: "number" },
1990
- totalPages: { type: "number" },
1991
- hasNextPage: { type: "boolean" },
1992
- hasPrevPage: { type: "boolean" }
1993
- }
1994
- }
1995
- }
1996
- }
1997
- }
1998
- }
1999
- };
2000
- var paginationQuery = {
2001
- type: "object",
2002
- properties: {
2003
- page: { type: "integer", minimum: 1, default: 1, description: "Page number" },
2004
- limit: { type: "integer", minimum: 1, maximum: 100, default: 20, description: "Items per page" },
2005
- sortBy: { type: "string", description: "Field to sort by" },
2006
- sortOrder: { type: "string", enum: ["asc", "desc"], default: "asc", description: "Sort order" },
2007
- search: { type: "string", description: "Search query" }
2008
- }
2009
- };
2010
- var idParam = {
2011
- type: "object",
2012
- properties: {
2013
- id: { type: "string", format: "uuid", description: "Resource ID" }
2014
- },
2015
- required: ["id"]
2016
- };
2017
-
2018
- // src/modules/auth/index.ts
2019
- import jwt from "@fastify/jwt";
2020
- import cookie from "@fastify/cookie";
2021
-
2022
- // src/modules/auth/auth.service.ts
2023
- import bcrypt from "bcryptjs";
2024
- var tokenBlacklist = /* @__PURE__ */ new Set();
2025
- var AuthService = class {
2026
- app;
2027
- SALT_ROUNDS = 12;
2028
- constructor(app) {
2029
- this.app = app;
2030
- }
2031
- async hashPassword(password) {
2032
- return bcrypt.hash(password, this.SALT_ROUNDS);
2033
- }
2034
- async verifyPassword(password, hash) {
2035
- return bcrypt.compare(password, hash);
2036
- }
2037
- generateTokenPair(user) {
2038
- const accessPayload = {
2039
- sub: user.id,
2040
- email: user.email,
2041
- role: user.role,
2042
- type: "access"
2043
- };
2044
- const refreshPayload = {
2045
- sub: user.id,
2046
- email: user.email,
2047
- role: user.role,
2048
- type: "refresh"
2049
- };
2050
- const accessToken = this.app.jwt.sign(accessPayload, {
2051
- expiresIn: config.jwt.accessExpiresIn
2052
- });
2053
- const refreshToken = this.app.jwt.sign(refreshPayload, {
2054
- expiresIn: config.jwt.refreshExpiresIn
2055
- });
2056
- const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
2057
- return { accessToken, refreshToken, expiresIn };
2058
- }
2059
- parseExpiration(expiration) {
2060
- const match = expiration.match(/^(\d+)([smhd])$/);
2061
- if (!match) return 900;
2062
- const value = parseInt(match[1] || "0", 10);
2063
- const unit = match[2];
2064
- switch (unit) {
2065
- case "s":
2066
- return value;
2067
- case "m":
2068
- return value * 60;
2069
- case "h":
2070
- return value * 3600;
2071
- case "d":
2072
- return value * 86400;
2073
- default:
2074
- return 900;
2075
- }
2076
- }
2077
- async verifyAccessToken(token) {
2078
- try {
2079
- if (this.isTokenBlacklisted(token)) {
2080
- throw new UnauthorizedError("Token has been revoked");
2081
- }
2082
- const payload = this.app.jwt.verify(token);
2083
- if (payload.type !== "access") {
2084
- throw new UnauthorizedError("Invalid token type");
2085
- }
2086
- return payload;
2087
- } catch (error2) {
2088
- if (error2 instanceof UnauthorizedError) throw error2;
2089
- logger.debug({ err: error2 }, "Token verification failed");
2090
- throw new UnauthorizedError("Invalid or expired token");
2091
- }
2092
- }
2093
- async verifyRefreshToken(token) {
2094
- try {
2095
- if (this.isTokenBlacklisted(token)) {
2096
- throw new UnauthorizedError("Token has been revoked");
2097
- }
2098
- const payload = this.app.jwt.verify(token);
2099
- if (payload.type !== "refresh") {
2100
- throw new UnauthorizedError("Invalid token type");
2101
- }
2102
- return payload;
2103
- } catch (error2) {
2104
- if (error2 instanceof UnauthorizedError) throw error2;
2105
- logger.debug({ err: error2 }, "Refresh token verification failed");
2106
- throw new UnauthorizedError("Invalid or expired refresh token");
2107
- }
2108
- }
2109
- blacklistToken(token) {
2110
- tokenBlacklist.add(token);
2111
- logger.debug("Token blacklisted");
2112
- }
2113
- isTokenBlacklisted(token) {
2114
- return tokenBlacklist.has(token);
2115
- }
2116
- // Clear expired tokens from blacklist periodically
2117
- cleanupBlacklist() {
2118
- tokenBlacklist.clear();
2119
- logger.debug("Token blacklist cleared");
2120
- }
2121
- };
2122
- function createAuthService(app) {
2123
- return new AuthService(app);
2124
- }
2125
-
2126
- // src/modules/auth/schemas.ts
2127
- import { z as z2 } from "zod";
2128
- var loginSchema = z2.object({
2129
- email: z2.string().email("Invalid email address"),
2130
- password: z2.string().min(1, "Password is required")
2131
- });
2132
- var registerSchema = z2.object({
2133
- email: z2.string().email("Invalid email address"),
2134
- password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
2135
- name: z2.string().min(2, "Name must be at least 2 characters").optional()
2136
- });
2137
- var refreshTokenSchema = z2.object({
2138
- refreshToken: z2.string().min(1, "Refresh token is required")
2139
- });
2140
- var passwordResetRequestSchema = z2.object({
2141
- email: z2.string().email("Invalid email address")
2142
- });
2143
- var passwordResetConfirmSchema = z2.object({
2144
- token: z2.string().min(1, "Token is required"),
2145
- password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
2146
- });
2147
- var changePasswordSchema = z2.object({
2148
- currentPassword: z2.string().min(1, "Current password is required"),
2149
- newPassword: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
2150
- });
2151
-
2152
- // src/utils/response.ts
2153
- function success3(reply, data, statusCode = 200) {
2154
- const response = {
2155
- success: true,
2156
- data
2157
- };
2158
- return reply.status(statusCode).send(response);
2159
- }
2160
- function created(reply, data) {
2161
- return success3(reply, data, 201);
2162
- }
2163
- function noContent(reply) {
2164
- return reply.status(204).send();
2165
- }
2166
-
2167
- // src/modules/validation/validator.ts
2168
- import { z as z3 } from "zod";
2169
- function validateBody(schema, data) {
2170
- const result = schema.safeParse(data);
2171
- if (!result.success) {
2172
- throw new ValidationError(formatZodErrors(result.error));
2173
- }
2174
- return result.data;
2175
- }
2176
- function validateQuery(schema, data) {
2177
- const result = schema.safeParse(data);
2178
- if (!result.success) {
2179
- throw new ValidationError(formatZodErrors(result.error));
2180
- }
2181
- return result.data;
2182
- }
2183
- function formatZodErrors(error2) {
2184
- const errors = {};
2185
- for (const issue of error2.issues) {
2186
- const path6 = issue.path.join(".") || "root";
2187
- if (!errors[path6]) {
2188
- errors[path6] = [];
2189
- }
2190
- errors[path6].push(issue.message);
2191
- }
2192
- return errors;
2193
- }
2194
- var idParamSchema = z3.object({
2195
- id: z3.string().uuid("Invalid ID format")
2196
- });
2197
- var paginationSchema = z3.object({
2198
- page: z3.string().transform(Number).optional().default("1"),
2199
- limit: z3.string().transform(Number).optional().default("20"),
2200
- sortBy: z3.string().optional(),
2201
- sortOrder: z3.enum(["asc", "desc"]).optional().default("asc")
2202
- });
2203
- var searchSchema = z3.object({
2204
- q: z3.string().min(1, "Search query is required").optional(),
2205
- search: z3.string().min(1).optional()
2206
- });
2207
- var emailSchema = z3.string().email("Invalid email address");
2208
- var passwordSchema = z3.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
2209
- var urlSchema = z3.string().url("Invalid URL format");
2210
- var phoneSchema = z3.string().regex(
2211
- /^\+?[1-9]\d{1,14}$/,
2212
- "Invalid phone number format"
2213
- );
2214
- var dateSchema = z3.coerce.date();
2215
- var futureDateSchema = z3.coerce.date().refine(
2216
- (date) => date > /* @__PURE__ */ new Date(),
2217
- "Date must be in the future"
2218
- );
2219
- var pastDateSchema = z3.coerce.date().refine(
2220
- (date) => date < /* @__PURE__ */ new Date(),
2221
- "Date must be in the past"
2222
- );
2223
-
2224
- // src/modules/auth/auth.controller.ts
2225
- var AuthController = class {
2226
- constructor(authService, userService) {
2227
- this.authService = authService;
2228
- this.userService = userService;
2229
- }
2230
- async register(request, reply) {
2231
- const data = validateBody(registerSchema, request.body);
2232
- const existingUser = await this.userService.findByEmail(data.email);
2233
- if (existingUser) {
2234
- throw new BadRequestError("Email already registered");
2235
- }
2236
- const hashedPassword = await this.authService.hashPassword(data.password);
2237
- const user = await this.userService.create({
2238
- email: data.email,
2239
- password: hashedPassword,
2240
- name: data.name
2241
- });
2242
- const tokens = this.authService.generateTokenPair({
2243
- id: user.id,
2244
- email: user.email,
2245
- role: user.role
2246
- });
2247
- created(reply, {
2248
- user: {
2249
- id: user.id,
2250
- email: user.email,
2251
- name: user.name,
2252
- role: user.role
2253
- },
2254
- ...tokens
2255
- });
2256
- }
2257
- async login(request, reply) {
2258
- const data = validateBody(loginSchema, request.body);
2259
- const user = await this.userService.findByEmail(data.email);
2260
- if (!user) {
2261
- throw new UnauthorizedError("Invalid credentials");
2262
- }
2263
- if (user.status !== "active") {
2264
- throw new UnauthorizedError("Account is not active");
2265
- }
2266
- const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
2267
- if (!isValidPassword) {
2268
- throw new UnauthorizedError("Invalid credentials");
2269
- }
2270
- await this.userService.updateLastLogin(user.id);
2271
- const tokens = this.authService.generateTokenPair({
2272
- id: user.id,
2273
- email: user.email,
2274
- role: user.role
2275
- });
2276
- success3(reply, {
2277
- user: {
2278
- id: user.id,
2279
- email: user.email,
2280
- name: user.name,
2281
- role: user.role
2282
- },
2283
- ...tokens
2284
- });
2285
- }
2286
- async refresh(request, reply) {
2287
- const data = validateBody(refreshTokenSchema, request.body);
2288
- const payload = await this.authService.verifyRefreshToken(data.refreshToken);
2289
- const user = await this.userService.findById(payload.sub);
2290
- if (!user || user.status !== "active") {
2291
- throw new UnauthorizedError("User not found or inactive");
2292
- }
2293
- this.authService.blacklistToken(data.refreshToken);
2294
- const tokens = this.authService.generateTokenPair({
2295
- id: user.id,
2296
- email: user.email,
2297
- role: user.role
2298
- });
2299
- success3(reply, tokens);
2300
- }
2301
- async logout(request, reply) {
2302
- const authHeader = request.headers.authorization;
2303
- if (authHeader?.startsWith("Bearer ")) {
2304
- const token = authHeader.substring(7);
2305
- this.authService.blacklistToken(token);
2306
- }
2307
- success3(reply, { message: "Logged out successfully" });
2308
- }
2309
- async me(request, reply) {
2310
- const authRequest = request;
2311
- const user = await this.userService.findById(authRequest.user.id);
2312
- if (!user) {
2313
- throw new UnauthorizedError("User not found");
2314
- }
2315
- success3(reply, {
2316
- id: user.id,
2317
- email: user.email,
2318
- name: user.name,
2319
- role: user.role,
2320
- status: user.status,
2321
- createdAt: user.createdAt
2322
- });
2323
- }
2324
- async changePassword(request, reply) {
2325
- const authRequest = request;
2326
- const data = validateBody(changePasswordSchema, request.body);
2327
- const user = await this.userService.findById(authRequest.user.id);
2328
- if (!user) {
2329
- throw new UnauthorizedError("User not found");
2330
- }
2331
- const isValidPassword = await this.authService.verifyPassword(
2332
- data.currentPassword,
2333
- user.password
2334
- );
2335
- if (!isValidPassword) {
2336
- throw new BadRequestError("Current password is incorrect");
2337
- }
2338
- const hashedPassword = await this.authService.hashPassword(data.newPassword);
2339
- await this.userService.updatePassword(user.id, hashedPassword);
2340
- success3(reply, { message: "Password changed successfully" });
2341
- }
2342
- };
2343
- function createAuthController(authService, userService) {
2344
- return new AuthController(authService, userService);
2345
- }
2346
-
2347
- // src/modules/auth/auth.middleware.ts
2348
- function createAuthMiddleware(authService) {
2349
- return async function authenticate(request, reply) {
2350
- const authHeader = request.headers.authorization;
2351
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
2352
- throw new UnauthorizedError("Missing or invalid authorization header");
2353
- }
2354
- const token = authHeader.substring(7);
2355
- const payload = await authService.verifyAccessToken(token);
2356
- request.user = {
2357
- id: payload.sub,
2358
- email: payload.email,
2359
- role: payload.role
2360
- };
2361
- };
2362
- }
2363
- function createRoleMiddleware(allowedRoles) {
2364
- return async function authorize(request, _reply) {
2365
- const user = request.user;
2366
- if (!user) {
2367
- throw new UnauthorizedError("Authentication required");
2368
- }
2369
- if (!allowedRoles.includes(user.role)) {
2370
- throw new ForbiddenError("Insufficient permissions");
2371
- }
2372
- };
2373
- }
2374
-
2375
- // src/modules/auth/auth.routes.ts
2376
- var credentialsBody = {
2377
- type: "object",
2378
- required: ["email", "password"],
2379
- properties: {
2380
- email: { type: "string", format: "email" },
2381
- password: { type: "string", minLength: 8 }
2382
- }
2383
- };
2384
- var changePasswordBody = {
2385
- type: "object",
2386
- required: ["currentPassword", "newPassword"],
2387
- properties: {
2388
- currentPassword: { type: "string", minLength: 8 },
2389
- newPassword: { type: "string", minLength: 8 }
2390
- }
2391
- };
2392
- function registerAuthRoutes(app, controller, authService) {
2393
- const authenticate = createAuthMiddleware(authService);
2394
- app.post("/auth/register", {
2395
- schema: {
2396
- tags: ["Auth"],
2397
- summary: "Register a new user",
2398
- body: credentialsBody,
2399
- response: {
2400
- 201: commonResponses.success,
2401
- 400: commonResponses.error,
2402
- 409: commonResponses.error
2403
- }
2404
- },
2405
- handler: controller.register.bind(controller)
2406
- });
2407
- app.post("/auth/login", {
2408
- schema: {
2409
- tags: ["Auth"],
2410
- summary: "Login and obtain tokens",
2411
- body: credentialsBody,
2412
- response: {
2413
- 200: commonResponses.success,
2414
- 400: commonResponses.error,
2415
- 401: commonResponses.unauthorized
2416
- }
2417
- },
2418
- handler: controller.login.bind(controller)
2419
- });
2420
- app.post("/auth/refresh", {
2421
- schema: {
2422
- tags: ["Auth"],
2423
- summary: "Refresh access token",
2424
- body: {
2425
- type: "object",
2426
- required: ["refreshToken"],
2427
- properties: {
2428
- refreshToken: { type: "string" }
2429
- }
2430
- },
2431
- response: {
2432
- 200: commonResponses.success,
2433
- 401: commonResponses.unauthorized
2434
- }
2435
- },
2436
- handler: controller.refresh.bind(controller)
2437
- });
2438
- app.post("/auth/logout", {
2439
- preHandler: [authenticate],
2440
- schema: {
2441
- tags: ["Auth"],
2442
- summary: "Logout current user",
2443
- security: [{ bearerAuth: [] }],
2444
- response: {
2445
- 200: commonResponses.success,
2446
- 401: commonResponses.unauthorized
2447
- }
2448
- },
2449
- handler: controller.logout.bind(controller)
2450
- });
2451
- app.get("/auth/me", {
2452
- preHandler: [authenticate],
2453
- schema: {
2454
- tags: ["Auth"],
2455
- summary: "Get current user profile",
2456
- security: [{ bearerAuth: [] }],
2457
- response: {
2458
- 200: commonResponses.success,
2459
- 401: commonResponses.unauthorized
2460
- }
2461
- },
2462
- handler: controller.me.bind(controller)
2463
- });
2464
- app.post("/auth/change-password", {
2465
- preHandler: [authenticate],
2466
- schema: {
2467
- tags: ["Auth"],
2468
- summary: "Change current user password",
2469
- security: [{ bearerAuth: [] }],
2470
- body: changePasswordBody,
2471
- response: {
2472
- 200: commonResponses.success,
2473
- 400: commonResponses.error,
2474
- 401: commonResponses.unauthorized
2475
- }
2476
- },
2477
- handler: controller.changePassword.bind(controller)
2478
- });
2479
- }
2480
-
2481
- // src/modules/user/user.repository.ts
2482
- import { randomUUID } from "crypto";
2483
-
2484
- // src/utils/pagination.ts
2485
- var DEFAULT_PAGE = 1;
2486
- var DEFAULT_LIMIT = 20;
2487
- var MAX_LIMIT = 100;
2488
- function parsePaginationParams(query) {
2489
- const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
2490
- const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
2491
- const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
2492
- const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
2493
- return { page, limit, sortBy, sortOrder };
2494
- }
2495
- function createPaginatedResult(data, total, params) {
2496
- const totalPages = Math.ceil(total / params.limit);
2497
- return {
2498
- data,
2499
- meta: {
2500
- total,
2501
- page: params.page,
2502
- limit: params.limit,
2503
- totalPages,
2504
- hasNextPage: params.page < totalPages,
2505
- hasPrevPage: params.page > 1
2506
- }
2507
- };
2508
- }
2509
- function getSkip(params) {
2510
- return (params.page - 1) * params.limit;
2511
- }
2512
-
2513
- // src/modules/user/user.repository.ts
2514
- var users = /* @__PURE__ */ new Map();
2515
- var UserRepository = class {
2516
- async findById(id) {
2517
- return users.get(id) || null;
2518
- }
2519
- async findByEmail(email) {
2520
- for (const user of users.values()) {
2521
- if (user.email.toLowerCase() === email.toLowerCase()) {
2522
- return user;
2523
- }
2524
- }
2525
- return null;
2526
- }
2527
- async findMany(params, filters) {
2528
- let filteredUsers = Array.from(users.values());
2529
- if (filters) {
2530
- if (filters.status) {
2531
- filteredUsers = filteredUsers.filter((u) => u.status === filters.status);
2532
- }
2533
- if (filters.role) {
2534
- filteredUsers = filteredUsers.filter((u) => u.role === filters.role);
2535
- }
2536
- if (filters.emailVerified !== void 0) {
2537
- filteredUsers = filteredUsers.filter((u) => u.emailVerified === filters.emailVerified);
2538
- }
2539
- if (filters.search) {
2540
- const search = filters.search.toLowerCase();
2541
- filteredUsers = filteredUsers.filter(
2542
- (u) => u.email.toLowerCase().includes(search) || u.name?.toLowerCase().includes(search)
2543
- );
2544
- }
2545
- }
2546
- if (params.sortBy) {
2547
- const sortKey = params.sortBy;
2548
- filteredUsers.sort((a, b) => {
2549
- const aVal = a[sortKey];
2550
- const bVal = b[sortKey];
2551
- if (aVal === void 0 || bVal === void 0) return 0;
2552
- if (aVal < bVal) return params.sortOrder === "desc" ? 1 : -1;
2553
- if (aVal > bVal) return params.sortOrder === "desc" ? -1 : 1;
2554
- return 0;
2555
- });
2556
- }
2557
- const total = filteredUsers.length;
2558
- const skip = getSkip(params);
2559
- const data = filteredUsers.slice(skip, skip + params.limit);
2560
- return createPaginatedResult(data, total, params);
2561
- }
2562
- async create(data) {
2563
- const now = /* @__PURE__ */ new Date();
2564
- const user = {
2565
- id: randomUUID(),
2566
- email: data.email,
2567
- password: data.password,
2568
- name: data.name,
2569
- role: data.role || "user",
2570
- status: "active",
2571
- emailVerified: false,
2572
- createdAt: now,
2573
- updatedAt: now
2574
- };
2575
- users.set(user.id, user);
2576
- return user;
2577
- }
2578
- async update(id, data) {
2579
- const user = users.get(id);
2580
- if (!user) return null;
2581
- const updatedUser = {
2582
- ...user,
2583
- ...data,
2584
- updatedAt: /* @__PURE__ */ new Date()
2585
- };
2586
- users.set(id, updatedUser);
2587
- return updatedUser;
2588
- }
2589
- async updatePassword(id, password) {
2590
- const user = users.get(id);
2591
- if (!user) return null;
2592
- const updatedUser = {
2593
- ...user,
2594
- password,
2595
- updatedAt: /* @__PURE__ */ new Date()
2596
- };
2597
- users.set(id, updatedUser);
2598
- return updatedUser;
2599
- }
2600
- async updateLastLogin(id) {
2601
- const user = users.get(id);
2602
- if (!user) return null;
2603
- const updatedUser = {
2604
- ...user,
2605
- lastLoginAt: /* @__PURE__ */ new Date(),
2606
- updatedAt: /* @__PURE__ */ new Date()
2607
- };
2608
- users.set(id, updatedUser);
2609
- return updatedUser;
2610
- }
2611
- async delete(id) {
2612
- return users.delete(id);
2613
- }
2614
- async count(filters) {
2615
- let count = 0;
2616
- for (const user of users.values()) {
2617
- if (filters) {
2618
- if (filters.status && user.status !== filters.status) continue;
2619
- if (filters.role && user.role !== filters.role) continue;
2620
- if (filters.emailVerified !== void 0 && user.emailVerified !== filters.emailVerified)
2621
- continue;
2622
- }
2623
- count++;
2624
- }
2625
- return count;
2626
- }
2627
- // Helper to clear all users (for testing)
2628
- async clear() {
2629
- users.clear();
2630
- }
2631
- };
2632
- function createUserRepository() {
2633
- return new UserRepository();
2634
- }
2635
-
2636
- // src/modules/user/types.ts
2637
- var DEFAULT_ROLE_PERMISSIONS = {
2638
- user: ["profile:read", "profile:update"],
2639
- moderator: [
2640
- "profile:read",
2641
- "profile:update",
2642
- "users:read",
2643
- "content:read",
2644
- "content:update",
2645
- "content:delete"
2646
- ],
2647
- admin: [
2648
- "profile:read",
2649
- "profile:update",
2650
- "users:read",
2651
- "users:update",
2652
- "users:delete",
2653
- "content:manage",
2654
- "settings:read"
2655
- ],
2656
- super_admin: ["*:manage"]
2657
- // All permissions
2658
- };
2659
-
2660
- // src/modules/user/user.service.ts
2661
- var UserService = class {
2662
- constructor(repository) {
2663
- this.repository = repository;
2664
- }
2665
- async findById(id) {
2666
- return this.repository.findById(id);
2667
- }
2668
- async findByEmail(email) {
2669
- return this.repository.findByEmail(email);
2670
- }
2671
- async findMany(params, filters) {
2672
- const result = await this.repository.findMany(params, filters);
2673
- return {
2674
- ...result,
2675
- data: result.data.map(({ password, ...user }) => user)
2676
- };
2677
- }
2678
- async create(data) {
2679
- const existing = await this.repository.findByEmail(data.email);
2680
- if (existing) {
2681
- throw new ConflictError("User with this email already exists");
2682
- }
2683
- const user = await this.repository.create(data);
2684
- logger.info({ userId: user.id, email: user.email }, "User created");
2685
- return user;
2686
- }
2687
- async update(id, data) {
2688
- const user = await this.repository.findById(id);
2689
- if (!user) {
2690
- throw new NotFoundError("User");
2691
- }
2692
- if (data.email && data.email !== user.email) {
2693
- const existing = await this.repository.findByEmail(data.email);
2694
- if (existing) {
2695
- throw new ConflictError("Email already in use");
2696
- }
2697
- }
2698
- const updatedUser = await this.repository.update(id, data);
2699
- if (!updatedUser) {
2700
- throw new NotFoundError("User");
2701
- }
2702
- logger.info({ userId: id }, "User updated");
2703
- return updatedUser;
2704
- }
2705
- async updatePassword(id, hashedPassword) {
2706
- const user = await this.repository.updatePassword(id, hashedPassword);
2707
- if (!user) {
2708
- throw new NotFoundError("User");
2709
- }
2710
- logger.info({ userId: id }, "User password updated");
2711
- return user;
2712
- }
2713
- async updateLastLogin(id) {
2714
- const user = await this.repository.updateLastLogin(id);
2715
- if (!user) {
2716
- throw new NotFoundError("User");
2717
- }
2718
- return user;
2719
- }
2720
- async delete(id) {
2721
- const user = await this.repository.findById(id);
2722
- if (!user) {
2723
- throw new NotFoundError("User");
2724
- }
2725
- await this.repository.delete(id);
2726
- logger.info({ userId: id }, "User deleted");
2727
- }
2728
- async suspend(id) {
2729
- return this.update(id, { status: "suspended" });
2730
- }
2731
- async ban(id) {
2732
- return this.update(id, { status: "banned" });
2733
- }
2734
- async activate(id) {
2735
- return this.update(id, { status: "active" });
2736
- }
2737
- async verifyEmail(id) {
2738
- return this.update(id, { emailVerified: true });
2739
- }
2740
- async changeRole(id, role) {
2741
- return this.update(id, { role });
2742
- }
2743
- // RBAC helpers
2744
- hasPermission(role, permission) {
2745
- const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
2746
- if (permissions.includes("*:manage")) {
2747
- return true;
2748
- }
2749
- if (permissions.includes(permission)) {
2750
- return true;
2751
- }
2752
- const [resource, action] = permission.split(":");
2753
- const managePermission = `${resource}:manage`;
2754
- if (permissions.includes(managePermission)) {
2755
- return true;
2756
- }
2757
- return false;
2758
- }
2759
- getPermissions(role) {
2760
- return DEFAULT_ROLE_PERMISSIONS[role] || [];
2761
- }
2762
- };
2763
- function createUserService(repository) {
2764
- return new UserService(repository || createUserRepository());
2765
- }
2766
-
2767
- // src/modules/auth/index.ts
2768
- async function registerAuthModule(app) {
2769
- await app.register(jwt, {
2770
- secret: config.jwt.secret,
2771
- sign: {
2772
- algorithm: "HS256"
2773
- }
2774
- });
2775
- await app.register(cookie, {
2776
- secret: config.jwt.secret,
2777
- hook: "onRequest"
2778
- });
2779
- const authService = createAuthService(app);
2780
- const userService = createUserService();
2781
- const authController = createAuthController(authService, userService);
2782
- registerAuthRoutes(app, authController, authService);
2783
- logger.info("Auth module registered");
2784
- return authService;
2785
- }
2786
-
2787
- // src/modules/user/schemas.ts
2788
- import { z as z4 } from "zod";
2789
- var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
2790
- var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
2791
- var createUserSchema = z4.object({
2792
- email: z4.string().email("Invalid email address"),
2793
- password: z4.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
2794
- name: z4.string().min(2, "Name must be at least 2 characters").optional(),
2795
- role: userRoleEnum.optional().default("user")
2796
- });
2797
- var updateUserSchema = z4.object({
2798
- email: z4.string().email("Invalid email address").optional(),
2799
- name: z4.string().min(2, "Name must be at least 2 characters").optional(),
2800
- role: userRoleEnum.optional(),
2801
- status: userStatusEnum.optional(),
2802
- emailVerified: z4.boolean().optional(),
2803
- metadata: z4.record(z4.unknown()).optional()
2804
- });
2805
- var updateProfileSchema = z4.object({
2806
- name: z4.string().min(2, "Name must be at least 2 characters").optional(),
2807
- metadata: z4.record(z4.unknown()).optional()
2808
- });
2809
- var userQuerySchema = z4.object({
2810
- page: z4.string().transform(Number).optional(),
2811
- limit: z4.string().transform(Number).optional(),
2812
- sortBy: z4.string().optional(),
2813
- sortOrder: z4.enum(["asc", "desc"]).optional(),
2814
- status: userStatusEnum.optional(),
2815
- role: userRoleEnum.optional(),
2816
- search: z4.string().optional(),
2817
- emailVerified: z4.string().transform((val) => val === "true").optional()
2818
- });
2819
-
2820
- // src/modules/user/user.controller.ts
2821
- var UserController = class {
2822
- constructor(userService) {
2823
- this.userService = userService;
2824
- }
2825
- async list(request, reply) {
2826
- const query = validateQuery(userQuerySchema, request.query);
2827
- const pagination = parsePaginationParams(query);
2828
- const filters = {
2829
- status: query.status,
2830
- role: query.role,
2831
- search: query.search,
2832
- emailVerified: query.emailVerified
2833
- };
2834
- const result = await this.userService.findMany(pagination, filters);
2835
- success3(reply, result);
2836
- }
2837
- async getById(request, reply) {
2838
- const user = await this.userService.findById(request.params.id);
2839
- if (!user) {
2840
- return reply.status(404).send({
2841
- success: false,
2842
- message: "User not found"
2843
- });
2844
- }
2845
- const { password, ...userData } = user;
2846
- success3(reply, userData);
2847
- }
2848
- async update(request, reply) {
2849
- const data = validateBody(updateUserSchema, request.body);
2850
- const user = await this.userService.update(request.params.id, data);
2851
- const { password, ...userData } = user;
2852
- success3(reply, userData);
2853
- }
2854
- async delete(request, reply) {
2855
- const authRequest = request;
2856
- if (authRequest.user.id === request.params.id) {
2857
- throw new ForbiddenError("Cannot delete your own account");
2858
- }
2859
- await this.userService.delete(request.params.id);
2860
- noContent(reply);
2861
- }
2862
- async suspend(request, reply) {
2863
- const authRequest = request;
2864
- if (authRequest.user.id === request.params.id) {
2865
- throw new ForbiddenError("Cannot suspend your own account");
2866
- }
2867
- const user = await this.userService.suspend(request.params.id);
2868
- const { password, ...userData } = user;
2869
- success3(reply, userData);
2870
- }
2871
- async ban(request, reply) {
2872
- const authRequest = request;
2873
- if (authRequest.user.id === request.params.id) {
2874
- throw new ForbiddenError("Cannot ban your own account");
2875
- }
2876
- const user = await this.userService.ban(request.params.id);
2877
- const { password, ...userData } = user;
2878
- success3(reply, userData);
2879
- }
2880
- async activate(request, reply) {
2881
- const user = await this.userService.activate(request.params.id);
2882
- const { password, ...userData } = user;
2883
- success3(reply, userData);
2884
- }
2885
- // Profile routes (for authenticated user)
2886
- async getProfile(request, reply) {
2887
- const authRequest = request;
2888
- const user = await this.userService.findById(authRequest.user.id);
2889
- if (!user) {
2890
- return reply.status(404).send({
2891
- success: false,
2892
- message: "User not found"
2893
- });
2894
- }
2895
- const { password, ...userData } = user;
2896
- success3(reply, userData);
2897
- }
2898
- async updateProfile(request, reply) {
2899
- const authRequest = request;
2900
- const data = validateBody(updateProfileSchema, request.body);
2901
- const user = await this.userService.update(authRequest.user.id, data);
2902
- const { password, ...userData } = user;
2903
- success3(reply, userData);
2904
- }
2905
- };
2906
- function createUserController(userService) {
2907
- return new UserController(userService);
2908
- }
2909
-
2910
- // src/modules/user/user.routes.ts
2911
- var userTag = "Users";
2912
- var userResponse = {
2913
- type: "object",
2914
- properties: {
2915
- success: { type: "boolean", example: true },
2916
- data: { type: "object" }
2917
- }
2918
- };
2919
- function registerUserRoutes(app, controller, authService) {
2920
- const authenticate = createAuthMiddleware(authService);
2921
- const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
2922
- const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
2923
- app.get(
2924
- "/profile",
2925
- {
2926
- preHandler: [authenticate],
2927
- schema: {
2928
- tags: [userTag],
2929
- summary: "Get current user profile",
2930
- security: [{ bearerAuth: [] }],
2931
- response: {
2932
- 200: userResponse,
2933
- 401: commonResponses.unauthorized
2934
- }
2935
- }
2936
- },
2937
- controller.getProfile.bind(controller)
2938
- );
2939
- app.patch(
2940
- "/profile",
2941
- {
2942
- preHandler: [authenticate],
2943
- schema: {
2944
- tags: [userTag],
2945
- summary: "Update current user profile",
2946
- security: [{ bearerAuth: [] }],
2947
- body: { type: "object" },
2948
- response: {
2949
- 200: userResponse,
2950
- 401: commonResponses.unauthorized,
2951
- 400: commonResponses.error
2952
- }
2953
- }
2954
- },
2955
- controller.updateProfile.bind(controller)
2956
- );
2957
- app.get(
2958
- "/users",
2959
- {
2960
- preHandler: [authenticate, isModerator],
2961
- schema: {
2962
- tags: [userTag],
2963
- summary: "List users",
2964
- security: [{ bearerAuth: [] }],
2965
- querystring: {
2966
- ...paginationQuery,
2967
- properties: {
2968
- ...paginationQuery.properties,
2969
- status: { type: "string", enum: ["active", "inactive", "suspended", "banned"] },
2970
- role: { type: "string", enum: ["user", "admin", "moderator", "super_admin"] },
2971
- search: { type: "string" },
2972
- emailVerified: { type: "boolean" }
2973
- }
2974
- },
2975
- response: {
2976
- 200: commonResponses.paginated,
2977
- 401: commonResponses.unauthorized
2978
- }
2979
- }
2980
- },
2981
- controller.list.bind(controller)
2982
- );
2983
- app.get(
2984
- "/users/:id",
2985
- {
2986
- preHandler: [authenticate, isModerator],
2987
- schema: {
2988
- tags: [userTag],
2989
- summary: "Get user by id",
2990
- security: [{ bearerAuth: [] }],
2991
- params: idParam,
2992
- response: {
2993
- 200: userResponse,
2994
- 401: commonResponses.unauthorized,
2995
- 404: commonResponses.notFound
2996
- }
2997
- }
2998
- },
2999
- controller.getById.bind(controller)
3000
- );
3001
- app.patch(
3002
- "/users/:id",
3003
- {
3004
- preHandler: [authenticate, isAdmin],
3005
- schema: {
3006
- tags: [userTag],
3007
- summary: "Update user",
3008
- security: [{ bearerAuth: [] }],
3009
- params: idParam,
3010
- body: { type: "object" },
3011
- response: {
3012
- 200: userResponse,
3013
- 401: commonResponses.unauthorized,
3014
- 404: commonResponses.notFound
3015
- }
3016
- }
3017
- },
3018
- controller.update.bind(controller)
3019
- );
3020
- app.delete(
3021
- "/users/:id",
3022
- {
3023
- preHandler: [authenticate, isAdmin],
3024
- schema: {
3025
- tags: [userTag],
3026
- summary: "Delete user",
3027
- security: [{ bearerAuth: [] }],
3028
- params: idParam,
3029
- response: {
3030
- 204: { description: "User deleted" },
3031
- 401: commonResponses.unauthorized,
3032
- 404: commonResponses.notFound
3033
- }
3034
- }
3035
- },
3036
- controller.delete.bind(controller)
3037
- );
3038
- app.post(
3039
- "/users/:id/suspend",
3040
- {
3041
- preHandler: [authenticate, isAdmin],
3042
- schema: {
3043
- tags: [userTag],
3044
- summary: "Suspend user",
3045
- security: [{ bearerAuth: [] }],
3046
- params: idParam,
3047
- response: {
3048
- 200: userResponse,
3049
- 401: commonResponses.unauthorized,
3050
- 404: commonResponses.notFound
3051
- }
3052
- }
3053
- },
3054
- controller.suspend.bind(controller)
3055
- );
3056
- app.post(
3057
- "/users/:id/ban",
3058
- {
3059
- preHandler: [authenticate, isAdmin],
3060
- schema: {
3061
- tags: [userTag],
3062
- summary: "Ban user",
3063
- security: [{ bearerAuth: [] }],
3064
- params: idParam,
3065
- response: {
3066
- 200: userResponse,
3067
- 401: commonResponses.unauthorized,
3068
- 404: commonResponses.notFound
3069
- }
3070
- }
3071
- },
3072
- controller.ban.bind(controller)
3073
- );
3074
- app.post(
3075
- "/users/:id/activate",
3076
- {
3077
- preHandler: [authenticate, isAdmin],
3078
- schema: {
3079
- tags: [userTag],
3080
- summary: "Activate user",
3081
- security: [{ bearerAuth: [] }],
3082
- params: idParam,
3083
- response: {
3084
- 200: userResponse,
3085
- 401: commonResponses.unauthorized,
3086
- 404: commonResponses.notFound
3087
- }
3088
- }
3089
- },
3090
- controller.activate.bind(controller)
3091
- );
3092
- }
3093
-
3094
- // src/modules/user/index.ts
3095
- async function registerUserModule(app, authService) {
3096
- const repository = createUserRepository();
3097
- const userService = createUserService(repository);
3098
- const userController = createUserController(userService);
3099
- registerUserRoutes(app, userController, authService);
3100
- logger.info("User module registered");
3101
- }
3102
-
3103
- // src/cli/utils/docs-generator.ts
3104
- async function generateDocs(outputPath = "openapi.json", silent = false) {
3105
- const spinner = silent ? null : ora2("Generating OpenAPI documentation...").start();
3106
- try {
3107
- const server = createServer({
3108
- port: config.server.port,
3109
- host: config.server.host
3110
- });
3111
- const app = server.instance;
3112
- registerErrorHandler(app);
3113
- await registerSecurity(app);
3114
- await registerSwagger(app, {
3115
- enabled: true,
3116
- route: config.swagger.route,
3117
- title: config.swagger.title,
3118
- description: config.swagger.description,
3119
- version: config.swagger.version
3120
- });
3121
- const authService = await registerAuthModule(app);
3122
- await registerUserModule(app, authService);
3123
- await app.ready();
3124
- const spec = app.swagger();
3125
- const absoluteOutput = path3.resolve(outputPath);
3126
- await fs3.mkdir(path3.dirname(absoluteOutput), { recursive: true });
3127
- await fs3.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
3128
- spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
3129
- await app.close();
3130
- return absoluteOutput;
3131
- } catch (error2) {
3132
- spinner?.fail("Failed to generate OpenAPI documentation");
3133
- throw error2;
3134
- }
3135
- }
3136
-
3137
- // src/cli/commands/generate.ts
3138
- var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
3139
- generateCommand.command("module <name> [fields...]").alias("m").description("Generate a complete module with controller, service, repository, types, schemas, and routes").option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
3140
- let fields = [];
3141
- if (options.interactive) {
3142
- fields = await promptForFields();
3143
- } else if (fieldsArgs.length > 0) {
3144
- fields = parseFields(fieldsArgs.join(" "));
3145
- }
3146
- const spinner = ora3("Generating module...").start();
3147
- try {
3148
- const kebabName = toKebabCase(name);
3149
- const pascalName = toPascalCase(name);
3150
- const camelName = toCamelCase(name);
3151
- const pluralName = pluralize(kebabName);
3152
- const tableName = pluralize(kebabName.replace(/-/g, "_"));
3153
- const validatorType = options.validator || "zod";
3154
- const moduleDir = path4.join(getModulesDir(), kebabName);
3155
- if (await fileExists(moduleDir)) {
3156
- spinner.stop();
3157
- error(`Module "${kebabName}" already exists`);
3158
- return;
3159
- }
3160
- const hasFields = fields.length > 0;
3161
- const files = [
3162
- {
3163
- name: `${kebabName}.types.ts`,
3164
- content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) : typesTemplate(kebabName, pascalName)
3165
- },
3166
- {
3167
- name: `${kebabName}.schemas.ts`,
3168
- content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) : schemasTemplate(kebabName, pascalName, camelName)
3169
- },
3170
- { name: `${kebabName}.service.ts`, content: serviceTemplate(kebabName, pascalName, camelName) },
3171
- { name: `${kebabName}.controller.ts`, content: controllerTemplate(kebabName, pascalName, camelName) },
3172
- { name: "index.ts", content: moduleIndexTemplate(kebabName, pascalName, camelName) }
3173
- ];
3174
- if (options.repository !== false) {
3175
- files.push({
3176
- name: `${kebabName}.repository.ts`,
3177
- content: repositoryTemplate(kebabName, pascalName, camelName, pluralName)
3178
- });
3179
- }
3180
- if (options.routes !== false) {
3181
- files.push({
3182
- name: `${kebabName}.routes.ts`,
3183
- content: routesTemplate(kebabName, pascalName, camelName, pluralName, fields)
3184
- });
3185
- }
3186
- for (const file of files) {
3187
- await writeFile(path4.join(moduleDir, file.name), file.content);
3188
- }
3189
- spinner.succeed(`Module "${pascalName}" generated successfully!`);
3190
- if (options.prisma || hasFields) {
3191
- console.log("\n" + "\u2500".repeat(50));
3192
- info("Prisma model suggestion:");
3193
- if (hasFields) {
3194
- console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
3195
- } else {
3196
- console.log(prismaModelTemplate(kebabName, pascalName, tableName));
1670
+ spinner.succeed(`Module "${pascalName}" generated successfully!`);
1671
+ if (options.prisma || hasFields) {
1672
+ console.log("\n" + "\u2500".repeat(50));
1673
+ info("Prisma model suggestion:");
1674
+ if (hasFields) {
1675
+ console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
1676
+ } else {
1677
+ console.log(prismaModelTemplate(kebabName, pascalName, tableName));
3197
1678
  }
3198
1679
  }
3199
1680
  if (hasFields) {
@@ -3222,31 +1703,20 @@ generateCommand.command("module <name> [fields...]").alias("m").description("Gen
3222
1703
  info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
3223
1704
  info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
3224
1705
  }
3225
- const { generateDocsNow } = await inquirer2.prompt([
3226
- {
3227
- type: "confirm",
3228
- name: "generateDocsNow",
3229
- message: "Generate Swagger/OpenAPI documentation now?",
3230
- default: true
3231
- }
3232
- ]);
3233
- if (generateDocsNow) {
3234
- await generateDocs("openapi.json", true);
3235
- }
3236
1706
  } catch (err) {
3237
1707
  spinner.fail("Failed to generate module");
3238
1708
  error(err instanceof Error ? err.message : String(err));
3239
1709
  }
3240
1710
  });
3241
1711
  generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3242
- const spinner = ora3("Generating controller...").start();
1712
+ const spinner = ora2("Generating controller...").start();
3243
1713
  try {
3244
1714
  const kebabName = toKebabCase(name);
3245
1715
  const pascalName = toPascalCase(name);
3246
1716
  const camelName = toCamelCase(name);
3247
1717
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3248
- const moduleDir = path4.join(getModulesDir(), moduleName);
3249
- const filePath = path4.join(moduleDir, `${kebabName}.controller.ts`);
1718
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1719
+ const filePath = path3.join(moduleDir, `${kebabName}.controller.ts`);
3250
1720
  if (await fileExists(filePath)) {
3251
1721
  spinner.stop();
3252
1722
  error(`Controller "${kebabName}" already exists`);
@@ -3261,14 +1731,14 @@ generateCommand.command("controller <name>").alias("c").description("Generate a
3261
1731
  }
3262
1732
  });
3263
1733
  generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3264
- const spinner = ora3("Generating service...").start();
1734
+ const spinner = ora2("Generating service...").start();
3265
1735
  try {
3266
1736
  const kebabName = toKebabCase(name);
3267
1737
  const pascalName = toPascalCase(name);
3268
1738
  const camelName = toCamelCase(name);
3269
1739
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3270
- const moduleDir = path4.join(getModulesDir(), moduleName);
3271
- const filePath = path4.join(moduleDir, `${kebabName}.service.ts`);
1740
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1741
+ const filePath = path3.join(moduleDir, `${kebabName}.service.ts`);
3272
1742
  if (await fileExists(filePath)) {
3273
1743
  spinner.stop();
3274
1744
  error(`Service "${kebabName}" already exists`);
@@ -3283,15 +1753,15 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
3283
1753
  }
3284
1754
  });
3285
1755
  generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3286
- const spinner = ora3("Generating repository...").start();
1756
+ const spinner = ora2("Generating repository...").start();
3287
1757
  try {
3288
1758
  const kebabName = toKebabCase(name);
3289
1759
  const pascalName = toPascalCase(name);
3290
1760
  const camelName = toCamelCase(name);
3291
1761
  const pluralName = pluralize(kebabName);
3292
1762
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3293
- const moduleDir = path4.join(getModulesDir(), moduleName);
3294
- const filePath = path4.join(moduleDir, `${kebabName}.repository.ts`);
1763
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1764
+ const filePath = path3.join(moduleDir, `${kebabName}.repository.ts`);
3295
1765
  if (await fileExists(filePath)) {
3296
1766
  spinner.stop();
3297
1767
  error(`Repository "${kebabName}" already exists`);
@@ -3306,157 +1776,1261 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
3306
1776
  }
3307
1777
  });
3308
1778
  generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3309
- const spinner = ora3("Generating types...").start();
1779
+ const spinner = ora2("Generating types...").start();
3310
1780
  try {
3311
1781
  const kebabName = toKebabCase(name);
3312
1782
  const pascalName = toPascalCase(name);
3313
1783
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3314
- const moduleDir = path4.join(getModulesDir(), moduleName);
3315
- const filePath = path4.join(moduleDir, `${kebabName}.types.ts`);
1784
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1785
+ const filePath = path3.join(moduleDir, `${kebabName}.types.ts`);
3316
1786
  if (await fileExists(filePath)) {
3317
1787
  spinner.stop();
3318
1788
  error(`Types file "${kebabName}.types.ts" already exists`);
3319
1789
  return;
3320
1790
  }
3321
- await writeFile(filePath, typesTemplate(kebabName, pascalName));
3322
- spinner.succeed(`Types for "${pascalName}" generated!`);
3323
- success(` src/modules/${moduleName}/${kebabName}.types.ts`);
3324
- } catch (err) {
3325
- spinner.fail("Failed to generate types");
3326
- error(err instanceof Error ? err.message : String(err));
1791
+ await writeFile(filePath, typesTemplate(kebabName, pascalName));
1792
+ spinner.succeed(`Types for "${pascalName}" generated!`);
1793
+ success(` src/modules/${moduleName}/${kebabName}.types.ts`);
1794
+ } catch (err) {
1795
+ spinner.fail("Failed to generate types");
1796
+ error(err instanceof Error ? err.message : String(err));
1797
+ }
1798
+ });
1799
+ generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
1800
+ const spinner = ora2("Generating schemas...").start();
1801
+ try {
1802
+ const kebabName = toKebabCase(name);
1803
+ const pascalName = toPascalCase(name);
1804
+ const camelName = toCamelCase(name);
1805
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1806
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1807
+ const filePath = path3.join(moduleDir, `${kebabName}.schemas.ts`);
1808
+ if (await fileExists(filePath)) {
1809
+ spinner.stop();
1810
+ error(`Schemas file "${kebabName}.schemas.ts" already exists`);
1811
+ return;
1812
+ }
1813
+ await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
1814
+ spinner.succeed(`Schemas for "${pascalName}" generated!`);
1815
+ success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
1816
+ } catch (err) {
1817
+ spinner.fail("Failed to generate schemas");
1818
+ error(err instanceof Error ? err.message : String(err));
1819
+ }
1820
+ });
1821
+ generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
1822
+ const spinner = ora2("Generating routes...").start();
1823
+ try {
1824
+ const kebabName = toKebabCase(name);
1825
+ const pascalName = toPascalCase(name);
1826
+ const camelName = toCamelCase(name);
1827
+ const pluralName = pluralize(kebabName);
1828
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1829
+ const moduleDir = path3.join(getModulesDir(), moduleName);
1830
+ const filePath = path3.join(moduleDir, `${kebabName}.routes.ts`);
1831
+ if (await fileExists(filePath)) {
1832
+ spinner.stop();
1833
+ error(`Routes file "${kebabName}.routes.ts" already exists`);
1834
+ return;
1835
+ }
1836
+ await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
1837
+ spinner.succeed(`Routes for "${pascalName}" generated!`);
1838
+ success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
1839
+ } catch (err) {
1840
+ spinner.fail("Failed to generate routes");
1841
+ error(err instanceof Error ? err.message : String(err));
1842
+ }
1843
+ });
1844
+ async function promptForFields() {
1845
+ const fields = [];
1846
+ console.log("\n\u{1F4DD} Define your model fields (press Enter with empty name to finish)\n");
1847
+ const fieldTypes = [
1848
+ "string",
1849
+ "number",
1850
+ "boolean",
1851
+ "date",
1852
+ "datetime",
1853
+ "text",
1854
+ "email",
1855
+ "url",
1856
+ "uuid",
1857
+ "int",
1858
+ "float",
1859
+ "decimal",
1860
+ "json"
1861
+ ];
1862
+ let addMore = true;
1863
+ while (addMore) {
1864
+ const answers = await inquirer2.prompt([
1865
+ {
1866
+ type: "input",
1867
+ name: "name",
1868
+ message: "Field name (empty to finish):"
1869
+ }
1870
+ ]);
1871
+ if (!answers.name) {
1872
+ addMore = false;
1873
+ continue;
1874
+ }
1875
+ const fieldDetails = await inquirer2.prompt([
1876
+ {
1877
+ type: "list",
1878
+ name: "type",
1879
+ message: `Type for "${answers.name}":`,
1880
+ choices: fieldTypes,
1881
+ default: "string"
1882
+ },
1883
+ {
1884
+ type: "confirm",
1885
+ name: "isOptional",
1886
+ message: "Is optional?",
1887
+ default: false
1888
+ },
1889
+ {
1890
+ type: "confirm",
1891
+ name: "isUnique",
1892
+ message: "Is unique?",
1893
+ default: false
1894
+ },
1895
+ {
1896
+ type: "confirm",
1897
+ name: "isArray",
1898
+ message: "Is array?",
1899
+ default: false
1900
+ }
1901
+ ]);
1902
+ fields.push({
1903
+ name: answers.name,
1904
+ type: fieldDetails.type,
1905
+ isOptional: fieldDetails.isOptional,
1906
+ isUnique: fieldDetails.isUnique,
1907
+ isArray: fieldDetails.isArray
1908
+ });
1909
+ console.log(` \u2713 Added: ${answers.name}: ${fieldDetails.type}
1910
+ `);
1911
+ }
1912
+ return fields;
1913
+ }
1914
+
1915
+ // src/cli/commands/add-module.ts
1916
+ import { Command as Command3 } from "commander";
1917
+ import path6 from "path";
1918
+ import ora3 from "ora";
1919
+ import chalk4 from "chalk";
1920
+ import * as fs5 from "fs/promises";
1921
+
1922
+ // src/cli/utils/env-manager.ts
1923
+ import * as fs3 from "fs/promises";
1924
+ import * as path4 from "path";
1925
+ import { existsSync } from "fs";
1926
+ var EnvManager = class {
1927
+ envPath;
1928
+ envExamplePath;
1929
+ constructor(projectRoot) {
1930
+ this.envPath = path4.join(projectRoot, ".env");
1931
+ this.envExamplePath = path4.join(projectRoot, ".env.example");
1932
+ }
1933
+ /**
1934
+ * Add environment variables to .env file
1935
+ */
1936
+ async addVariables(sections) {
1937
+ const added = [];
1938
+ const skipped = [];
1939
+ let created = false;
1940
+ let envContent = "";
1941
+ if (existsSync(this.envPath)) {
1942
+ envContent = await fs3.readFile(this.envPath, "utf-8");
1943
+ } else {
1944
+ created = true;
1945
+ }
1946
+ const existingKeys = this.parseExistingKeys(envContent);
1947
+ let newContent = envContent;
1948
+ if (newContent && !newContent.endsWith("\n\n")) {
1949
+ newContent += "\n\n";
1950
+ }
1951
+ for (const section of sections) {
1952
+ newContent += `# ${section.title}
1953
+ `;
1954
+ for (const variable of section.variables) {
1955
+ if (existingKeys.has(variable.key)) {
1956
+ skipped.push(variable.key);
1957
+ continue;
1958
+ }
1959
+ if (variable.comment) {
1960
+ newContent += `# ${variable.comment}
1961
+ `;
1962
+ }
1963
+ const value = variable.value || "";
1964
+ const prefix = variable.required ? "" : "# ";
1965
+ newContent += `${prefix}${variable.key}=${value}
1966
+ `;
1967
+ added.push(variable.key);
1968
+ }
1969
+ newContent += "\n";
1970
+ }
1971
+ await fs3.writeFile(this.envPath, newContent, "utf-8");
1972
+ if (existsSync(this.envExamplePath)) {
1973
+ await this.updateEnvExample(sections);
1974
+ }
1975
+ return { added, skipped, created };
1976
+ }
1977
+ /**
1978
+ * Update .env.example file
1979
+ */
1980
+ async updateEnvExample(sections) {
1981
+ let exampleContent = "";
1982
+ if (existsSync(this.envExamplePath)) {
1983
+ exampleContent = await fs3.readFile(this.envExamplePath, "utf-8");
1984
+ }
1985
+ const existingKeys = this.parseExistingKeys(exampleContent);
1986
+ let newContent = exampleContent;
1987
+ if (newContent && !newContent.endsWith("\n\n")) {
1988
+ newContent += "\n\n";
1989
+ }
1990
+ for (const section of sections) {
1991
+ newContent += `# ${section.title}
1992
+ `;
1993
+ for (const variable of section.variables) {
1994
+ if (existingKeys.has(variable.key)) {
1995
+ continue;
1996
+ }
1997
+ if (variable.comment) {
1998
+ newContent += `# ${variable.comment}
1999
+ `;
2000
+ }
2001
+ const placeholder = this.getPlaceholder(variable.key);
2002
+ newContent += `${variable.key}=${placeholder}
2003
+ `;
2004
+ }
2005
+ newContent += "\n";
2006
+ }
2007
+ await fs3.writeFile(this.envExamplePath, newContent, "utf-8");
2008
+ }
2009
+ /**
2010
+ * Parse existing environment variable keys
2011
+ */
2012
+ parseExistingKeys(content) {
2013
+ const keys = /* @__PURE__ */ new Set();
2014
+ const lines = content.split("\n");
2015
+ for (const line of lines) {
2016
+ const trimmed = line.trim();
2017
+ if (!trimmed || trimmed.startsWith("#")) {
2018
+ continue;
2019
+ }
2020
+ const match = trimmed.match(/^#?\s*([A-Z_][A-Z0-9_]*)\s*=/);
2021
+ if (match && match[1]) {
2022
+ keys.add(match[1]);
2023
+ }
2024
+ }
2025
+ return keys;
2026
+ }
2027
+ /**
2028
+ * Get placeholder value for .env.example
2029
+ */
2030
+ getPlaceholder(key) {
2031
+ if (key.includes("SECRET") || key.includes("KEY") || key.includes("PASSWORD")) {
2032
+ return "your-secret-key-here";
2033
+ }
2034
+ if (key.includes("HOST")) {
2035
+ return "localhost";
2036
+ }
2037
+ if (key.includes("PORT")) {
2038
+ return "3000";
2039
+ }
2040
+ if (key.includes("URL")) {
2041
+ return "http://localhost:3000";
2042
+ }
2043
+ if (key.includes("EMAIL")) {
2044
+ return "user@example.com";
2045
+ }
2046
+ if (key.includes("REDIS")) {
2047
+ return "redis://localhost:6379";
2048
+ }
2049
+ if (key.includes("DATABASE")) {
2050
+ return "postgresql://user:pass@localhost:5432/db";
2051
+ }
2052
+ if (key.includes("NODE")) {
2053
+ return "http://localhost:9200";
2054
+ }
2055
+ return "";
2056
+ }
2057
+ /**
2058
+ * Get environment variables for a specific module
2059
+ */
2060
+ static getModuleEnvVariables(moduleName) {
2061
+ const moduleEnvMap = {
2062
+ "rate-limit": [
2063
+ {
2064
+ title: "Rate Limiting Configuration",
2065
+ variables: [
2066
+ {
2067
+ key: "RATE_LIMIT_ENABLED",
2068
+ value: "true",
2069
+ comment: "Enable rate limiting",
2070
+ required: true
2071
+ },
2072
+ {
2073
+ key: "RATE_LIMIT_REDIS_URL",
2074
+ value: "redis://localhost:6379",
2075
+ comment: "Redis URL for distributed rate limiting (optional, uses in-memory if not set)",
2076
+ required: false
2077
+ }
2078
+ ]
2079
+ }
2080
+ ],
2081
+ webhook: [
2082
+ {
2083
+ title: "Webhook Configuration",
2084
+ variables: [
2085
+ {
2086
+ key: "WEBHOOK_TIMEOUT",
2087
+ value: "10000",
2088
+ comment: "Webhook request timeout in milliseconds",
2089
+ required: true
2090
+ },
2091
+ {
2092
+ key: "WEBHOOK_MAX_RETRIES",
2093
+ value: "5",
2094
+ comment: "Maximum number of retry attempts",
2095
+ required: true
2096
+ },
2097
+ {
2098
+ key: "WEBHOOK_SIGNATURE_ENABLED",
2099
+ value: "true",
2100
+ comment: "Enable HMAC signature verification",
2101
+ required: true
2102
+ }
2103
+ ]
2104
+ }
2105
+ ],
2106
+ queue: [
2107
+ {
2108
+ title: "Queue/Jobs Configuration",
2109
+ variables: [
2110
+ {
2111
+ key: "REDIS_HOST",
2112
+ value: "localhost",
2113
+ comment: "Redis host for Bull queue",
2114
+ required: true
2115
+ },
2116
+ {
2117
+ key: "REDIS_PORT",
2118
+ value: "6379",
2119
+ comment: "Redis port",
2120
+ required: true
2121
+ },
2122
+ {
2123
+ key: "REDIS_PASSWORD",
2124
+ value: "",
2125
+ comment: "Redis password (optional)",
2126
+ required: false
2127
+ },
2128
+ {
2129
+ key: "QUEUE_METRICS_ENABLED",
2130
+ value: "true",
2131
+ comment: "Enable queue metrics collection",
2132
+ required: true
2133
+ }
2134
+ ]
2135
+ }
2136
+ ],
2137
+ websocket: [
2138
+ {
2139
+ title: "WebSocket Configuration",
2140
+ variables: [
2141
+ {
2142
+ key: "WEBSOCKET_PORT",
2143
+ value: "3001",
2144
+ comment: "WebSocket server port",
2145
+ required: true
2146
+ },
2147
+ {
2148
+ key: "WEBSOCKET_CORS_ORIGIN",
2149
+ value: "http://localhost:3000",
2150
+ comment: "CORS origin for WebSocket",
2151
+ required: true
2152
+ },
2153
+ {
2154
+ key: "WEBSOCKET_REDIS_URL",
2155
+ value: "redis://localhost:6379",
2156
+ comment: "Redis URL for Socket.io adapter (optional, for multi-instance)",
2157
+ required: false
2158
+ }
2159
+ ]
2160
+ }
2161
+ ],
2162
+ search: [
2163
+ {
2164
+ title: "Search Configuration (Elasticsearch)",
2165
+ variables: [
2166
+ {
2167
+ key: "SEARCH_ENGINE",
2168
+ value: "memory",
2169
+ comment: "Search engine: elasticsearch, meilisearch, or memory",
2170
+ required: true
2171
+ },
2172
+ {
2173
+ key: "ELASTICSEARCH_NODE",
2174
+ value: "http://localhost:9200",
2175
+ comment: "Elasticsearch node URL",
2176
+ required: false
2177
+ },
2178
+ {
2179
+ key: "ELASTICSEARCH_USERNAME",
2180
+ value: "",
2181
+ comment: "Elasticsearch username (optional)",
2182
+ required: false
2183
+ },
2184
+ {
2185
+ key: "ELASTICSEARCH_PASSWORD",
2186
+ value: "",
2187
+ comment: "Elasticsearch password (optional)",
2188
+ required: false
2189
+ }
2190
+ ]
2191
+ },
2192
+ {
2193
+ title: "Search Configuration (Meilisearch)",
2194
+ variables: [
2195
+ {
2196
+ key: "MEILISEARCH_HOST",
2197
+ value: "http://localhost:7700",
2198
+ comment: "Meilisearch host URL",
2199
+ required: false
2200
+ },
2201
+ {
2202
+ key: "MEILISEARCH_API_KEY",
2203
+ value: "",
2204
+ comment: "Meilisearch API key (optional)",
2205
+ required: false
2206
+ }
2207
+ ]
2208
+ }
2209
+ ],
2210
+ i18n: [
2211
+ {
2212
+ title: "i18n/Localization Configuration",
2213
+ variables: [
2214
+ {
2215
+ key: "DEFAULT_LOCALE",
2216
+ value: "en",
2217
+ comment: "Default locale/language",
2218
+ required: true
2219
+ },
2220
+ {
2221
+ key: "SUPPORTED_LOCALES",
2222
+ value: "en,fr,es,de,ar,zh,ja",
2223
+ comment: "Comma-separated list of supported locales",
2224
+ required: true
2225
+ },
2226
+ {
2227
+ key: "TRANSLATIONS_DIR",
2228
+ value: "./locales",
2229
+ comment: "Directory for translation files",
2230
+ required: true
2231
+ },
2232
+ {
2233
+ key: "I18N_CACHE_ENABLED",
2234
+ value: "true",
2235
+ comment: "Enable translation caching",
2236
+ required: true
2237
+ }
2238
+ ]
2239
+ }
2240
+ ],
2241
+ cache: [
2242
+ {
2243
+ title: "Cache Configuration",
2244
+ variables: [
2245
+ {
2246
+ key: "CACHE_PROVIDER",
2247
+ value: "redis",
2248
+ comment: "Cache provider: redis or memory",
2249
+ required: true
2250
+ },
2251
+ {
2252
+ key: "CACHE_REDIS_URL",
2253
+ value: "redis://localhost:6379",
2254
+ comment: "Redis URL for cache",
2255
+ required: false
2256
+ },
2257
+ {
2258
+ key: "CACHE_TTL",
2259
+ value: "3600",
2260
+ comment: "Default cache TTL in seconds",
2261
+ required: true
2262
+ }
2263
+ ]
2264
+ }
2265
+ ],
2266
+ mfa: [
2267
+ {
2268
+ title: "MFA/TOTP Configuration",
2269
+ variables: [
2270
+ {
2271
+ key: "MFA_ISSUER",
2272
+ value: "MyApp",
2273
+ comment: "MFA issuer name shown in authenticator apps",
2274
+ required: true
2275
+ },
2276
+ {
2277
+ key: "MFA_ALGORITHM",
2278
+ value: "SHA1",
2279
+ comment: "TOTP algorithm: SHA1, SHA256, or SHA512",
2280
+ required: true
2281
+ }
2282
+ ]
2283
+ }
2284
+ ],
2285
+ oauth: [
2286
+ {
2287
+ title: "OAuth Configuration",
2288
+ variables: [
2289
+ {
2290
+ key: "OAUTH_GOOGLE_CLIENT_ID",
2291
+ value: "",
2292
+ comment: "Google OAuth client ID",
2293
+ required: false
2294
+ },
2295
+ {
2296
+ key: "OAUTH_GOOGLE_CLIENT_SECRET",
2297
+ value: "",
2298
+ comment: "Google OAuth client secret",
2299
+ required: false
2300
+ },
2301
+ {
2302
+ key: "OAUTH_GITHUB_CLIENT_ID",
2303
+ value: "",
2304
+ comment: "GitHub OAuth client ID",
2305
+ required: false
2306
+ },
2307
+ {
2308
+ key: "OAUTH_GITHUB_CLIENT_SECRET",
2309
+ value: "",
2310
+ comment: "GitHub OAuth client secret",
2311
+ required: false
2312
+ },
2313
+ {
2314
+ key: "OAUTH_REDIRECT_URL",
2315
+ value: "http://localhost:3000/auth/callback",
2316
+ comment: "OAuth callback URL",
2317
+ required: true
2318
+ }
2319
+ ]
2320
+ }
2321
+ ],
2322
+ payment: [
2323
+ {
2324
+ title: "Payment Configuration",
2325
+ variables: [
2326
+ {
2327
+ key: "STRIPE_SECRET_KEY",
2328
+ value: "",
2329
+ comment: "Stripe secret key",
2330
+ required: false
2331
+ },
2332
+ {
2333
+ key: "STRIPE_PUBLISHABLE_KEY",
2334
+ value: "",
2335
+ comment: "Stripe publishable key",
2336
+ required: false
2337
+ },
2338
+ {
2339
+ key: "PAYPAL_CLIENT_ID",
2340
+ value: "",
2341
+ comment: "PayPal client ID",
2342
+ required: false
2343
+ },
2344
+ {
2345
+ key: "PAYPAL_CLIENT_SECRET",
2346
+ value: "",
2347
+ comment: "PayPal client secret",
2348
+ required: false
2349
+ },
2350
+ {
2351
+ key: "PAYPAL_MODE",
2352
+ value: "sandbox",
2353
+ comment: "PayPal mode: sandbox or live",
2354
+ required: false
2355
+ }
2356
+ ]
2357
+ }
2358
+ ],
2359
+ "feature-flag": [
2360
+ {
2361
+ title: "Feature Flags Configuration",
2362
+ variables: [
2363
+ {
2364
+ key: "FEATURE_FLAGS_ENABLED",
2365
+ value: "true",
2366
+ comment: "Enable feature flags",
2367
+ required: true
2368
+ },
2369
+ {
2370
+ key: "FEATURE_FLAGS_ENVIRONMENT",
2371
+ value: "development",
2372
+ comment: "Feature flags environment: development, staging, production, test",
2373
+ required: true
2374
+ },
2375
+ {
2376
+ key: "FEATURE_FLAGS_ANALYTICS",
2377
+ value: "true",
2378
+ comment: "Enable feature flag analytics",
2379
+ required: true
2380
+ },
2381
+ {
2382
+ key: "FEATURE_FLAGS_CACHE_TTL",
2383
+ value: "300",
2384
+ comment: "Cache TTL in seconds",
2385
+ required: true
2386
+ }
2387
+ ]
2388
+ }
2389
+ ],
2390
+ upload: [
2391
+ {
2392
+ title: "File Upload Configuration",
2393
+ variables: [
2394
+ {
2395
+ key: "UPLOAD_PROVIDER",
2396
+ value: "local",
2397
+ comment: "Upload provider: local, s3, cloudinary",
2398
+ required: true
2399
+ },
2400
+ {
2401
+ key: "UPLOAD_MAX_SIZE",
2402
+ value: "10485760",
2403
+ comment: "Max upload size in bytes (10MB)",
2404
+ required: true
2405
+ },
2406
+ {
2407
+ key: "UPLOAD_DIR",
2408
+ value: "./uploads",
2409
+ comment: "Local upload directory",
2410
+ required: true
2411
+ },
2412
+ {
2413
+ key: "AWS_ACCESS_KEY_ID",
2414
+ value: "",
2415
+ comment: "AWS access key for S3",
2416
+ required: false
2417
+ },
2418
+ {
2419
+ key: "AWS_SECRET_ACCESS_KEY",
2420
+ value: "",
2421
+ comment: "AWS secret key for S3",
2422
+ required: false
2423
+ },
2424
+ {
2425
+ key: "AWS_S3_BUCKET",
2426
+ value: "",
2427
+ comment: "S3 bucket name",
2428
+ required: false
2429
+ }
2430
+ ]
2431
+ }
2432
+ ],
2433
+ notification: [
2434
+ {
2435
+ title: "Notification Configuration",
2436
+ variables: [
2437
+ {
2438
+ key: "NOTIFICATION_EMAIL_ENABLED",
2439
+ value: "true",
2440
+ comment: "Enable email notifications",
2441
+ required: true
2442
+ },
2443
+ {
2444
+ key: "NOTIFICATION_SMS_ENABLED",
2445
+ value: "false",
2446
+ comment: "Enable SMS notifications",
2447
+ required: false
2448
+ },
2449
+ {
2450
+ key: "NOTIFICATION_PUSH_ENABLED",
2451
+ value: "false",
2452
+ comment: "Enable push notifications",
2453
+ required: false
2454
+ },
2455
+ {
2456
+ key: "TWILIO_ACCOUNT_SID",
2457
+ value: "",
2458
+ comment: "Twilio account SID for SMS",
2459
+ required: false
2460
+ },
2461
+ {
2462
+ key: "TWILIO_AUTH_TOKEN",
2463
+ value: "",
2464
+ comment: "Twilio auth token",
2465
+ required: false
2466
+ }
2467
+ ]
2468
+ }
2469
+ ],
2470
+ analytics: [
2471
+ {
2472
+ title: "Analytics/Metrics Configuration",
2473
+ variables: [
2474
+ {
2475
+ key: "ANALYTICS_ENABLED",
2476
+ value: "true",
2477
+ comment: "Enable analytics and metrics collection",
2478
+ required: true
2479
+ },
2480
+ {
2481
+ key: "ANALYTICS_PREFIX",
2482
+ value: "app",
2483
+ comment: "Metrics prefix",
2484
+ required: true
2485
+ },
2486
+ {
2487
+ key: "PROMETHEUS_ENABLED",
2488
+ value: "true",
2489
+ comment: "Enable Prometheus metrics endpoint",
2490
+ required: true
2491
+ },
2492
+ {
2493
+ key: "METRICS_FLUSH_INTERVAL",
2494
+ value: "60000",
2495
+ comment: "Metrics flush interval in milliseconds",
2496
+ required: true
2497
+ }
2498
+ ]
2499
+ }
2500
+ ],
2501
+ "media-processing": [
2502
+ {
2503
+ title: "Media Processing Configuration",
2504
+ variables: [
2505
+ {
2506
+ key: "FFMPEG_PATH",
2507
+ value: "ffmpeg",
2508
+ comment: "Path to FFmpeg binary",
2509
+ required: true
2510
+ },
2511
+ {
2512
+ key: "FFPROBE_PATH",
2513
+ value: "ffprobe",
2514
+ comment: "Path to FFprobe binary",
2515
+ required: true
2516
+ },
2517
+ {
2518
+ key: "MEDIA_TEMP_DIR",
2519
+ value: "./temp/media",
2520
+ comment: "Temporary directory for media processing",
2521
+ required: true
2522
+ },
2523
+ {
2524
+ key: "MEDIA_MAX_CONCURRENT",
2525
+ value: "3",
2526
+ comment: "Maximum concurrent processing jobs",
2527
+ required: true
2528
+ },
2529
+ {
2530
+ key: "MEDIA_GPU_ACCELERATION",
2531
+ value: "false",
2532
+ comment: "Enable GPU acceleration (requires NVIDIA GPU)",
2533
+ required: false
2534
+ }
2535
+ ]
2536
+ }
2537
+ ],
2538
+ "api-versioning": [
2539
+ {
2540
+ title: "API Versioning Configuration",
2541
+ variables: [
2542
+ {
2543
+ key: "API_VERSION_STRATEGY",
2544
+ value: "url",
2545
+ comment: "Versioning strategy: url, header, query, accept-header",
2546
+ required: true
2547
+ },
2548
+ {
2549
+ key: "API_DEFAULT_VERSION",
2550
+ value: "v1",
2551
+ comment: "Default API version",
2552
+ required: true
2553
+ },
2554
+ {
2555
+ key: "API_VERSION_HEADER",
2556
+ value: "X-API-Version",
2557
+ comment: "Header name for version (if strategy is header)",
2558
+ required: false
2559
+ },
2560
+ {
2561
+ key: "API_VERSION_STRICT",
2562
+ value: "true",
2563
+ comment: "Strict mode - reject unknown versions",
2564
+ required: true
2565
+ },
2566
+ {
2567
+ key: "API_DEPRECATION_WARNINGS",
2568
+ value: "true",
2569
+ comment: "Show deprecation warnings in headers",
2570
+ required: true
2571
+ }
2572
+ ]
2573
+ }
2574
+ ]
2575
+ };
2576
+ return moduleEnvMap[moduleName] || [];
2577
+ }
2578
+ };
2579
+
2580
+ // src/cli/utils/template-manager.ts
2581
+ import * as fs4 from "fs/promises";
2582
+ import * as path5 from "path";
2583
+ import { createHash } from "crypto";
2584
+ import { existsSync as existsSync2 } from "fs";
2585
+ var TemplateManager = class {
2586
+ templatesDir;
2587
+ manifestsDir;
2588
+ constructor(projectRoot) {
2589
+ this.templatesDir = path5.join(projectRoot, ".servcraft", "templates");
2590
+ this.manifestsDir = path5.join(projectRoot, ".servcraft", "manifests");
2591
+ }
2592
+ /**
2593
+ * Initialize template system
2594
+ */
2595
+ async initialize() {
2596
+ await fs4.mkdir(this.templatesDir, { recursive: true });
2597
+ await fs4.mkdir(this.manifestsDir, { recursive: true });
2598
+ }
2599
+ /**
2600
+ * Save module template
2601
+ */
2602
+ async saveTemplate(moduleName, files) {
2603
+ await this.initialize();
2604
+ const moduleTemplateDir = path5.join(this.templatesDir, moduleName);
2605
+ await fs4.mkdir(moduleTemplateDir, { recursive: true });
2606
+ for (const [fileName, content] of Object.entries(files)) {
2607
+ const filePath = path5.join(moduleTemplateDir, fileName);
2608
+ await fs4.writeFile(filePath, content, "utf-8");
2609
+ }
2610
+ }
2611
+ /**
2612
+ * Get template content
2613
+ */
2614
+ async getTemplate(moduleName, fileName) {
2615
+ try {
2616
+ const filePath = path5.join(this.templatesDir, moduleName, fileName);
2617
+ return await fs4.readFile(filePath, "utf-8");
2618
+ } catch {
2619
+ return null;
2620
+ }
2621
+ }
2622
+ /**
2623
+ * Save module manifest
2624
+ */
2625
+ async saveManifest(moduleName, files) {
2626
+ await this.initialize();
2627
+ const fileHashes = {};
2628
+ for (const [fileName, content] of Object.entries(files)) {
2629
+ fileHashes[fileName] = {
2630
+ hash: this.hashContent(content),
2631
+ path: `src/modules/${moduleName}/${fileName}`
2632
+ };
2633
+ }
2634
+ const manifest = {
2635
+ name: moduleName,
2636
+ version: "1.0.0",
2637
+ files: fileHashes,
2638
+ installedAt: /* @__PURE__ */ new Date(),
2639
+ updatedAt: /* @__PURE__ */ new Date()
2640
+ };
2641
+ const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
2642
+ await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
2643
+ }
2644
+ /**
2645
+ * Get module manifest
2646
+ */
2647
+ async getManifest(moduleName) {
2648
+ try {
2649
+ const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
2650
+ const content = await fs4.readFile(manifestPath, "utf-8");
2651
+ return JSON.parse(content);
2652
+ } catch {
2653
+ return null;
2654
+ }
3327
2655
  }
3328
- });
3329
- generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3330
- const spinner = ora3("Generating schemas...").start();
3331
- try {
3332
- const kebabName = toKebabCase(name);
3333
- const pascalName = toPascalCase(name);
3334
- const camelName = toCamelCase(name);
3335
- const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3336
- const moduleDir = path4.join(getModulesDir(), moduleName);
3337
- const filePath = path4.join(moduleDir, `${kebabName}.schemas.ts`);
3338
- if (await fileExists(filePath)) {
3339
- spinner.stop();
3340
- error(`Schemas file "${kebabName}.schemas.ts" already exists`);
3341
- return;
2656
+ /**
2657
+ * Check if file has been modified by user
2658
+ */
2659
+ async isFileModified(moduleName, fileName, currentContent) {
2660
+ const manifest = await this.getManifest(moduleName);
2661
+ if (!manifest || !manifest.files[fileName]) {
2662
+ return false;
2663
+ }
2664
+ const originalHash = manifest.files[fileName].hash;
2665
+ const currentHash = this.hashContent(currentContent);
2666
+ return originalHash !== currentHash;
2667
+ }
2668
+ /**
2669
+ * Get all modified files in a module
2670
+ */
2671
+ async getModifiedFiles(moduleName, moduleDir) {
2672
+ const manifest = await this.getManifest(moduleName);
2673
+ if (!manifest) {
2674
+ return [];
2675
+ }
2676
+ const results = [];
2677
+ for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
2678
+ const filePath = path5.join(moduleDir, fileName);
2679
+ if (!existsSync2(filePath)) {
2680
+ results.push({
2681
+ fileName,
2682
+ isModified: true,
2683
+ originalHash: fileInfo.hash,
2684
+ currentHash: ""
2685
+ });
2686
+ continue;
2687
+ }
2688
+ const currentContent = await fs4.readFile(filePath, "utf-8");
2689
+ const currentHash = this.hashContent(currentContent);
2690
+ results.push({
2691
+ fileName,
2692
+ isModified: fileInfo.hash !== currentHash,
2693
+ originalHash: fileInfo.hash,
2694
+ currentHash
2695
+ });
2696
+ }
2697
+ return results;
2698
+ }
2699
+ /**
2700
+ * Create backup of module
2701
+ */
2702
+ async createBackup(moduleName, moduleDir) {
2703
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
2704
+ const backupDir = path5.join(path5.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
2705
+ await this.copyDirectory(moduleDir, backupDir);
2706
+ return backupDir;
2707
+ }
2708
+ /**
2709
+ * Copy directory recursively
2710
+ */
2711
+ async copyDirectory(src, dest) {
2712
+ await fs4.mkdir(dest, { recursive: true });
2713
+ const entries = await fs4.readdir(src, { withFileTypes: true });
2714
+ for (const entry of entries) {
2715
+ const srcPath = path5.join(src, entry.name);
2716
+ const destPath = path5.join(dest, entry.name);
2717
+ if (entry.isDirectory()) {
2718
+ await this.copyDirectory(srcPath, destPath);
2719
+ } else {
2720
+ await fs4.copyFile(srcPath, destPath);
2721
+ }
3342
2722
  }
3343
- await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
3344
- spinner.succeed(`Schemas for "${pascalName}" generated!`);
3345
- success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
3346
- } catch (err) {
3347
- spinner.fail("Failed to generate schemas");
3348
- error(err instanceof Error ? err.message : String(err));
3349
2723
  }
3350
- });
3351
- generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
3352
- const spinner = ora3("Generating routes...").start();
3353
- try {
3354
- const kebabName = toKebabCase(name);
3355
- const pascalName = toPascalCase(name);
3356
- const camelName = toCamelCase(name);
3357
- const pluralName = pluralize(kebabName);
3358
- const moduleName = options.module ? toKebabCase(options.module) : kebabName;
3359
- const moduleDir = path4.join(getModulesDir(), moduleName);
3360
- const filePath = path4.join(moduleDir, `${kebabName}.routes.ts`);
3361
- if (await fileExists(filePath)) {
3362
- spinner.stop();
3363
- error(`Routes file "${kebabName}.routes.ts" already exists`);
2724
+ /**
2725
+ * Perform 3-way merge
2726
+ */
2727
+ async mergeFiles(original, modified, incoming) {
2728
+ const conflicts = [];
2729
+ let hasConflicts = false;
2730
+ const originalLines = original.split("\n");
2731
+ const modifiedLines = modified.split("\n");
2732
+ const incomingLines = incoming.split("\n");
2733
+ const merged = [];
2734
+ const maxLength = Math.max(originalLines.length, modifiedLines.length, incomingLines.length);
2735
+ for (let i = 0; i < maxLength; i++) {
2736
+ const origLine = originalLines[i] || "";
2737
+ const modLine = modifiedLines[i] || "";
2738
+ const incLine = incomingLines[i] || "";
2739
+ if (modLine === origLine) {
2740
+ merged.push(incLine);
2741
+ } else if (incLine === origLine) {
2742
+ merged.push(modLine);
2743
+ } else if (modLine === incLine) {
2744
+ merged.push(modLine);
2745
+ } else {
2746
+ hasConflicts = true;
2747
+ conflicts.push(`Line ${i + 1}: User and template both modified`);
2748
+ merged.push(`<<<<<<< YOUR VERSION`);
2749
+ merged.push(modLine);
2750
+ merged.push(`=======`);
2751
+ merged.push(incLine);
2752
+ merged.push(`>>>>>>> NEW VERSION`);
2753
+ }
2754
+ }
2755
+ return {
2756
+ merged: merged.join("\n"),
2757
+ hasConflicts,
2758
+ conflicts
2759
+ };
2760
+ }
2761
+ /**
2762
+ * Generate diff between two files
2763
+ */
2764
+ generateDiff(original, modified) {
2765
+ const originalLines = original.split("\n");
2766
+ const modifiedLines = modified.split("\n");
2767
+ const diff = [];
2768
+ diff.push("--- Original");
2769
+ diff.push("+++ Modified");
2770
+ diff.push("");
2771
+ const maxLength = Math.max(originalLines.length, modifiedLines.length);
2772
+ for (let i = 0; i < maxLength; i++) {
2773
+ const origLine = originalLines[i];
2774
+ const modLine = modifiedLines[i];
2775
+ if (origLine !== modLine) {
2776
+ if (origLine !== void 0) {
2777
+ diff.push(`- ${origLine}`);
2778
+ }
2779
+ if (modLine !== void 0) {
2780
+ diff.push(`+ ${modLine}`);
2781
+ }
2782
+ }
2783
+ }
2784
+ return diff.join("\n");
2785
+ }
2786
+ /**
2787
+ * Hash content for change detection
2788
+ */
2789
+ hashContent(content) {
2790
+ return createHash("md5").update(content).digest("hex");
2791
+ }
2792
+ /**
2793
+ * Check if module is installed
2794
+ */
2795
+ async isModuleInstalled(moduleName) {
2796
+ const manifest = await this.getManifest(moduleName);
2797
+ return manifest !== null;
2798
+ }
2799
+ /**
2800
+ * Update manifest after merge
2801
+ */
2802
+ async updateManifest(moduleName, files) {
2803
+ const manifest = await this.getManifest(moduleName);
2804
+ if (!manifest) {
2805
+ await this.saveManifest(moduleName, files);
3364
2806
  return;
3365
2807
  }
3366
- await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
3367
- spinner.succeed(`Routes for "${pascalName}" generated!`);
3368
- success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
3369
- } catch (err) {
3370
- spinner.fail("Failed to generate routes");
3371
- error(err instanceof Error ? err.message : String(err));
2808
+ const fileHashes = {};
2809
+ for (const [fileName, content] of Object.entries(files)) {
2810
+ fileHashes[fileName] = {
2811
+ hash: this.hashContent(content),
2812
+ path: `src/modules/${moduleName}/${fileName}`
2813
+ };
2814
+ }
2815
+ manifest.files = fileHashes;
2816
+ manifest.updatedAt = /* @__PURE__ */ new Date();
2817
+ const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
2818
+ await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
3372
2819
  }
3373
- });
3374
- async function promptForFields() {
3375
- const fields = [];
3376
- console.log("\n\u{1F4DD} Define your model fields (press Enter with empty name to finish)\n");
3377
- const fieldTypes = [
3378
- "string",
3379
- "number",
3380
- "boolean",
3381
- "date",
3382
- "datetime",
3383
- "text",
3384
- "email",
3385
- "url",
3386
- "uuid",
3387
- "int",
3388
- "float",
3389
- "decimal",
3390
- "json"
3391
- ];
3392
- let addMore = true;
3393
- while (addMore) {
3394
- const answers = await inquirer2.prompt([
2820
+ };
2821
+
2822
+ // src/cli/utils/interactive-prompt.ts
2823
+ import inquirer3 from "inquirer";
2824
+ import chalk3 from "chalk";
2825
+ var InteractivePrompt = class {
2826
+ /**
2827
+ * Ask what to do when module already exists
2828
+ */
2829
+ static async askModuleExists(moduleName, hasModifications) {
2830
+ console.log(chalk3.yellow(`
2831
+ \u26A0\uFE0F Module "${moduleName}" already exists`));
2832
+ if (hasModifications) {
2833
+ console.log(chalk3.yellow(" Some files have been modified by you.\n"));
2834
+ }
2835
+ const { action } = await inquirer3.prompt([
3395
2836
  {
3396
- type: "input",
3397
- name: "name",
3398
- message: "Field name (empty to finish):"
2837
+ type: "list",
2838
+ name: "action",
2839
+ message: "What would you like to do?",
2840
+ choices: [
2841
+ {
2842
+ name: "\u2713 Skip (keep existing files, recommended if you made custom changes)",
2843
+ value: "skip"
2844
+ },
2845
+ {
2846
+ name: "\u21BB Update (merge new features, preserve your customizations)",
2847
+ value: "update"
2848
+ },
2849
+ {
2850
+ name: "\u26A0 Overwrite (replace all files, will lose your changes)",
2851
+ value: "overwrite"
2852
+ },
2853
+ {
2854
+ name: "\u{1F4CB} Show diff (see what changed)",
2855
+ value: "diff"
2856
+ },
2857
+ {
2858
+ name: "\u{1F4BE} Backup & overwrite (save backup then replace)",
2859
+ value: "backup-overwrite"
2860
+ }
2861
+ ],
2862
+ default: "skip"
3399
2863
  }
3400
2864
  ]);
3401
- if (!answers.name) {
3402
- addMore = false;
3403
- continue;
3404
- }
3405
- const fieldDetails = await inquirer2.prompt([
2865
+ return { action };
2866
+ }
2867
+ /**
2868
+ * Ask what to do with a specific file
2869
+ */
2870
+ static async askFileAction(fileName, isModified, yourLines, newLines) {
2871
+ console.log(chalk3.cyan(`
2872
+ \u{1F4C1} ${fileName}`));
2873
+ console.log(
2874
+ chalk3.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
2875
+ );
2876
+ console.log(chalk3.gray(` New version: ${newLines} lines
2877
+ `));
2878
+ const { action } = await inquirer3.prompt([
3406
2879
  {
3407
2880
  type: "list",
3408
- name: "type",
3409
- message: `Type for "${answers.name}":`,
3410
- choices: fieldTypes,
3411
- default: "string"
3412
- },
3413
- {
3414
- type: "confirm",
3415
- name: "isOptional",
3416
- message: "Is optional?",
3417
- default: false
3418
- },
3419
- {
3420
- type: "confirm",
3421
- name: "isUnique",
3422
- message: "Is unique?",
3423
- default: false
3424
- },
2881
+ name: "action",
2882
+ message: "Action for this file:",
2883
+ choices: [
2884
+ {
2885
+ name: "[M]erge - Smart merge, preserve your changes",
2886
+ value: "merge"
2887
+ },
2888
+ {
2889
+ name: "[K]eep - Keep your version (skip update)",
2890
+ value: "keep"
2891
+ },
2892
+ {
2893
+ name: "[O]verwrite - Use new version",
2894
+ value: "overwrite"
2895
+ },
2896
+ {
2897
+ name: "[D]iff - Show differences",
2898
+ value: "diff"
2899
+ },
2900
+ {
2901
+ name: "[S]kip - Skip this file",
2902
+ value: "skip"
2903
+ }
2904
+ ],
2905
+ default: isModified ? "merge" : "overwrite"
2906
+ }
2907
+ ]);
2908
+ return { action };
2909
+ }
2910
+ /**
2911
+ * Confirm action
2912
+ */
2913
+ static async confirm(message, defaultValue = false) {
2914
+ const { confirmed } = await inquirer3.prompt([
3425
2915
  {
3426
2916
  type: "confirm",
3427
- name: "isArray",
3428
- message: "Is array?",
3429
- default: false
2917
+ name: "confirmed",
2918
+ message,
2919
+ default: defaultValue
3430
2920
  }
3431
2921
  ]);
3432
- fields.push({
3433
- name: answers.name,
3434
- type: fieldDetails.type,
3435
- isOptional: fieldDetails.isOptional,
3436
- isUnique: fieldDetails.isUnique,
3437
- isArray: fieldDetails.isArray
2922
+ return confirmed;
2923
+ }
2924
+ /**
2925
+ * Display diff and ask to continue
2926
+ */
2927
+ static async showDiffAndAsk(diff) {
2928
+ console.log(chalk3.cyan("\n\u{1F4CA} Differences:\n"));
2929
+ console.log(diff);
2930
+ return await this.confirm("\nDo you want to proceed with this change?", true);
2931
+ }
2932
+ /**
2933
+ * Display merge conflicts
2934
+ */
2935
+ static displayConflicts(conflicts) {
2936
+ console.log(chalk3.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
2937
+ conflicts.forEach((conflict, i) => {
2938
+ console.log(chalk3.yellow(` ${i + 1}. ${conflict}`));
3438
2939
  });
3439
- console.log(` \u2713 Added: ${answers.name}: ${fieldDetails.type}
3440
- `);
2940
+ console.log(chalk3.gray("\n Conflict markers have been added to the file:"));
2941
+ console.log(chalk3.gray(" <<<<<<< YOUR VERSION"));
2942
+ console.log(chalk3.gray(" ... your code ..."));
2943
+ console.log(chalk3.gray(" ======="));
2944
+ console.log(chalk3.gray(" ... new code ..."));
2945
+ console.log(chalk3.gray(" >>>>>>> NEW VERSION\n"));
2946
+ }
2947
+ /**
2948
+ * Show backup location
2949
+ */
2950
+ static showBackupCreated(backupPath) {
2951
+ console.log(chalk3.green(`
2952
+ \u2713 Backup created: ${chalk3.cyan(backupPath)}`));
2953
+ }
2954
+ /**
2955
+ * Show merge summary
2956
+ */
2957
+ static showMergeSummary(stats) {
2958
+ console.log(chalk3.bold("\n\u{1F4CA} Merge Summary:\n"));
2959
+ if (stats.merged > 0) {
2960
+ console.log(chalk3.green(` \u2713 Merged: ${stats.merged} file(s)`));
2961
+ }
2962
+ if (stats.kept > 0) {
2963
+ console.log(chalk3.blue(` \u2192 Kept: ${stats.kept} file(s)`));
2964
+ }
2965
+ if (stats.overwritten > 0) {
2966
+ console.log(chalk3.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
2967
+ }
2968
+ if (stats.conflicts > 0) {
2969
+ console.log(chalk3.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
2970
+ console.log(chalk3.gray("\n Please resolve conflicts manually before committing.\n"));
2971
+ }
2972
+ }
2973
+ /**
2974
+ * Ask for batch action on all files
2975
+ */
2976
+ static async askBatchAction() {
2977
+ const { action } = await inquirer3.prompt([
2978
+ {
2979
+ type: "list",
2980
+ name: "action",
2981
+ message: "Multiple files found. Choose action:",
2982
+ choices: [
2983
+ {
2984
+ name: "Ask for each file individually (recommended)",
2985
+ value: "individual"
2986
+ },
2987
+ {
2988
+ name: "Merge all files automatically",
2989
+ value: "merge-all"
2990
+ },
2991
+ {
2992
+ name: "Keep all existing files (skip update)",
2993
+ value: "keep-all"
2994
+ },
2995
+ {
2996
+ name: "Overwrite all files with new versions",
2997
+ value: "overwrite-all"
2998
+ }
2999
+ ],
3000
+ default: "individual"
3001
+ }
3002
+ ]);
3003
+ return action;
3441
3004
  }
3442
- return fields;
3443
- }
3005
+ };
3444
3006
 
3445
3007
  // src/cli/commands/add-module.ts
3446
- import { Command as Command3 } from "commander";
3447
- import path5 from "path";
3448
- import ora4 from "ora";
3449
- import chalk3 from "chalk";
3450
3008
  var AVAILABLE_MODULES = {
3451
3009
  auth: {
3452
3010
  name: "Authentication",
3453
3011
  description: "JWT authentication with access/refresh tokens",
3454
- files: ["auth.service", "auth.controller", "auth.routes", "auth.middleware", "auth.schemas", "auth.types", "index"]
3012
+ files: [
3013
+ "auth.service",
3014
+ "auth.controller",
3015
+ "auth.routes",
3016
+ "auth.middleware",
3017
+ "auth.schemas",
3018
+ "auth.types",
3019
+ "index"
3020
+ ]
3455
3021
  },
3456
3022
  users: {
3457
3023
  name: "User Management",
3458
3024
  description: "User CRUD with RBAC (roles & permissions)",
3459
- files: ["user.service", "user.controller", "user.repository", "user.routes", "user.schemas", "user.types", "index"]
3025
+ files: [
3026
+ "user.service",
3027
+ "user.controller",
3028
+ "user.repository",
3029
+ "user.routes",
3030
+ "user.schemas",
3031
+ "user.types",
3032
+ "index"
3033
+ ]
3460
3034
  },
3461
3035
  email: {
3462
3036
  name: "Email Service",
@@ -3468,91 +3042,222 @@ var AVAILABLE_MODULES = {
3468
3042
  description: "Activity logging and audit trail",
3469
3043
  files: ["audit.service", "audit.types", "index"]
3470
3044
  },
3471
- upload: {
3472
- name: "File Upload",
3473
- description: "File upload with local/S3 storage",
3474
- files: ["upload.service", "upload.controller", "upload.routes", "upload.types", "index"]
3475
- },
3476
3045
  cache: {
3477
3046
  name: "Redis Cache",
3478
- description: "Redis caching service",
3047
+ description: "Redis caching with TTL & invalidation",
3479
3048
  files: ["cache.service", "cache.types", "index"]
3480
3049
  },
3481
- notifications: {
3050
+ upload: {
3051
+ name: "File Upload",
3052
+ description: "File upload with local/S3/Cloudinary storage",
3053
+ files: ["upload.service", "upload.routes", "upload.types", "index"]
3054
+ },
3055
+ mfa: {
3056
+ name: "MFA/TOTP",
3057
+ description: "Two-factor authentication with QR codes",
3058
+ files: ["mfa.service", "mfa.routes", "totp.ts", "types.ts", "index"]
3059
+ },
3060
+ oauth: {
3061
+ name: "OAuth",
3062
+ description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
3063
+ files: ["oauth.service", "oauth.routes", "providers", "types.ts", "index"]
3064
+ },
3065
+ payment: {
3066
+ name: "Payments",
3067
+ description: "Payment processing (Stripe, PayPal, Mobile Money)",
3068
+ files: ["payment.service", "payment.routes", "providers", "types.ts", "index"]
3069
+ },
3070
+ notification: {
3482
3071
  name: "Notifications",
3483
- description: "In-app and push notifications",
3484
- files: ["notification.service", "notification.types", "index"]
3072
+ description: "Email, SMS, Push notifications",
3073
+ files: ["notification.service", "types.ts", "index"]
3074
+ },
3075
+ "rate-limit": {
3076
+ name: "Rate Limiting",
3077
+ description: "Advanced rate limiting with multiple algorithms",
3078
+ files: [
3079
+ "rate-limit.service",
3080
+ "rate-limit.middleware",
3081
+ "rate-limit.routes",
3082
+ "stores",
3083
+ "types.ts",
3084
+ "index"
3085
+ ]
3086
+ },
3087
+ webhook: {
3088
+ name: "Webhooks",
3089
+ description: "Outgoing webhooks with HMAC signatures & retry",
3090
+ files: ["webhook.service", "webhook.routes", "signature.ts", "retry.ts", "types.ts", "index"]
3091
+ },
3092
+ queue: {
3093
+ name: "Queue/Jobs",
3094
+ description: "Background jobs with Bull/BullMQ & cron scheduling",
3095
+ files: ["queue.service", "cron.ts", "workers.ts", "routes.ts", "types.ts", "index"]
3096
+ },
3097
+ websocket: {
3098
+ name: "WebSockets",
3099
+ description: "Real-time communication with Socket.io",
3100
+ files: ["websocket.service", "features.ts", "middlewares.ts", "types.ts", "index"]
3101
+ },
3102
+ search: {
3103
+ name: "Search",
3104
+ description: "Full-text search with Elasticsearch/Meilisearch",
3105
+ files: ["search.service", "adapters", "types.ts", "index"]
3485
3106
  },
3486
- settings: {
3487
- name: "Settings",
3488
- description: "Application settings management",
3489
- files: ["settings.service", "settings.controller", "settings.routes", "settings.types", "index"]
3107
+ i18n: {
3108
+ name: "i18n/Localization",
3109
+ description: "Multi-language support with 7+ locales",
3110
+ files: ["i18n.service", "i18n.middleware", "i18n.routes", "types.ts", "index"]
3111
+ },
3112
+ "feature-flag": {
3113
+ name: "Feature Flags",
3114
+ description: "A/B testing & progressive rollout",
3115
+ files: ["feature-flag.service", "feature-flag.routes", "types.ts", "index"]
3116
+ },
3117
+ analytics: {
3118
+ name: "Analytics/Metrics",
3119
+ description: "Prometheus metrics & event tracking",
3120
+ files: ["analytics.service", "analytics.routes", "types.ts", "index"]
3121
+ },
3122
+ "media-processing": {
3123
+ name: "Media Processing",
3124
+ description: "Image/video processing with FFmpeg",
3125
+ files: ["media-processing.service", "media-processing.routes", "types.ts", "index"]
3126
+ },
3127
+ "api-versioning": {
3128
+ name: "API Versioning",
3129
+ description: "Multiple API versions support",
3130
+ files: [
3131
+ "versioning.service",
3132
+ "versioning.middleware",
3133
+ "versioning.routes",
3134
+ "types.ts",
3135
+ "index"
3136
+ ]
3490
3137
  }
3491
3138
  };
3492
- var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument("[module]", "Module to add (auth, users, email, audit, upload, cache, notifications, settings)").option("-l, --list", "List available modules").action(async (moduleName, options) => {
3493
- if (options?.list || !moduleName) {
3494
- console.log(chalk3.bold("\n\u{1F4E6} Available Modules:\n"));
3495
- for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
3496
- console.log(` ${chalk3.cyan(key.padEnd(15))} ${mod.name}`);
3497
- console.log(` ${" ".repeat(15)} ${chalk3.gray(mod.description)}
3139
+ var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
3140
+ "[module]",
3141
+ "Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
3142
+ ).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").action(
3143
+ async (moduleName, options) => {
3144
+ if (options?.list || !moduleName) {
3145
+ console.log(chalk4.bold("\n\u{1F4E6} Available Modules:\n"));
3146
+ for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
3147
+ console.log(` ${chalk4.cyan(key.padEnd(15))} ${mod.name}`);
3148
+ console.log(` ${" ".repeat(15)} ${chalk4.gray(mod.description)}
3498
3149
  `);
3499
- }
3500
- console.log(chalk3.bold("Usage:"));
3501
- console.log(` ${chalk3.yellow("servcraft add auth")} Add authentication module`);
3502
- console.log(` ${chalk3.yellow("servcraft add users")} Add user management module`);
3503
- console.log(` ${chalk3.yellow("servcraft add email")} Add email service module
3150
+ }
3151
+ console.log(chalk4.bold("Usage:"));
3152
+ console.log(` ${chalk4.yellow("servcraft add auth")} Add authentication module`);
3153
+ console.log(` ${chalk4.yellow("servcraft add users")} Add user management module`);
3154
+ console.log(` ${chalk4.yellow("servcraft add email")} Add email service module
3504
3155
  `);
3505
- return;
3506
- }
3507
- const module = AVAILABLE_MODULES[moduleName];
3508
- if (!module) {
3509
- error(`Unknown module: ${moduleName}`);
3510
- info('Run "servcraft add --list" to see available modules');
3511
- return;
3512
- }
3513
- const spinner = ora4(`Adding ${module.name} module...`).start();
3514
- try {
3515
- const moduleDir = path5.join(getModulesDir(), moduleName);
3516
- if (await fileExists(moduleDir)) {
3517
- spinner.stop();
3518
- warn(`Module "${moduleName}" already exists`);
3519
3156
  return;
3520
3157
  }
3521
- await ensureDir(moduleDir);
3522
- switch (moduleName) {
3523
- case "auth":
3524
- await generateAuthModule(moduleDir);
3525
- break;
3526
- case "users":
3527
- await generateUsersModule(moduleDir);
3528
- break;
3529
- case "email":
3530
- await generateEmailModule(moduleDir);
3531
- break;
3532
- case "audit":
3533
- await generateAuditModule(moduleDir);
3534
- break;
3535
- case "upload":
3536
- await generateUploadModule(moduleDir);
3537
- break;
3538
- case "cache":
3539
- await generateCacheModule(moduleDir);
3540
- break;
3541
- default:
3542
- await generateGenericModule(moduleDir, moduleName);
3543
- }
3544
- spinner.succeed(`${module.name} module added successfully!`);
3545
- console.log("\n\u{1F4C1} Files created:");
3546
- module.files.forEach((f) => success(` src/modules/${moduleName}/${f}.ts`));
3547
- console.log("\n\u{1F4CC} Next steps:");
3548
- info(" 1. Register the module in your main app file");
3549
- info(" 2. Configure any required environment variables");
3550
- info(" 3. Run database migrations if needed");
3551
- } catch (err) {
3552
- spinner.fail("Failed to add module");
3553
- error(err instanceof Error ? err.message : String(err));
3158
+ const module = AVAILABLE_MODULES[moduleName];
3159
+ if (!module) {
3160
+ error(`Unknown module: ${moduleName}`);
3161
+ info('Run "servcraft add --list" to see available modules');
3162
+ return;
3163
+ }
3164
+ const spinner = ora3(`Adding ${module.name} module...`).start();
3165
+ try {
3166
+ const moduleDir = path6.join(getModulesDir(), moduleName);
3167
+ const templateManager = new TemplateManager(process.cwd());
3168
+ const moduleExists = await fileExists(moduleDir);
3169
+ if (moduleExists) {
3170
+ spinner.stop();
3171
+ if (options?.skipExisting) {
3172
+ info(`Module "${moduleName}" already exists, skipping...`);
3173
+ return;
3174
+ }
3175
+ const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
3176
+ const hasModifications = modifiedFiles.some((f) => f.isModified);
3177
+ let action;
3178
+ if (options?.force) {
3179
+ action = "overwrite";
3180
+ } else if (options?.update) {
3181
+ action = "update";
3182
+ } else {
3183
+ const choice = await InteractivePrompt.askModuleExists(moduleName, hasModifications);
3184
+ action = choice.action;
3185
+ }
3186
+ if (action === "skip") {
3187
+ info("Keeping existing module");
3188
+ return;
3189
+ }
3190
+ if (action === "diff") {
3191
+ await showDiffForModule(templateManager, moduleName, moduleDir);
3192
+ return;
3193
+ }
3194
+ if (action === "backup-overwrite" || action === "overwrite") {
3195
+ if (action === "backup-overwrite") {
3196
+ const backupPath = await templateManager.createBackup(moduleName, moduleDir);
3197
+ InteractivePrompt.showBackupCreated(backupPath);
3198
+ }
3199
+ await fs5.rm(moduleDir, { recursive: true, force: true });
3200
+ await ensureDir(moduleDir);
3201
+ await generateModuleFiles(moduleName, moduleDir);
3202
+ const files = await getModuleFiles(moduleName, moduleDir);
3203
+ await templateManager.saveTemplate(moduleName, files);
3204
+ await templateManager.saveManifest(moduleName, files);
3205
+ spinner.succeed(
3206
+ `${module.name} module ${action === "backup-overwrite" ? "backed up and " : ""}overwritten!`
3207
+ );
3208
+ } else if (action === "update") {
3209
+ await performSmartMerge(templateManager, moduleName, moduleDir, module.name);
3210
+ }
3211
+ } else {
3212
+ await ensureDir(moduleDir);
3213
+ await generateModuleFiles(moduleName, moduleDir);
3214
+ const files = await getModuleFiles(moduleName, moduleDir);
3215
+ await templateManager.saveTemplate(moduleName, files);
3216
+ await templateManager.saveManifest(moduleName, files);
3217
+ spinner.succeed(`${module.name} module added successfully!`);
3218
+ }
3219
+ if (!moduleExists) {
3220
+ console.log("\n\u{1F4C1} Files created:");
3221
+ module.files.forEach((f) => success(` src/modules/${moduleName}/${f}.ts`));
3222
+ }
3223
+ const envManager = new EnvManager(process.cwd());
3224
+ const envSections = EnvManager.getModuleEnvVariables(moduleName);
3225
+ if (envSections.length > 0) {
3226
+ const envSpinner = ora3("Updating environment variables...").start();
3227
+ try {
3228
+ const result = await envManager.addVariables(envSections);
3229
+ envSpinner.succeed("Environment variables updated!");
3230
+ if (result.created) {
3231
+ info("\n\u{1F4DD} Created new .env file");
3232
+ }
3233
+ if (result.added.length > 0) {
3234
+ console.log(chalk4.bold("\n\u2705 Added to .env:"));
3235
+ result.added.forEach((key) => success(` ${key}`));
3236
+ }
3237
+ if (result.skipped.length > 0) {
3238
+ console.log(chalk4.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
3239
+ result.skipped.forEach((key) => info(` ${key}`));
3240
+ }
3241
+ const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
3242
+ if (requiredVars.length > 0) {
3243
+ console.log(chalk4.bold("\n\u26A0\uFE0F Required configuration:"));
3244
+ requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
3245
+ }
3246
+ } catch (err) {
3247
+ envSpinner.fail("Failed to update environment variables");
3248
+ error(err instanceof Error ? err.message : String(err));
3249
+ }
3250
+ }
3251
+ console.log("\n\u{1F4CC} Next steps:");
3252
+ info(" 1. Configure environment variables in .env (if needed)");
3253
+ info(" 2. Register the module in your main app file");
3254
+ info(" 3. Run database migrations if needed");
3255
+ } catch (err) {
3256
+ spinner.fail("Failed to add module");
3257
+ error(err instanceof Error ? err.message : String(err));
3258
+ }
3554
3259
  }
3555
- });
3260
+ );
3556
3261
  async function generateAuthModule(dir) {
3557
3262
  const files = {
3558
3263
  "auth.types.ts": `export interface JwtPayload {
@@ -3597,7 +3302,7 @@ export * from './auth.schemas.js';
3597
3302
  `
3598
3303
  };
3599
3304
  for (const [name, content] of Object.entries(files)) {
3600
- await writeFile(path5.join(dir, name), content);
3305
+ await writeFile(path6.join(dir, name), content);
3601
3306
  }
3602
3307
  }
3603
3308
  async function generateUsersModule(dir) {
@@ -3637,7 +3342,7 @@ export * from './user.schemas.js';
3637
3342
  `
3638
3343
  };
3639
3344
  for (const [name, content] of Object.entries(files)) {
3640
- await writeFile(path5.join(dir, name), content);
3345
+ await writeFile(path6.join(dir, name), content);
3641
3346
  }
3642
3347
  }
3643
3348
  async function generateEmailModule(dir) {
@@ -3694,7 +3399,7 @@ export { EmailService, emailService } from './email.service.js';
3694
3399
  `
3695
3400
  };
3696
3401
  for (const [name, content] of Object.entries(files)) {
3697
- await writeFile(path5.join(dir, name), content);
3402
+ await writeFile(path6.join(dir, name), content);
3698
3403
  }
3699
3404
  }
3700
3405
  async function generateAuditModule(dir) {
@@ -3738,7 +3443,7 @@ export { AuditService, auditService } from './audit.service.js';
3738
3443
  `
3739
3444
  };
3740
3445
  for (const [name, content] of Object.entries(files)) {
3741
- await writeFile(path5.join(dir, name), content);
3446
+ await writeFile(path6.join(dir, name), content);
3742
3447
  }
3743
3448
  }
3744
3449
  async function generateUploadModule(dir) {
@@ -3764,7 +3469,7 @@ export interface UploadOptions {
3764
3469
  `
3765
3470
  };
3766
3471
  for (const [name, content] of Object.entries(files)) {
3767
- await writeFile(path5.join(dir, name), content);
3472
+ await writeFile(path6.join(dir, name), content);
3768
3473
  }
3769
3474
  }
3770
3475
  async function generateCacheModule(dir) {
@@ -3810,7 +3515,7 @@ export { CacheService, cacheService } from './cache.service.js';
3810
3515
  `
3811
3516
  };
3812
3517
  for (const [name, content] of Object.entries(files)) {
3813
- await writeFile(path5.join(dir, name), content);
3518
+ await writeFile(path6.join(dir, name), content);
3814
3519
  }
3815
3520
  }
3816
3521
  async function generateGenericModule(dir, name) {
@@ -3824,18 +3529,184 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
3824
3529
  `
3825
3530
  };
3826
3531
  for (const [fileName, content] of Object.entries(files)) {
3827
- await writeFile(path5.join(dir, fileName), content);
3532
+ await writeFile(path6.join(dir, fileName), content);
3533
+ }
3534
+ }
3535
+ async function generateModuleFiles(moduleName, moduleDir) {
3536
+ const sourceModuleDir = path6.join(process.cwd(), "src", "modules", moduleName);
3537
+ if (await fileExists(sourceModuleDir)) {
3538
+ await copyModuleFromSource(sourceModuleDir, moduleDir);
3539
+ return;
3540
+ }
3541
+ switch (moduleName) {
3542
+ case "auth":
3543
+ await generateAuthModule(moduleDir);
3544
+ break;
3545
+ case "users":
3546
+ await generateUsersModule(moduleDir);
3547
+ break;
3548
+ case "email":
3549
+ await generateEmailModule(moduleDir);
3550
+ break;
3551
+ case "audit":
3552
+ await generateAuditModule(moduleDir);
3553
+ break;
3554
+ case "upload":
3555
+ await generateUploadModule(moduleDir);
3556
+ break;
3557
+ case "cache":
3558
+ await generateCacheModule(moduleDir);
3559
+ break;
3560
+ default:
3561
+ await generateGenericModule(moduleDir, moduleName);
3562
+ }
3563
+ }
3564
+ async function copyModuleFromSource(sourceDir, targetDir) {
3565
+ const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
3566
+ for (const entry of entries) {
3567
+ const sourcePath = path6.join(sourceDir, entry.name);
3568
+ const targetPath = path6.join(targetDir, entry.name);
3569
+ if (entry.isDirectory()) {
3570
+ await fs5.mkdir(targetPath, { recursive: true });
3571
+ await copyModuleFromSource(sourcePath, targetPath);
3572
+ } else {
3573
+ await fs5.copyFile(sourcePath, targetPath);
3574
+ }
3575
+ }
3576
+ }
3577
+ async function getModuleFiles(moduleName, moduleDir) {
3578
+ const files = {};
3579
+ const entries = await fs5.readdir(moduleDir);
3580
+ for (const entry of entries) {
3581
+ const filePath = path6.join(moduleDir, entry);
3582
+ const stat2 = await fs5.stat(filePath);
3583
+ if (stat2.isFile() && entry.endsWith(".ts")) {
3584
+ const content = await fs5.readFile(filePath, "utf-8");
3585
+ files[entry] = content;
3586
+ }
3587
+ }
3588
+ return files;
3589
+ }
3590
+ async function showDiffForModule(templateManager, moduleName, moduleDir) {
3591
+ const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
3592
+ console.log(chalk4.cyan(`
3593
+ \u{1F4CA} Changes in module "${moduleName}":
3594
+ `));
3595
+ for (const file of modifiedFiles) {
3596
+ if (file.isModified) {
3597
+ console.log(chalk4.yellow(`
3598
+ \u{1F4C4} ${file.fileName}:`));
3599
+ const currentPath = path6.join(moduleDir, file.fileName);
3600
+ const currentContent = await fs5.readFile(currentPath, "utf-8");
3601
+ const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
3602
+ if (originalContent) {
3603
+ const diff = templateManager.generateDiff(originalContent, currentContent);
3604
+ console.log(diff);
3605
+ }
3606
+ }
3607
+ }
3608
+ }
3609
+ async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
3610
+ const spinner = ora3("Analyzing files for merge...").start();
3611
+ const newFiles = {};
3612
+ const templateDir = path6.join(templateManager["templatesDir"], moduleName);
3613
+ try {
3614
+ const entries = await fs5.readdir(templateDir);
3615
+ for (const entry of entries) {
3616
+ const content = await fs5.readFile(path6.join(templateDir, entry), "utf-8");
3617
+ newFiles[entry] = content;
3618
+ }
3619
+ } catch {
3620
+ spinner.fail("Could not find template files");
3621
+ return;
3622
+ }
3623
+ const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
3624
+ spinner.stop();
3625
+ const batchAction = await InteractivePrompt.askBatchAction();
3626
+ const stats = {
3627
+ merged: 0,
3628
+ kept: 0,
3629
+ overwritten: 0,
3630
+ conflicts: 0
3631
+ };
3632
+ for (const fileInfo of modifiedFiles) {
3633
+ const fileName = fileInfo.fileName;
3634
+ const filePath = path6.join(moduleDir, fileName);
3635
+ const newContent = newFiles[fileName];
3636
+ if (!newContent) {
3637
+ continue;
3638
+ }
3639
+ let fileAction;
3640
+ if (batchAction === "merge-all") {
3641
+ fileAction = "merge";
3642
+ } else if (batchAction === "keep-all") {
3643
+ fileAction = "keep";
3644
+ } else if (batchAction === "overwrite-all") {
3645
+ fileAction = "overwrite";
3646
+ } else {
3647
+ const currentContent = await fs5.readFile(filePath, "utf-8");
3648
+ const yourLines = currentContent.split("\n").length;
3649
+ const newLines = newContent.split("\n").length;
3650
+ const choice = await InteractivePrompt.askFileAction(
3651
+ fileName,
3652
+ fileInfo.isModified,
3653
+ yourLines,
3654
+ newLines
3655
+ );
3656
+ fileAction = choice.action;
3657
+ if (fileAction === "diff") {
3658
+ const originalContent = await templateManager.getTemplate(moduleName, fileName);
3659
+ if (originalContent) {
3660
+ const diff = templateManager.generateDiff(originalContent, currentContent);
3661
+ const proceed = await InteractivePrompt.showDiffAndAsk(diff);
3662
+ fileAction = proceed ? "merge" : "keep";
3663
+ }
3664
+ }
3665
+ }
3666
+ if (fileAction === "keep" || fileAction === "skip") {
3667
+ stats.kept++;
3668
+ continue;
3669
+ }
3670
+ if (fileAction === "overwrite") {
3671
+ await fs5.writeFile(filePath, newContent, "utf-8");
3672
+ stats.overwritten++;
3673
+ continue;
3674
+ }
3675
+ if (fileAction === "merge") {
3676
+ const originalContent = await templateManager.getTemplate(moduleName, fileName);
3677
+ const currentContent = await fs5.readFile(filePath, "utf-8");
3678
+ if (originalContent) {
3679
+ const mergeResult = await templateManager.mergeFiles(
3680
+ originalContent,
3681
+ currentContent,
3682
+ newContent
3683
+ );
3684
+ await fs5.writeFile(filePath, mergeResult.merged, "utf-8");
3685
+ if (mergeResult.hasConflicts) {
3686
+ stats.conflicts++;
3687
+ InteractivePrompt.displayConflicts(mergeResult.conflicts);
3688
+ } else {
3689
+ stats.merged++;
3690
+ }
3691
+ } else {
3692
+ await fs5.writeFile(filePath, newContent, "utf-8");
3693
+ stats.overwritten++;
3694
+ }
3695
+ }
3828
3696
  }
3697
+ const files = await getModuleFiles(moduleName, moduleDir);
3698
+ await templateManager.updateManifest(moduleName, files);
3699
+ InteractivePrompt.showMergeSummary(stats);
3829
3700
  }
3830
3701
 
3831
3702
  // src/cli/commands/db.ts
3832
3703
  import { Command as Command4 } from "commander";
3833
3704
  import { execSync as execSync2, spawn } from "child_process";
3834
- import ora5 from "ora";
3835
- import chalk4 from "chalk";
3705
+ import ora4 from "ora";
3706
+ import chalk5 from "chalk";
3836
3707
  var dbCommand = new Command4("db").description("Database management commands");
3837
3708
  dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
3838
- const spinner = ora5("Running migrations...").start();
3709
+ const spinner = ora4("Running migrations...").start();
3839
3710
  try {
3840
3711
  const cmd = options.name ? `npx prisma migrate dev --name ${options.name}` : "npx prisma migrate dev";
3841
3712
  execSync2(cmd, { stdio: "inherit" });
@@ -3846,7 +3717,7 @@ dbCommand.command("migrate").description("Run database migrations").option("-n,
3846
3717
  }
3847
3718
  });
3848
3719
  dbCommand.command("push").description("Push schema changes to database (no migration)").action(async () => {
3849
- const spinner = ora5("Pushing schema...").start();
3720
+ const spinner = ora4("Pushing schema...").start();
3850
3721
  try {
3851
3722
  execSync2("npx prisma db push", { stdio: "inherit" });
3852
3723
  spinner.succeed("Schema pushed successfully!");
@@ -3856,7 +3727,7 @@ dbCommand.command("push").description("Push schema changes to database (no migra
3856
3727
  }
3857
3728
  });
3858
3729
  dbCommand.command("generate").description("Generate Prisma client").action(async () => {
3859
- const spinner = ora5("Generating Prisma client...").start();
3730
+ const spinner = ora4("Generating Prisma client...").start();
3860
3731
  try {
3861
3732
  execSync2("npx prisma generate", { stdio: "inherit" });
3862
3733
  spinner.succeed("Prisma client generated!");
@@ -3878,7 +3749,7 @@ dbCommand.command("studio").description("Open Prisma Studio").action(async () =>
3878
3749
  });
3879
3750
  });
3880
3751
  dbCommand.command("seed").description("Run database seed").action(async () => {
3881
- const spinner = ora5("Seeding database...").start();
3752
+ const spinner = ora4("Seeding database...").start();
3882
3753
  try {
3883
3754
  execSync2("npx prisma db seed", { stdio: "inherit" });
3884
3755
  spinner.succeed("Database seeded!");
@@ -3889,7 +3760,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
3889
3760
  });
3890
3761
  dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
3891
3762
  if (!options.force) {
3892
- console.log(chalk4.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
3763
+ console.log(chalk5.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
3893
3764
  const readline = await import("readline");
3894
3765
  const rl = readline.createInterface({
3895
3766
  input: process.stdin,
@@ -3904,7 +3775,7 @@ dbCommand.command("reset").description("Reset database (drop all data and re-run
3904
3775
  return;
3905
3776
  }
3906
3777
  }
3907
- const spinner = ora5("Resetting database...").start();
3778
+ const spinner = ora4("Resetting database...").start();
3908
3779
  try {
3909
3780
  execSync2("npx prisma migrate reset --force", { stdio: "inherit" });
3910
3781
  spinner.succeed("Database reset completed!");
@@ -3916,30 +3787,17 @@ dbCommand.command("reset").description("Reset database (drop all data and re-run
3916
3787
  dbCommand.command("status").description("Show migration status").action(async () => {
3917
3788
  try {
3918
3789
  execSync2("npx prisma migrate status", { stdio: "inherit" });
3919
- } catch (err) {
3790
+ } catch {
3920
3791
  error("Failed to get migration status");
3921
3792
  }
3922
3793
  });
3923
3794
 
3924
- // src/cli/commands/docs.ts
3925
- import { Command as Command5 } from "commander";
3926
- var docsCommand = new Command5("docs").description("Generate Swagger/OpenAPI documentation").option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
3927
- try {
3928
- const outputPath = await generateDocs(options.output);
3929
- success(`Documentation written to ${outputPath}`);
3930
- } catch (err) {
3931
- error(err instanceof Error ? err.message : String(err));
3932
- process.exitCode = 1;
3933
- }
3934
- });
3935
-
3936
3795
  // src/cli/index.ts
3937
- var program = new Command6();
3796
+ var program = new Command5();
3938
3797
  program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
3939
3798
  program.addCommand(initCommand);
3940
3799
  program.addCommand(generateCommand);
3941
3800
  program.addCommand(addModuleCommand);
3942
3801
  program.addCommand(dbCommand);
3943
- program.addCommand(docsCommand);
3944
3802
  program.parse();
3945
3803
  //# sourceMappingURL=index.js.map