servcraft 0.1.7 → 0.3.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.
@@ -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
+ });
@@ -0,0 +1,221 @@
1
+ /* eslint-disable no-console */
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import inquirer from 'inquirer';
8
+ import { getProjectRoot, getModulesDir } from '../utils/helpers.js';
9
+ import { validateProject, displayError } from '../utils/error-handler.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ // Get list of available modules (same as list.ts)
15
+ const AVAILABLE_MODULES = [
16
+ 'auth',
17
+ 'users',
18
+ 'email',
19
+ 'mfa',
20
+ 'oauth',
21
+ 'rate-limit',
22
+ 'cache',
23
+ 'upload',
24
+ 'search',
25
+ 'notification',
26
+ 'webhook',
27
+ 'websocket',
28
+ 'queue',
29
+ 'payment',
30
+ 'i18n',
31
+ 'feature-flag',
32
+ 'analytics',
33
+ 'media-processing',
34
+ 'api-versioning',
35
+ 'audit',
36
+ 'swagger',
37
+ 'validation',
38
+ ];
39
+
40
+ async function getInstalledModules(): Promise<string[]> {
41
+ try {
42
+ const modulesDir = getModulesDir();
43
+
44
+ const entries = await fs.readdir(modulesDir, { withFileTypes: true });
45
+ const installedModules = entries
46
+ .filter((entry) => entry.isDirectory())
47
+ .map((entry) => entry.name)
48
+ .filter((name) => AVAILABLE_MODULES.includes(name));
49
+
50
+ return installedModules;
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ async function copyModuleFiles(moduleName: string, _projectRoot: string): Promise<void> {
57
+ const cliRoot = path.resolve(__dirname, '../../../');
58
+ const sourceModulePath = path.join(cliRoot, 'src', 'modules', moduleName);
59
+ const targetModulesDir = getModulesDir();
60
+ const targetModulePath = path.join(targetModulesDir, moduleName);
61
+
62
+ // Check if source module exists
63
+ try {
64
+ await fs.access(sourceModulePath);
65
+ } catch {
66
+ throw new Error(`Module source not found: ${moduleName}`);
67
+ }
68
+
69
+ // Copy module files
70
+ await fs.cp(sourceModulePath, targetModulePath, { recursive: true });
71
+ }
72
+
73
+ async function updateModule(moduleName: string, options: { check?: boolean }): Promise<void> {
74
+ const projectError = validateProject();
75
+ if (projectError) {
76
+ displayError(projectError);
77
+ return;
78
+ }
79
+
80
+ const projectRoot = getProjectRoot();
81
+ const installedModules = await getInstalledModules();
82
+
83
+ if (!installedModules.includes(moduleName)) {
84
+ console.log(chalk.yellow(`\n⚠ Module "${moduleName}" is not installed\n`));
85
+ console.log(
86
+ chalk.gray(`Run ${chalk.cyan(`servcraft add ${moduleName}`)} to install it first.\n`)
87
+ );
88
+ return;
89
+ }
90
+
91
+ if (options.check) {
92
+ console.log(chalk.cyan(`\nšŸ“¦ Checking updates for "${moduleName}"...\n`));
93
+ console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
94
+ console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
95
+ return;
96
+ }
97
+
98
+ // Confirm update
99
+ const { confirmed } = await inquirer.prompt([
100
+ {
101
+ type: 'confirm',
102
+ name: 'confirmed',
103
+ message: `Update "${moduleName}" module? This will overwrite existing files.`,
104
+ default: false,
105
+ },
106
+ ]);
107
+
108
+ if (!confirmed) {
109
+ console.log(chalk.yellow('\n⚠ Update cancelled\n'));
110
+ return;
111
+ }
112
+
113
+ console.log(chalk.cyan(`\nšŸ”„ Updating "${moduleName}" module...\n`));
114
+
115
+ try {
116
+ await copyModuleFiles(moduleName, projectRoot);
117
+ console.log(chalk.green(`āœ” Module "${moduleName}" updated successfully!\n`));
118
+ console.log(
119
+ chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
120
+ );
121
+ } catch (error) {
122
+ if (error instanceof Error) {
123
+ console.error(chalk.red(`\nāœ— Failed to update module: ${error.message}\n`));
124
+ }
125
+ }
126
+ }
127
+
128
+ async function updateAllModules(options: { check?: boolean }): Promise<void> {
129
+ const projectError = validateProject();
130
+ if (projectError) {
131
+ displayError(projectError);
132
+ return;
133
+ }
134
+
135
+ const installedModules = await getInstalledModules();
136
+
137
+ if (installedModules.length === 0) {
138
+ console.log(chalk.yellow('\n⚠ No modules installed\n'));
139
+ console.log(chalk.gray(`Run ${chalk.cyan('servcraft list')} to see available modules.\n`));
140
+ return;
141
+ }
142
+
143
+ if (options.check) {
144
+ console.log(chalk.cyan('\nšŸ“¦ Checking updates for all modules...\n'));
145
+ console.log(chalk.bold('Installed modules:'));
146
+ installedModules.forEach((mod) => {
147
+ console.log(` • ${chalk.cyan(mod)}`);
148
+ });
149
+ console.log();
150
+ console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
151
+ console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
152
+ return;
153
+ }
154
+
155
+ console.log(chalk.cyan(`\nšŸ“¦ Found ${installedModules.length} installed module(s):\n`));
156
+ installedModules.forEach((mod) => {
157
+ console.log(` • ${chalk.cyan(mod)}`);
158
+ });
159
+ console.log();
160
+
161
+ // Confirm update all
162
+ const { confirmed } = await inquirer.prompt([
163
+ {
164
+ type: 'confirm',
165
+ name: 'confirmed',
166
+ message: 'Update all modules? This will overwrite existing files.',
167
+ default: false,
168
+ },
169
+ ]);
170
+
171
+ if (!confirmed) {
172
+ console.log(chalk.yellow('\n⚠ Update cancelled\n'));
173
+ return;
174
+ }
175
+
176
+ console.log(chalk.cyan('\nšŸ”„ Updating all modules...\n'));
177
+
178
+ const projectRoot = getProjectRoot();
179
+ let successCount = 0;
180
+ let failCount = 0;
181
+
182
+ for (const moduleName of installedModules) {
183
+ try {
184
+ await copyModuleFiles(moduleName, projectRoot);
185
+ console.log(chalk.green(`āœ” Updated: ${moduleName}`));
186
+ successCount++;
187
+ } catch {
188
+ console.error(chalk.red(`āœ— Failed: ${moduleName}`));
189
+ failCount++;
190
+ }
191
+ }
192
+
193
+ console.log();
194
+ console.log(
195
+ chalk.bold(
196
+ `\nāœ” Update complete: ${chalk.green(successCount)} succeeded, ${chalk.red(failCount)} failed\n`
197
+ )
198
+ );
199
+
200
+ if (successCount > 0) {
201
+ console.log(
202
+ chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
203
+ );
204
+ }
205
+ }
206
+
207
+ export const updateCommand = new Command('update')
208
+ .description('Update installed modules to latest version')
209
+ .argument('[module]', 'Specific module to update')
210
+ .option('--check', 'Check for updates without applying')
211
+ .option('-y, --yes', 'Skip confirmation prompt')
212
+ .action(async (moduleName?: string, options?: { check?: boolean; yes?: boolean }) => {
213
+ // If --yes flag is provided, we'll handle it by auto-confirming in the inquirer prompts
214
+ // For now, we'll just pass through to the update functions
215
+
216
+ if (moduleName) {
217
+ await updateModule(moduleName, { check: options?.check });
218
+ } else {
219
+ await updateAllModules({ check: options?.check });
220
+ }
221
+ });
package/src/cli/index.ts CHANGED
@@ -6,6 +6,11 @@ import { generateCommand } from './commands/generate.js';
6
6
  import { addModuleCommand } from './commands/add-module.js';
7
7
  import { dbCommand } from './commands/db.js';
8
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';
12
+ import { updateCommand } from './commands/update.js';
13
+ import { completionCommand } from './commands/completion.js';
9
14
 
10
15
  const program = new Command();
11
16
 
@@ -29,4 +34,19 @@ program.addCommand(dbCommand);
29
34
  // Documentation commands
30
35
  program.addCommand(docsCommand);
31
36
 
37
+ // List modules
38
+ program.addCommand(listCommand);
39
+
40
+ // Remove module
41
+ program.addCommand(removeCommand);
42
+
43
+ // Diagnose project
44
+ program.addCommand(doctorCommand);
45
+
46
+ // Update modules
47
+ program.addCommand(updateCommand);
48
+
49
+ // Shell completion
50
+ program.addCommand(completionCommand);
51
+
32
52
  program.parse();