servcraft 0.1.7 ā 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/ROADMAP.md +15 -8
- package/dist/cli/index.cjs +900 -174
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +919 -172
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/add-module.ts +36 -7
- package/src/cli/commands/doctor.ts +8 -0
- package/src/cli/commands/generate.ts +43 -1
- package/src/cli/commands/init.ts +29 -10
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/index.ts +12 -0
- package/src/cli/utils/dry-run.ts +155 -0
- package/src/cli/utils/error-handler.ts +184 -0
- package/src/cli/utils/helpers.ts +13 -0
package/package.json
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { EnvManager } from '../utils/env-manager.js';
|
|
17
17
|
import { TemplateManager } from '../utils/template-manager.js';
|
|
18
18
|
import { InteractivePrompt } from '../utils/interactive-prompt.js';
|
|
19
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
20
|
+
import { ErrorTypes, displayError, validateProject } from '../utils/error-handler.js';
|
|
19
21
|
|
|
20
22
|
// Pre-built modules that can be added
|
|
21
23
|
const AVAILABLE_MODULES = {
|
|
@@ -160,10 +162,17 @@ export const addModuleCommand = new Command('add')
|
|
|
160
162
|
.option('-f, --force', 'Force overwrite existing module')
|
|
161
163
|
.option('-u, --update', 'Update existing module (smart merge)')
|
|
162
164
|
.option('--skip-existing', 'Skip if module already exists')
|
|
165
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
163
166
|
.action(
|
|
164
167
|
async (
|
|
165
168
|
moduleName?: string,
|
|
166
|
-
options?: {
|
|
169
|
+
options?: {
|
|
170
|
+
list?: boolean;
|
|
171
|
+
force?: boolean;
|
|
172
|
+
update?: boolean;
|
|
173
|
+
skipExisting?: boolean;
|
|
174
|
+
dryRun?: boolean;
|
|
175
|
+
}
|
|
167
176
|
) => {
|
|
168
177
|
if (options?.list || !moduleName) {
|
|
169
178
|
console.log(chalk.bold('\nš¦ Available Modules:\n'));
|
|
@@ -180,11 +189,24 @@ export const addModuleCommand = new Command('add')
|
|
|
180
189
|
return;
|
|
181
190
|
}
|
|
182
191
|
|
|
192
|
+
// Enable dry-run mode if specified
|
|
193
|
+
const dryRun = DryRunManager.getInstance();
|
|
194
|
+
if (options?.dryRun) {
|
|
195
|
+
dryRun.enable();
|
|
196
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
const module = AVAILABLE_MODULES[moduleName as keyof typeof AVAILABLE_MODULES];
|
|
184
200
|
|
|
185
201
|
if (!module) {
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate project structure
|
|
207
|
+
const projectError = validateProject();
|
|
208
|
+
if (projectError) {
|
|
209
|
+
displayError(projectError);
|
|
188
210
|
return;
|
|
189
211
|
}
|
|
190
212
|
|
|
@@ -319,10 +341,17 @@ export const addModuleCommand = new Command('add')
|
|
|
319
341
|
}
|
|
320
342
|
}
|
|
321
343
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
344
|
+
if (!options?.dryRun) {
|
|
345
|
+
console.log('\nš Next steps:');
|
|
346
|
+
info(' 1. Configure environment variables in .env (if needed)');
|
|
347
|
+
info(' 2. Register the module in your main app file');
|
|
348
|
+
info(' 3. Run database migrations if needed');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Show dry-run summary if enabled
|
|
352
|
+
if (options?.dryRun) {
|
|
353
|
+
dryRun.printSummary();
|
|
354
|
+
}
|
|
326
355
|
} catch (err) {
|
|
327
356
|
spinner.fail('Failed to add module');
|
|
328
357
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export const doctorCommand = new Command('doctor')
|
|
5
|
+
.description('Diagnose project configuration and dependencies')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
console.log(chalk.bold.cyan('\nServCraft Doctor - Coming soon!\n'));
|
|
8
|
+
});
|
|
@@ -14,7 +14,26 @@ import {
|
|
|
14
14
|
info,
|
|
15
15
|
getModulesDir,
|
|
16
16
|
} from '../utils/helpers.js';
|
|
17
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
17
19
|
import { parseFields, type FieldDefinition } from '../utils/field-parser.js';
|
|
20
|
+
import { ErrorTypes, displayError } from '../utils/error-handler.js';
|
|
21
|
+
|
|
22
|
+
// Helper to enable dry-run mode
|
|
23
|
+
function enableDryRunIfNeeded(options: { dryRun?: boolean }): void {
|
|
24
|
+
const dryRun = DryRunManager.getInstance();
|
|
25
|
+
if (options.dryRun) {
|
|
26
|
+
dryRun.enable();
|
|
27
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helper to show dry-run summary
|
|
32
|
+
function showDryRunSummary(options: { dryRun?: boolean }): void {
|
|
33
|
+
if (options.dryRun) {
|
|
34
|
+
DryRunManager.getInstance().printSummary();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
18
37
|
import { controllerTemplate } from '../templates/controller.js';
|
|
19
38
|
import { serviceTemplate } from '../templates/service.js';
|
|
20
39
|
import { repositoryTemplate } from '../templates/repository.js';
|
|
@@ -43,7 +62,9 @@ generateCommand
|
|
|
43
62
|
.option('--prisma', 'Generate Prisma model suggestion')
|
|
44
63
|
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
45
64
|
.option('-i, --interactive', 'Interactive mode to define fields')
|
|
65
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
46
66
|
.action(async (name: string, fieldsArgs: string[], options) => {
|
|
67
|
+
enableDryRunIfNeeded(options);
|
|
47
68
|
let fields: FieldDefinition[] = [];
|
|
48
69
|
|
|
49
70
|
// Parse fields from command line or interactive mode
|
|
@@ -161,6 +182,9 @@ generateCommand
|
|
|
161
182
|
info(` ${hasFields ? '3' : '4'}. Add the Prisma model to schema.prisma`);
|
|
162
183
|
info(` ${hasFields ? '4' : '5'}. Run: npm run db:migrate`);
|
|
163
184
|
}
|
|
185
|
+
|
|
186
|
+
// Show dry-run summary if enabled
|
|
187
|
+
showDryRunSummary(options);
|
|
164
188
|
} catch (err) {
|
|
165
189
|
spinner.fail('Failed to generate module');
|
|
166
190
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -173,7 +197,9 @@ generateCommand
|
|
|
173
197
|
.alias('c')
|
|
174
198
|
.description('Generate a controller')
|
|
175
199
|
.option('-m, --module <module>', 'Target module name')
|
|
200
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
176
201
|
.action(async (name: string, options) => {
|
|
202
|
+
enableDryRunIfNeeded(options);
|
|
177
203
|
const spinner = ora('Generating controller...').start();
|
|
178
204
|
|
|
179
205
|
try {
|
|
@@ -187,7 +213,7 @@ generateCommand
|
|
|
187
213
|
|
|
188
214
|
if (await fileExists(filePath)) {
|
|
189
215
|
spinner.stop();
|
|
190
|
-
|
|
216
|
+
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
191
217
|
return;
|
|
192
218
|
}
|
|
193
219
|
|
|
@@ -195,6 +221,7 @@ generateCommand
|
|
|
195
221
|
|
|
196
222
|
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
197
223
|
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
224
|
+
showDryRunSummary(options);
|
|
198
225
|
} catch (err) {
|
|
199
226
|
spinner.fail('Failed to generate controller');
|
|
200
227
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -207,7 +234,9 @@ generateCommand
|
|
|
207
234
|
.alias('s')
|
|
208
235
|
.description('Generate a service')
|
|
209
236
|
.option('-m, --module <module>', 'Target module name')
|
|
237
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
210
238
|
.action(async (name: string, options) => {
|
|
239
|
+
enableDryRunIfNeeded(options);
|
|
211
240
|
const spinner = ora('Generating service...').start();
|
|
212
241
|
|
|
213
242
|
try {
|
|
@@ -229,6 +258,7 @@ generateCommand
|
|
|
229
258
|
|
|
230
259
|
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
231
260
|
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
261
|
+
showDryRunSummary(options);
|
|
232
262
|
} catch (err) {
|
|
233
263
|
spinner.fail('Failed to generate service');
|
|
234
264
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -241,7 +271,9 @@ generateCommand
|
|
|
241
271
|
.alias('r')
|
|
242
272
|
.description('Generate a repository')
|
|
243
273
|
.option('-m, --module <module>', 'Target module name')
|
|
274
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
244
275
|
.action(async (name: string, options) => {
|
|
276
|
+
enableDryRunIfNeeded(options);
|
|
245
277
|
const spinner = ora('Generating repository...').start();
|
|
246
278
|
|
|
247
279
|
try {
|
|
@@ -264,6 +296,7 @@ generateCommand
|
|
|
264
296
|
|
|
265
297
|
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
266
298
|
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
299
|
+
showDryRunSummary(options);
|
|
267
300
|
} catch (err) {
|
|
268
301
|
spinner.fail('Failed to generate repository');
|
|
269
302
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -276,7 +309,9 @@ generateCommand
|
|
|
276
309
|
.alias('t')
|
|
277
310
|
.description('Generate types/interfaces')
|
|
278
311
|
.option('-m, --module <module>', 'Target module name')
|
|
312
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
279
313
|
.action(async (name: string, options) => {
|
|
314
|
+
enableDryRunIfNeeded(options);
|
|
280
315
|
const spinner = ora('Generating types...').start();
|
|
281
316
|
|
|
282
317
|
try {
|
|
@@ -297,6 +332,7 @@ generateCommand
|
|
|
297
332
|
|
|
298
333
|
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
299
334
|
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
335
|
+
showDryRunSummary(options);
|
|
300
336
|
} catch (err) {
|
|
301
337
|
spinner.fail('Failed to generate types');
|
|
302
338
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -309,7 +345,9 @@ generateCommand
|
|
|
309
345
|
.alias('v')
|
|
310
346
|
.description('Generate validation schemas')
|
|
311
347
|
.option('-m, --module <module>', 'Target module name')
|
|
348
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
312
349
|
.action(async (name: string, options) => {
|
|
350
|
+
enableDryRunIfNeeded(options);
|
|
313
351
|
const spinner = ora('Generating schemas...').start();
|
|
314
352
|
|
|
315
353
|
try {
|
|
@@ -331,6 +369,7 @@ generateCommand
|
|
|
331
369
|
|
|
332
370
|
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
333
371
|
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
372
|
+
showDryRunSummary(options);
|
|
334
373
|
} catch (err) {
|
|
335
374
|
spinner.fail('Failed to generate schemas');
|
|
336
375
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -342,7 +381,9 @@ generateCommand
|
|
|
342
381
|
.command('routes <name>')
|
|
343
382
|
.description('Generate routes')
|
|
344
383
|
.option('-m, --module <module>', 'Target module name')
|
|
384
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
345
385
|
.action(async (name: string, options) => {
|
|
386
|
+
enableDryRunIfNeeded(options);
|
|
346
387
|
const spinner = ora('Generating routes...').start();
|
|
347
388
|
|
|
348
389
|
try {
|
|
@@ -365,6 +406,7 @@ generateCommand
|
|
|
365
406
|
|
|
366
407
|
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
367
408
|
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
409
|
+
showDryRunSummary(options);
|
|
368
410
|
} catch (err) {
|
|
369
411
|
spinner.fail('Failed to generate routes');
|
|
370
412
|
error(err instanceof Error ? err.message : String(err));
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -6,6 +6,7 @@ import inquirer from 'inquirer';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
import { ensureDir, writeFile, error, warn } from '../utils/helpers.js';
|
|
9
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
9
10
|
|
|
10
11
|
interface InitOptions {
|
|
11
12
|
name: string;
|
|
@@ -27,6 +28,7 @@ export const initCommand = new Command('init')
|
|
|
27
28
|
.option('--esm', 'Use ES Modules (import/export) - default')
|
|
28
29
|
.option('--cjs, --commonjs', 'Use CommonJS (require/module.exports)')
|
|
29
30
|
.option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
|
|
31
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
30
32
|
.action(
|
|
31
33
|
async (
|
|
32
34
|
name?: string,
|
|
@@ -37,8 +39,16 @@ export const initCommand = new Command('init')
|
|
|
37
39
|
esm?: boolean;
|
|
38
40
|
commonjs?: boolean;
|
|
39
41
|
db?: string;
|
|
42
|
+
dryRun?: boolean;
|
|
40
43
|
}
|
|
41
44
|
) => {
|
|
45
|
+
// Enable dry-run mode if specified
|
|
46
|
+
const dryRun = DryRunManager.getInstance();
|
|
47
|
+
if (cmdOptions?.dryRun) {
|
|
48
|
+
dryRun.enable();
|
|
49
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
console.log(
|
|
43
53
|
chalk.blue(`
|
|
44
54
|
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -274,19 +284,23 @@ export const initCommand = new Command('init')
|
|
|
274
284
|
|
|
275
285
|
spinner.succeed('Project files generated!');
|
|
276
286
|
|
|
277
|
-
// Install dependencies
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
// Install dependencies (skip in dry-run mode)
|
|
288
|
+
if (!cmdOptions?.dryRun) {
|
|
289
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
|
|
293
|
+
installSpinner.succeed('Dependencies installed!');
|
|
294
|
+
} catch {
|
|
295
|
+
installSpinner.warn('Failed to install dependencies automatically');
|
|
296
|
+
warn(' Run "npm install" manually in the project directory');
|
|
297
|
+
}
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
// Print success message
|
|
289
|
-
|
|
301
|
+
if (!cmdOptions?.dryRun) {
|
|
302
|
+
console.log('\n' + chalk.green('⨠Project created successfully!'));
|
|
303
|
+
}
|
|
290
304
|
console.log('\n' + chalk.bold('š Project structure:'));
|
|
291
305
|
console.log(`
|
|
292
306
|
${options.name}/
|
|
@@ -317,6 +331,11 @@ export const initCommand = new Command('init')
|
|
|
317
331
|
${chalk.yellow('servcraft generate service <name>')} Generate a service
|
|
318
332
|
${chalk.yellow('servcraft add auth')} Add authentication module
|
|
319
333
|
`);
|
|
334
|
+
|
|
335
|
+
// Show dry-run summary if enabled
|
|
336
|
+
if (cmdOptions?.dryRun) {
|
|
337
|
+
dryRun.printSummary();
|
|
338
|
+
}
|
|
320
339
|
} catch (err) {
|
|
321
340
|
spinner.fail('Failed to create project');
|
|
322
341
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import { getProjectRoot, getModulesDir } from '../utils/helpers.js';
|
|
6
|
+
|
|
7
|
+
// Pre-built modules that can be added
|
|
8
|
+
const AVAILABLE_MODULES: Record<string, { name: string; description: string; category: string }> = {
|
|
9
|
+
// Core
|
|
10
|
+
auth: {
|
|
11
|
+
name: 'Authentication',
|
|
12
|
+
description: 'JWT authentication with access/refresh tokens',
|
|
13
|
+
category: 'Core',
|
|
14
|
+
},
|
|
15
|
+
users: {
|
|
16
|
+
name: 'User Management',
|
|
17
|
+
description: 'User CRUD with RBAC (roles & permissions)',
|
|
18
|
+
category: 'Core',
|
|
19
|
+
},
|
|
20
|
+
email: {
|
|
21
|
+
name: 'Email Service',
|
|
22
|
+
description: 'SMTP email with templates (Handlebars)',
|
|
23
|
+
category: 'Core',
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Security
|
|
27
|
+
mfa: {
|
|
28
|
+
name: 'MFA/TOTP',
|
|
29
|
+
description: 'Two-factor authentication with QR codes',
|
|
30
|
+
category: 'Security',
|
|
31
|
+
},
|
|
32
|
+
oauth: {
|
|
33
|
+
name: 'OAuth',
|
|
34
|
+
description: 'Social login (Google, GitHub, Facebook, Twitter, Apple)',
|
|
35
|
+
category: 'Security',
|
|
36
|
+
},
|
|
37
|
+
'rate-limit': {
|
|
38
|
+
name: 'Rate Limiting',
|
|
39
|
+
description: 'Advanced rate limiting with multiple algorithms',
|
|
40
|
+
category: 'Security',
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Data & Storage
|
|
44
|
+
cache: {
|
|
45
|
+
name: 'Redis Cache',
|
|
46
|
+
description: 'Redis caching with TTL & invalidation',
|
|
47
|
+
category: 'Data & Storage',
|
|
48
|
+
},
|
|
49
|
+
upload: {
|
|
50
|
+
name: 'File Upload',
|
|
51
|
+
description: 'File upload with local/S3/Cloudinary storage',
|
|
52
|
+
category: 'Data & Storage',
|
|
53
|
+
},
|
|
54
|
+
search: {
|
|
55
|
+
name: 'Search',
|
|
56
|
+
description: 'Full-text search with Elasticsearch/Meilisearch',
|
|
57
|
+
category: 'Data & Storage',
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Communication
|
|
61
|
+
notification: {
|
|
62
|
+
name: 'Notifications',
|
|
63
|
+
description: 'Email, SMS, Push notifications',
|
|
64
|
+
category: 'Communication',
|
|
65
|
+
},
|
|
66
|
+
webhook: {
|
|
67
|
+
name: 'Webhooks',
|
|
68
|
+
description: 'Outgoing webhooks with HMAC signatures & retry',
|
|
69
|
+
category: 'Communication',
|
|
70
|
+
},
|
|
71
|
+
websocket: {
|
|
72
|
+
name: 'WebSockets',
|
|
73
|
+
description: 'Real-time communication with Socket.io',
|
|
74
|
+
category: 'Communication',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Background Processing
|
|
78
|
+
queue: {
|
|
79
|
+
name: 'Queue/Jobs',
|
|
80
|
+
description: 'Background jobs with Bull/BullMQ & cron scheduling',
|
|
81
|
+
category: 'Background Processing',
|
|
82
|
+
},
|
|
83
|
+
'media-processing': {
|
|
84
|
+
name: 'Media Processing',
|
|
85
|
+
description: 'Image/video processing with FFmpeg',
|
|
86
|
+
category: 'Background Processing',
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Monitoring & Analytics
|
|
90
|
+
audit: {
|
|
91
|
+
name: 'Audit Logs',
|
|
92
|
+
description: 'Activity logging and audit trail',
|
|
93
|
+
category: 'Monitoring & Analytics',
|
|
94
|
+
},
|
|
95
|
+
analytics: {
|
|
96
|
+
name: 'Analytics/Metrics',
|
|
97
|
+
description: 'Prometheus metrics & event tracking',
|
|
98
|
+
category: 'Monitoring & Analytics',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Internationalization
|
|
102
|
+
i18n: {
|
|
103
|
+
name: 'i18n/Localization',
|
|
104
|
+
description: 'Multi-language support with 7+ locales',
|
|
105
|
+
category: 'Internationalization',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// API Management
|
|
109
|
+
'feature-flag': {
|
|
110
|
+
name: 'Feature Flags',
|
|
111
|
+
description: 'A/B testing & progressive rollout',
|
|
112
|
+
category: 'API Management',
|
|
113
|
+
},
|
|
114
|
+
'api-versioning': {
|
|
115
|
+
name: 'API Versioning',
|
|
116
|
+
description: 'Multiple API versions support',
|
|
117
|
+
category: 'API Management',
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Payments
|
|
121
|
+
payment: {
|
|
122
|
+
name: 'Payments',
|
|
123
|
+
description: 'Payment processing (Stripe, PayPal, Mobile Money)',
|
|
124
|
+
category: 'Payments',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
async function getInstalledModules(): Promise<string[]> {
|
|
129
|
+
try {
|
|
130
|
+
const modulesDir = getModulesDir();
|
|
131
|
+
const entries = await fs.readdir(modulesDir, { withFileTypes: true });
|
|
132
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isServercraftProject(): boolean {
|
|
139
|
+
try {
|
|
140
|
+
getProjectRoot();
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const listCommand = new Command('list')
|
|
148
|
+
.alias('ls')
|
|
149
|
+
.description('List available and installed modules')
|
|
150
|
+
.option('-a, --available', 'Show only available modules')
|
|
151
|
+
.option('-i, --installed', 'Show only installed modules')
|
|
152
|
+
.option('-c, --category <category>', 'Filter by category')
|
|
153
|
+
.option('--json', 'Output as JSON')
|
|
154
|
+
.action(
|
|
155
|
+
async (options: {
|
|
156
|
+
available?: boolean;
|
|
157
|
+
installed?: boolean;
|
|
158
|
+
category?: string;
|
|
159
|
+
json?: boolean;
|
|
160
|
+
}) => {
|
|
161
|
+
const installedModules = await getInstalledModules();
|
|
162
|
+
const isProject = isServercraftProject();
|
|
163
|
+
|
|
164
|
+
if (options.json) {
|
|
165
|
+
const output: Record<string, unknown> = {
|
|
166
|
+
available: Object.entries(AVAILABLE_MODULES).map(([key, mod]) => ({
|
|
167
|
+
id: key,
|
|
168
|
+
...mod,
|
|
169
|
+
installed: installedModules.includes(key),
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (isProject) {
|
|
174
|
+
output.installed = installedModules;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(JSON.stringify(output, null, 2));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Group modules by category
|
|
182
|
+
const byCategory: Record<
|
|
183
|
+
string,
|
|
184
|
+
Array<{ id: string; name: string; description: string; installed: boolean }>
|
|
185
|
+
> = {};
|
|
186
|
+
|
|
187
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
188
|
+
if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!byCategory[mod.category]) {
|
|
193
|
+
byCategory[mod.category] = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
byCategory[mod.category].push({
|
|
197
|
+
id: key,
|
|
198
|
+
name: mod.name,
|
|
199
|
+
description: mod.description,
|
|
200
|
+
installed: installedModules.includes(key),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Show installed modules only
|
|
205
|
+
if (options.installed) {
|
|
206
|
+
if (!isProject) {
|
|
207
|
+
console.log(chalk.yellow('\nā Not in a Servcraft project directory\n'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(chalk.bold('\nš¦ Installed Modules:\n'));
|
|
212
|
+
|
|
213
|
+
if (installedModules.length === 0) {
|
|
214
|
+
console.log(chalk.gray(' No modules installed yet.\n'));
|
|
215
|
+
console.log(` Run ${chalk.cyan('servcraft add <module>')} to add a module.\n`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const modId of installedModules) {
|
|
220
|
+
const mod = AVAILABLE_MODULES[modId];
|
|
221
|
+
if (mod) {
|
|
222
|
+
console.log(` ${chalk.green('ā')} ${chalk.cyan(modId.padEnd(18))} ${mod.name}`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(
|
|
225
|
+
` ${chalk.green('ā')} ${chalk.cyan(modId.padEnd(18))} ${chalk.gray('(custom module)')}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`\n Total: ${chalk.bold(installedModules.length)} module(s) installed\n`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Show available modules (default or --available)
|
|
235
|
+
console.log(chalk.bold('\nš¦ Available Modules\n'));
|
|
236
|
+
|
|
237
|
+
if (isProject) {
|
|
238
|
+
console.log(
|
|
239
|
+
chalk.gray(` ${chalk.green('ā')} = installed ${chalk.dim('ā')} = not installed\n`)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const [category, modules] of Object.entries(byCategory)) {
|
|
244
|
+
console.log(chalk.bold.blue(` ${category}`));
|
|
245
|
+
console.log(chalk.gray(' ' + 'ā'.repeat(40)));
|
|
246
|
+
|
|
247
|
+
for (const mod of modules) {
|
|
248
|
+
const status = isProject ? (mod.installed ? chalk.green('ā') : chalk.dim('ā')) : ' ';
|
|
249
|
+
const nameColor = mod.installed ? chalk.green : chalk.cyan;
|
|
250
|
+
console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
|
|
251
|
+
console.log(` ${chalk.gray(mod.description)}`);
|
|
252
|
+
}
|
|
253
|
+
console.log();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Summary
|
|
257
|
+
const totalAvailable = Object.keys(AVAILABLE_MODULES).length;
|
|
258
|
+
const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES[m]).length;
|
|
259
|
+
|
|
260
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
261
|
+
console.log(
|
|
262
|
+
` ${chalk.bold(totalAvailable)} modules available` +
|
|
263
|
+
(isProject ? ` | ${chalk.green.bold(totalInstalled)} installed` : '')
|
|
264
|
+
);
|
|
265
|
+
console.log();
|
|
266
|
+
|
|
267
|
+
// Usage hints
|
|
268
|
+
console.log(chalk.bold(' Usage:'));
|
|
269
|
+
console.log(` ${chalk.yellow('servcraft add <module>')} Add a module`);
|
|
270
|
+
console.log(` ${chalk.yellow('servcraft list --installed')} Show installed only`);
|
|
271
|
+
console.log(` ${chalk.yellow('servcraft list --category Security')} Filter by category`);
|
|
272
|
+
console.log();
|
|
273
|
+
}
|
|
274
|
+
);
|