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.
- package/.github/workflows/ci.yml +9 -4
- package/README.md +63 -2
- package/ROADMAP.md +86 -41
- package/dist/cli/index.cjs +1510 -172
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1516 -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/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/generate.ts +73 -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/commands/update.ts +221 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -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/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
|
@@ -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();
|