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