servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -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/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -146,8 +146,20 @@ export function parseField(fieldStr: string): FieldDefinition {
146
146
 
147
147
  // Validate type
148
148
  const validTypes: FieldType[] = [
149
- 'string', 'number', 'boolean', 'date', 'datetime',
150
- 'text', 'json', 'email', 'url', 'uuid', 'int', 'float', 'decimal', 'enum'
149
+ 'string',
150
+ 'number',
151
+ 'boolean',
152
+ 'date',
153
+ 'datetime',
154
+ 'text',
155
+ 'json',
156
+ 'email',
157
+ 'url',
158
+ 'uuid',
159
+ 'int',
160
+ 'float',
161
+ 'decimal',
162
+ 'enum',
151
163
  ];
152
164
 
153
165
  let type: FieldType = 'string';
@@ -192,10 +204,7 @@ export function parseField(fieldStr: string): FieldDefinition {
192
204
  export function parseFields(fieldsStr: string): FieldDefinition[] {
193
205
  if (!fieldsStr) return [];
194
206
 
195
- return fieldsStr
196
- .split(/\s+/)
197
- .filter(Boolean)
198
- .map(parseField);
207
+ return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
199
208
  }
200
209
 
201
210
  /**
@@ -303,7 +312,7 @@ export function generatePrismaModel(
303
312
  lines.push('');
304
313
 
305
314
  // Add indexes for unique fields
306
- const uniqueFields = fields.filter(f => f.isUnique);
315
+ const uniqueFields = fields.filter((f) => f.isUnique);
307
316
  for (const field of uniqueFields) {
308
317
  lines.push(` @@index([${field.name}])`);
309
318
  }
@@ -0,0 +1,223 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+
4
+ export interface MergeChoice {
5
+ action: 'skip' | 'update' | 'overwrite' | 'diff' | 'backup-overwrite';
6
+ }
7
+
8
+ export interface FileChoice {
9
+ action: 'merge' | 'keep' | 'overwrite' | 'diff' | 'skip';
10
+ }
11
+
12
+ /**
13
+ * Interactive prompt utilities
14
+ */
15
+ export class InteractivePrompt {
16
+ /**
17
+ * Ask what to do when module already exists
18
+ */
19
+ static async askModuleExists(
20
+ moduleName: string,
21
+ hasModifications: boolean
22
+ ): Promise<MergeChoice> {
23
+ console.log(chalk.yellow(`\n⚠️ Module "${moduleName}" already exists`));
24
+
25
+ if (hasModifications) {
26
+ console.log(chalk.yellow(' Some files have been modified by you.\n'));
27
+ }
28
+
29
+ const { action } = await inquirer.prompt<{ action: MergeChoice['action'] }>([
30
+ {
31
+ type: 'list',
32
+ name: 'action',
33
+ message: 'What would you like to do?',
34
+ choices: [
35
+ {
36
+ name: '✓ Skip (keep existing files, recommended if you made custom changes)',
37
+ value: 'skip',
38
+ },
39
+ {
40
+ name: '↻ Update (merge new features, preserve your customizations)',
41
+ value: 'update',
42
+ },
43
+ {
44
+ name: '⚠ Overwrite (replace all files, will lose your changes)',
45
+ value: 'overwrite',
46
+ },
47
+ {
48
+ name: '📋 Show diff (see what changed)',
49
+ value: 'diff',
50
+ },
51
+ {
52
+ name: '💾 Backup & overwrite (save backup then replace)',
53
+ value: 'backup-overwrite',
54
+ },
55
+ ],
56
+ default: 'skip',
57
+ },
58
+ ]);
59
+
60
+ return { action };
61
+ }
62
+
63
+ /**
64
+ * Ask what to do with a specific file
65
+ */
66
+ static async askFileAction(
67
+ fileName: string,
68
+ isModified: boolean,
69
+ yourLines: number,
70
+ newLines: number
71
+ ): Promise<FileChoice> {
72
+ console.log(chalk.cyan(`\n📁 ${fileName}`));
73
+ console.log(
74
+ chalk.gray(` Your version: ${yourLines} lines${isModified ? ' (modified)' : ''}`)
75
+ );
76
+ console.log(chalk.gray(` New version: ${newLines} lines\n`));
77
+
78
+ const { action } = await inquirer.prompt<{ action: FileChoice['action'] }>([
79
+ {
80
+ type: 'list',
81
+ name: 'action',
82
+ message: 'Action for this file:',
83
+ choices: [
84
+ {
85
+ name: '[M]erge - Smart merge, preserve your changes',
86
+ value: 'merge',
87
+ },
88
+ {
89
+ name: '[K]eep - Keep your version (skip update)',
90
+ value: 'keep',
91
+ },
92
+ {
93
+ name: '[O]verwrite - Use new version',
94
+ value: 'overwrite',
95
+ },
96
+ {
97
+ name: '[D]iff - Show differences',
98
+ value: 'diff',
99
+ },
100
+ {
101
+ name: '[S]kip - Skip this file',
102
+ value: 'skip',
103
+ },
104
+ ],
105
+ default: isModified ? 'merge' : 'overwrite',
106
+ },
107
+ ]);
108
+
109
+ return { action };
110
+ }
111
+
112
+ /**
113
+ * Confirm action
114
+ */
115
+ static async confirm(message: string, defaultValue = false): Promise<boolean> {
116
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
117
+ {
118
+ type: 'confirm',
119
+ name: 'confirmed',
120
+ message,
121
+ default: defaultValue,
122
+ },
123
+ ]);
124
+
125
+ return confirmed;
126
+ }
127
+
128
+ /**
129
+ * Display diff and ask to continue
130
+ */
131
+ static async showDiffAndAsk(diff: string): Promise<boolean> {
132
+ console.log(chalk.cyan('\n📊 Differences:\n'));
133
+ console.log(diff);
134
+
135
+ return await this.confirm('\nDo you want to proceed with this change?', true);
136
+ }
137
+
138
+ /**
139
+ * Display merge conflicts
140
+ */
141
+ static displayConflicts(conflicts: string[]): void {
142
+ console.log(chalk.red('\n⚠️ Merge Conflicts Detected:\n'));
143
+ conflicts.forEach((conflict, i) => {
144
+ console.log(chalk.yellow(` ${i + 1}. ${conflict}`));
145
+ });
146
+ console.log(chalk.gray('\n Conflict markers have been added to the file:'));
147
+ console.log(chalk.gray(' <<<<<<< YOUR VERSION'));
148
+ console.log(chalk.gray(' ... your code ...'));
149
+ console.log(chalk.gray(' ======='));
150
+ console.log(chalk.gray(' ... new code ...'));
151
+ console.log(chalk.gray(' >>>>>>> NEW VERSION\n'));
152
+ }
153
+
154
+ /**
155
+ * Show backup location
156
+ */
157
+ static showBackupCreated(backupPath: string): void {
158
+ console.log(chalk.green(`\n✓ Backup created: ${chalk.cyan(backupPath)}`));
159
+ }
160
+
161
+ /**
162
+ * Show merge summary
163
+ */
164
+ static showMergeSummary(stats: {
165
+ merged: number;
166
+ kept: number;
167
+ overwritten: number;
168
+ conflicts: number;
169
+ }): void {
170
+ console.log(chalk.bold('\n📊 Merge Summary:\n'));
171
+ if (stats.merged > 0) {
172
+ console.log(chalk.green(` ✓ Merged: ${stats.merged} file(s)`));
173
+ }
174
+ if (stats.kept > 0) {
175
+ console.log(chalk.blue(` → Kept: ${stats.kept} file(s)`));
176
+ }
177
+ if (stats.overwritten > 0) {
178
+ console.log(chalk.yellow(` ⚠ Overwritten: ${stats.overwritten} file(s)`));
179
+ }
180
+ if (stats.conflicts > 0) {
181
+ console.log(chalk.red(` ⚠ Conflicts: ${stats.conflicts} file(s)`));
182
+ console.log(chalk.gray('\n Please resolve conflicts manually before committing.\n'));
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Ask for batch action on all files
188
+ */
189
+ static async askBatchAction(): Promise<
190
+ 'individual' | 'merge-all' | 'keep-all' | 'overwrite-all'
191
+ > {
192
+ const { action } = await inquirer.prompt<{
193
+ action: 'individual' | 'merge-all' | 'keep-all' | 'overwrite-all';
194
+ }>([
195
+ {
196
+ type: 'list',
197
+ name: 'action',
198
+ message: 'Multiple files found. Choose action:',
199
+ choices: [
200
+ {
201
+ name: 'Ask for each file individually (recommended)',
202
+ value: 'individual',
203
+ },
204
+ {
205
+ name: 'Merge all files automatically',
206
+ value: 'merge-all',
207
+ },
208
+ {
209
+ name: 'Keep all existing files (skip update)',
210
+ value: 'keep-all',
211
+ },
212
+ {
213
+ name: 'Overwrite all files with new versions',
214
+ value: 'overwrite-all',
215
+ },
216
+ ],
217
+ default: 'individual',
218
+ },
219
+ ]);
220
+
221
+ return action;
222
+ }
223
+ }
@@ -0,0 +1,346 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { existsSync } from 'fs';
5
+
6
+ export interface Template {
7
+ /** Template name */
8
+ name: string;
9
+ /** Template content */
10
+ content: string;
11
+ /** Template hash for change detection */
12
+ hash: string;
13
+ /** Template version */
14
+ version: string;
15
+ }
16
+
17
+ export interface ModuleManifest {
18
+ /** Module name */
19
+ name: string;
20
+ /** Module version */
21
+ version: string;
22
+ /** Files in this module */
23
+ files: Record<string, { hash: string; path: string }>;
24
+ /** Install date */
25
+ installedAt: Date;
26
+ /** Last update */
27
+ updatedAt: Date;
28
+ }
29
+
30
+ /**
31
+ * Template Manager
32
+ * Manages module templates and tracks installed versions
33
+ */
34
+ export class TemplateManager {
35
+ private templatesDir: string;
36
+ private manifestsDir: string;
37
+
38
+ constructor(projectRoot: string) {
39
+ this.templatesDir = path.join(projectRoot, '.servcraft', 'templates');
40
+ this.manifestsDir = path.join(projectRoot, '.servcraft', 'manifests');
41
+ }
42
+
43
+ /**
44
+ * Initialize template system
45
+ */
46
+ async initialize(): Promise<void> {
47
+ await fs.mkdir(this.templatesDir, { recursive: true });
48
+ await fs.mkdir(this.manifestsDir, { recursive: true });
49
+ }
50
+
51
+ /**
52
+ * Save module template
53
+ */
54
+ async saveTemplate(moduleName: string, files: Record<string, string>): Promise<void> {
55
+ await this.initialize();
56
+
57
+ const moduleTemplateDir = path.join(this.templatesDir, moduleName);
58
+ await fs.mkdir(moduleTemplateDir, { recursive: true });
59
+
60
+ // Save each file
61
+ for (const [fileName, content] of Object.entries(files)) {
62
+ const filePath = path.join(moduleTemplateDir, fileName);
63
+ await fs.writeFile(filePath, content, 'utf-8');
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get template content
69
+ */
70
+ async getTemplate(moduleName: string, fileName: string): Promise<string | null> {
71
+ try {
72
+ const filePath = path.join(this.templatesDir, moduleName, fileName);
73
+ return await fs.readFile(filePath, 'utf-8');
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Save module manifest
81
+ */
82
+ async saveManifest(moduleName: string, files: Record<string, string>): Promise<void> {
83
+ await this.initialize();
84
+
85
+ const fileHashes: Record<string, { hash: string; path: string }> = {};
86
+
87
+ for (const [fileName, content] of Object.entries(files)) {
88
+ fileHashes[fileName] = {
89
+ hash: this.hashContent(content),
90
+ path: `src/modules/${moduleName}/${fileName}`,
91
+ };
92
+ }
93
+
94
+ const manifest: ModuleManifest = {
95
+ name: moduleName,
96
+ version: '1.0.0',
97
+ files: fileHashes,
98
+ installedAt: new Date(),
99
+ updatedAt: new Date(),
100
+ };
101
+
102
+ const manifestPath = path.join(this.manifestsDir, `${moduleName}.json`);
103
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
104
+ }
105
+
106
+ /**
107
+ * Get module manifest
108
+ */
109
+ async getManifest(moduleName: string): Promise<ModuleManifest | null> {
110
+ try {
111
+ const manifestPath = path.join(this.manifestsDir, `${moduleName}.json`);
112
+ const content = await fs.readFile(manifestPath, 'utf-8');
113
+ return JSON.parse(content) as ModuleManifest;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if file has been modified by user
121
+ */
122
+ async isFileModified(
123
+ moduleName: string,
124
+ fileName: string,
125
+ currentContent: string
126
+ ): Promise<boolean> {
127
+ const manifest = await this.getManifest(moduleName);
128
+
129
+ if (!manifest || !manifest.files[fileName]) {
130
+ return false;
131
+ }
132
+
133
+ const originalHash = manifest.files[fileName].hash;
134
+ const currentHash = this.hashContent(currentContent);
135
+
136
+ return originalHash !== currentHash;
137
+ }
138
+
139
+ /**
140
+ * Get all modified files in a module
141
+ */
142
+ async getModifiedFiles(
143
+ moduleName: string,
144
+ moduleDir: string
145
+ ): Promise<
146
+ Array<{ fileName: string; isModified: boolean; originalHash: string; currentHash: string }>
147
+ > {
148
+ const manifest = await this.getManifest(moduleName);
149
+
150
+ if (!manifest) {
151
+ return [];
152
+ }
153
+
154
+ const results = [];
155
+
156
+ for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
157
+ const filePath = path.join(moduleDir, fileName);
158
+
159
+ if (!existsSync(filePath)) {
160
+ results.push({
161
+ fileName,
162
+ isModified: true,
163
+ originalHash: fileInfo.hash,
164
+ currentHash: '',
165
+ });
166
+ continue;
167
+ }
168
+
169
+ const currentContent = await fs.readFile(filePath, 'utf-8');
170
+ const currentHash = this.hashContent(currentContent);
171
+
172
+ results.push({
173
+ fileName,
174
+ isModified: fileInfo.hash !== currentHash,
175
+ originalHash: fileInfo.hash,
176
+ currentHash,
177
+ });
178
+ }
179
+
180
+ return results;
181
+ }
182
+
183
+ /**
184
+ * Create backup of module
185
+ */
186
+ async createBackup(moduleName: string, moduleDir: string): Promise<string> {
187
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
188
+ const backupDir = path.join(path.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
189
+
190
+ await this.copyDirectory(moduleDir, backupDir);
191
+
192
+ return backupDir;
193
+ }
194
+
195
+ /**
196
+ * Copy directory recursively
197
+ */
198
+ private async copyDirectory(src: string, dest: string): Promise<void> {
199
+ await fs.mkdir(dest, { recursive: true });
200
+
201
+ const entries = await fs.readdir(src, { withFileTypes: true });
202
+
203
+ for (const entry of entries) {
204
+ const srcPath = path.join(src, entry.name);
205
+ const destPath = path.join(dest, entry.name);
206
+
207
+ if (entry.isDirectory()) {
208
+ await this.copyDirectory(srcPath, destPath);
209
+ } else {
210
+ await fs.copyFile(srcPath, destPath);
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Perform 3-way merge
217
+ */
218
+ async mergeFiles(
219
+ original: string,
220
+ modified: string,
221
+ incoming: string
222
+ ): Promise<{ merged: string; hasConflicts: boolean; conflicts: string[] }> {
223
+ const conflicts: string[] = [];
224
+ let hasConflicts = false;
225
+
226
+ // Simple line-based merge
227
+ const originalLines = original.split('\n');
228
+ const modifiedLines = modified.split('\n');
229
+ const incomingLines = incoming.split('\n');
230
+
231
+ const merged: string[] = [];
232
+
233
+ // This is a simplified merge - in production, use a proper diff3 algorithm
234
+ // For now, we'll use a basic strategy:
235
+ // 1. If modified === original, use incoming (user hasn't changed it)
236
+ // 2. If modified !== original, keep modified (user changed it)
237
+ // 3. If both changed, mark as conflict
238
+
239
+ const maxLength = Math.max(originalLines.length, modifiedLines.length, incomingLines.length);
240
+
241
+ for (let i = 0; i < maxLength; i++) {
242
+ const origLine = originalLines[i] || '';
243
+ const modLine = modifiedLines[i] || '';
244
+ const incLine = incomingLines[i] || '';
245
+
246
+ if (modLine === origLine) {
247
+ // User hasn't modified, use incoming
248
+ merged.push(incLine);
249
+ } else if (incLine === origLine) {
250
+ // Template hasn't changed, keep user's modification
251
+ merged.push(modLine);
252
+ } else if (modLine === incLine) {
253
+ // Both have same change, no conflict
254
+ merged.push(modLine);
255
+ } else {
256
+ // Conflict: both modified differently
257
+ hasConflicts = true;
258
+ conflicts.push(`Line ${i + 1}: User and template both modified`);
259
+ merged.push(`<<<<<<< YOUR VERSION`);
260
+ merged.push(modLine);
261
+ merged.push(`=======`);
262
+ merged.push(incLine);
263
+ merged.push(`>>>>>>> NEW VERSION`);
264
+ }
265
+ }
266
+
267
+ return {
268
+ merged: merged.join('\n'),
269
+ hasConflicts,
270
+ conflicts,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Generate diff between two files
276
+ */
277
+ generateDiff(original: string, modified: string): string {
278
+ const originalLines = original.split('\n');
279
+ const modifiedLines = modified.split('\n');
280
+
281
+ const diff: string[] = [];
282
+ diff.push('--- Original');
283
+ diff.push('+++ Modified');
284
+ diff.push('');
285
+
286
+ const maxLength = Math.max(originalLines.length, modifiedLines.length);
287
+
288
+ for (let i = 0; i < maxLength; i++) {
289
+ const origLine = originalLines[i];
290
+ const modLine = modifiedLines[i];
291
+
292
+ if (origLine !== modLine) {
293
+ if (origLine !== undefined) {
294
+ diff.push(`- ${origLine}`);
295
+ }
296
+ if (modLine !== undefined) {
297
+ diff.push(`+ ${modLine}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ return diff.join('\n');
303
+ }
304
+
305
+ /**
306
+ * Hash content for change detection
307
+ */
308
+ private hashContent(content: string): string {
309
+ return createHash('md5').update(content).digest('hex');
310
+ }
311
+
312
+ /**
313
+ * Check if module is installed
314
+ */
315
+ async isModuleInstalled(moduleName: string): Promise<boolean> {
316
+ const manifest = await this.getManifest(moduleName);
317
+ return manifest !== null;
318
+ }
319
+
320
+ /**
321
+ * Update manifest after merge
322
+ */
323
+ async updateManifest(moduleName: string, files: Record<string, string>): Promise<void> {
324
+ const manifest = await this.getManifest(moduleName);
325
+
326
+ if (!manifest) {
327
+ await this.saveManifest(moduleName, files);
328
+ return;
329
+ }
330
+
331
+ const fileHashes: Record<string, { hash: string; path: string }> = {};
332
+
333
+ for (const [fileName, content] of Object.entries(files)) {
334
+ fileHashes[fileName] = {
335
+ hash: this.hashContent(content),
336
+ path: `src/modules/${moduleName}/${fileName}`,
337
+ };
338
+ }
339
+
340
+ manifest.files = fileHashes;
341
+ manifest.updatedAt = new Date();
342
+
343
+ const manifestPath = path.join(this.manifestsDir, `${moduleName}.json`);
344
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
345
+ }
346
+ }