servcraft 0.1.6 → 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.
@@ -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
+ );
@@ -0,0 +1,102 @@
1
+ /* eslint-disable no-console */
2
+ import { Command } from 'commander';
3
+ import path from 'path';
4
+ import ora from 'ora';
5
+ import chalk from 'chalk';
6
+ import fs from 'fs/promises';
7
+ import inquirer from 'inquirer';
8
+ import { getModulesDir, success, error, info } from '../utils/helpers.js';
9
+ import { ServCraftError, displayError, validateProject } from '../utils/error-handler.js';
10
+
11
+ export const removeCommand = new Command('remove')
12
+ .alias('rm')
13
+ .description('Remove an installed module from your project')
14
+ .argument('<module>', 'Module to remove')
15
+ .option('-y, --yes', 'Skip confirmation prompt')
16
+ .option('--keep-env', 'Keep environment variables')
17
+ .action(async (moduleName: string, options?: { yes?: boolean; keepEnv?: boolean }) => {
18
+ // Validate project
19
+ const projectError = validateProject();
20
+ if (projectError) {
21
+ displayError(projectError);
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold.cyan('\nšŸ—‘ļø ServCraft Module Removal\n'));
26
+
27
+ const moduleDir = path.join(getModulesDir(), moduleName);
28
+
29
+ try {
30
+ // Check if module exists
31
+ const exists = await fs
32
+ .access(moduleDir)
33
+ .then(() => true)
34
+ .catch(() => false);
35
+
36
+ if (!exists) {
37
+ displayError(
38
+ new ServCraftError(`Module "${moduleName}" is not installed`, [
39
+ `Run ${chalk.cyan('servcraft list --installed')} to see installed modules`,
40
+ `Check the spelling of the module name`,
41
+ ])
42
+ );
43
+ return;
44
+ }
45
+
46
+ // Get list of files
47
+ const files = await fs.readdir(moduleDir);
48
+ const fileCount = files.length;
49
+
50
+ // Confirm removal
51
+ if (!options?.yes) {
52
+ console.log(chalk.yellow(`⚠ This will remove the "${moduleName}" module:`));
53
+ console.log(chalk.gray(` Directory: ${moduleDir}`));
54
+ console.log(chalk.gray(` Files: ${fileCount} file(s)`));
55
+ console.log();
56
+
57
+ const { confirm } = await inquirer.prompt([
58
+ {
59
+ type: 'confirm',
60
+ name: 'confirm',
61
+ message: 'Are you sure you want to remove this module?',
62
+ default: false,
63
+ },
64
+ ]);
65
+
66
+ if (!confirm) {
67
+ console.log(chalk.yellow('\nāœ– Removal cancelled\n'));
68
+ return;
69
+ }
70
+ }
71
+
72
+ const spinner = ora('Removing module...').start();
73
+
74
+ // Remove module directory
75
+ await fs.rm(moduleDir, { recursive: true, force: true });
76
+
77
+ spinner.succeed(`Module "${moduleName}" removed successfully!`);
78
+
79
+ // Show what was removed
80
+ console.log('\n' + chalk.bold('āœ“ Removed:'));
81
+ success(` src/modules/${moduleName}/ (${fileCount} files)`);
82
+
83
+ // Instructions for cleanup
84
+ if (!options?.keepEnv) {
85
+ console.log('\n' + chalk.bold('šŸ“Œ Manual cleanup needed:'));
86
+ info(' 1. Remove environment variables related to this module from .env');
87
+ info(' 2. Remove module imports from your main app file');
88
+ info(' 3. Remove related database migrations if any');
89
+ info(' 4. Update your routes if they reference this module');
90
+ } else {
91
+ console.log('\n' + chalk.bold('šŸ“Œ Manual cleanup needed:'));
92
+ info(' 1. Environment variables were kept (--keep-env flag)');
93
+ info(' 2. Remove module imports from your main app file');
94
+ info(' 3. Update your routes if they reference this module');
95
+ }
96
+
97
+ console.log();
98
+ } catch (err) {
99
+ error(err instanceof Error ? err.message : String(err));
100
+ console.log();
101
+ }
102
+ });
package/src/cli/index.ts CHANGED
@@ -5,6 +5,10 @@ import { initCommand } from './commands/init.js';
5
5
  import { generateCommand } from './commands/generate.js';
6
6
  import { addModuleCommand } from './commands/add-module.js';
7
7
  import { dbCommand } from './commands/db.js';
8
+ import { docsCommand } from './commands/docs.js';
9
+ import { listCommand } from './commands/list.js';
10
+ import { removeCommand } from './commands/remove.js';
11
+ import { doctorCommand } from './commands/doctor.js';
8
12
 
9
13
  const program = new Command();
10
14
 
@@ -25,4 +29,16 @@ program.addCommand(addModuleCommand);
25
29
  // Database commands
26
30
  program.addCommand(dbCommand);
27
31
 
32
+ // Documentation commands
33
+ program.addCommand(docsCommand);
34
+
35
+ // List modules
36
+ program.addCommand(listCommand);
37
+
38
+ // Remove module
39
+ program.addCommand(removeCommand);
40
+
41
+ // Diagnose project
42
+ program.addCommand(doctorCommand);
43
+
28
44
  program.parse();
