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.
- package/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- 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',
|
|
150
|
-
'
|
|
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
|
+
}
|