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.
- package/README.md +51 -4
- package/ROADMAP.md +327 -0
- package/dist/cli/index.cjs +3039 -222
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3058 -220
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/commands/add-module.ts +37 -36
- package/src/cli/commands/doctor.ts +8 -0
- package/src/cli/commands/generate.ts +43 -1
- package/src/cli/commands/init.ts +470 -75
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/index.ts +16 -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
|
@@ -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
|
+
}
|