@@ -0,0 +1,155 @@
1
+ /* eslint-disable no-console */
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+
5
+ export interface FileOperation {
6
+ type: 'create' | 'modify' | 'delete';
7
+ path: string;
8
+ content?: string;
9
+ size?: number;
10
+ }
11
+
12
+ export class DryRunManager {
13
+ private static instance: DryRunManager;
14
+ private enabled = false;
15
+ private operations: FileOperation[] = [];
16
+
17
+ private constructor() {}
18
+
19
+ static getInstance(): DryRunManager {
20
+ if (!DryRunManager.instance) {
21
+ DryRunManager.instance = new DryRunManager();
22
+ }
23
+ return DryRunManager.instance;
24
+ }
25
+
26
+ enable(): void {
27
+ this.enabled = true;
28
+ this.operations = [];
29
+ }
30
+
31
+ disable(): void {
32
+ this.enabled = false;
33
+ this.operations = [];
34
+ }
35
+
36
+ isEnabled(): boolean {
37
+ return this.enabled;
38
+ }
39
+
40
+ addOperation(operation: FileOperation): void {
41
+ if (this.enabled) {
42
+ this.operations.push(operation);
43
+ }
44
+ }
45
+
46
+ getOperations(): FileOperation[] {
47
+ return [...this.operations];
48
+ }
49
+
50
+ printSummary(): void {
51
+ if (!this.enabled || this.operations.length === 0) {
52
+ return;
53
+ }
54
+
55
+ console.log(chalk.bold.yellow('\nšŸ“‹ Dry Run - Preview of changes:\n'));
56
+ console.log(chalk.gray('No files will be written. Remove --dry-run to apply changes.\n'));
57
+
58
+ const createOps = this.operations.filter((op) => op.type === 'create');
59
+ const modifyOps = this.operations.filter((op) => op.type === 'modify');
60
+ const deleteOps = this.operations.filter((op) => op.type === 'delete');
61
+
62
+ if (createOps.length > 0) {
63
+ console.log(chalk.green.bold(`\nāœ“ Files to be created (${createOps.length}):`));
64
+ createOps.forEach((op) => {
65
+ const size = op.content ? `${op.content.length} bytes` : 'unknown size';
66
+ console.log(` ${chalk.green('+')} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
67
+ });
68
+ }
69
+
70
+ if (modifyOps.length > 0) {
71
+ console.log(chalk.yellow.bold(`\n~ Files to be modified (${modifyOps.length}):`));
72
+ modifyOps.forEach((op) => {
73
+ console.log(` ${chalk.yellow('~')} ${chalk.cyan(op.path)}`);
74
+ });
75
+ }
76
+
77
+ if (deleteOps.length > 0) {
78
+ console.log(chalk.red.bold(`\n- Files to be deleted (${deleteOps.length}):`));
79
+ deleteOps.forEach((op) => {
80
+ console.log(` ${chalk.red('-')} ${chalk.cyan(op.path)}`);
81
+ });
82
+ }
83
+
84
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
85
+ console.log(
86
+ chalk.bold(` Total operations: ${this.operations.length}`) +
87
+ chalk.gray(
88
+ ` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
89
+ )
90
+ );
91
+ console.log(chalk.gray('─'.repeat(60)));
92
+ console.log(chalk.yellow('\n⚠ This was a dry run. No files were created or modified.'));
93
+ console.log(chalk.gray(' Remove --dry-run to apply these changes.\n'));
94
+ }
95
+
96
+ // Helper to format file path relative to cwd
97
+ relativePath(filePath: string): string {
98
+ return path.relative(process.cwd(), filePath);
99
+ }
100
+ }
101
+
102
+ // Wrapper functions for file operations
103
+ export async function dryRunWriteFile(
104
+ filePath: string,
105
+ content: string,
106
+ actualWriteFn: (path: string, content: string) => Promise<void>
107
+ ): Promise<void> {
108
+ const dryRun = DryRunManager.getInstance();
109
+
110
+ if (dryRun.isEnabled()) {
111
+ dryRun.addOperation({
112
+ type: 'create',
113
+ path: dryRun.relativePath(filePath),
114
+ content,
115
+ size: content.length,
116
+ });
117
+ return;
118
+ }
119
+
120
+ await actualWriteFn(filePath, content);
121
+ }
122
+
123
+ export async function dryRunModifyFile(
124
+ filePath: string,
125
+ actualModifyFn: (path: string) => Promise<void>
126
+ ): Promise<void> {
127
+ const dryRun = DryRunManager.getInstance();
128
+
129
+ if (dryRun.isEnabled()) {
130
+ dryRun.addOperation({
131
+ type: 'modify',
132
+ path: dryRun.relativePath(filePath),
133
+ });
134
+ return;
135
+ }
136
+
137
+ await actualModifyFn(filePath);
138
+ }
139
+
140
+ export async function dryRunDeleteFile(
141
+ filePath: string,
142
+ actualDeleteFn: (path: string) => Promise<void>
143
+ ): Promise<void> {
144
+ const dryRun = DryRunManager.getInstance();
145
+
146
+ if (dryRun.isEnabled()) {
147
+ dryRun.addOperation({
148
+ type: 'delete',
149
+ path: dryRun.relativePath(filePath),
150
+ });
151
+ return;
152
+ }
153
+
154
+ await actualDeleteFn(filePath);
155
+ }