devcompass 2.7.0 → 2.8.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.
@@ -1,253 +1,335 @@
1
1
  // src/commands/fix.js
2
- const chalk = require('chalk');
3
- const ora = require('ora');
4
2
  const { execSync } = require('child_process');
5
- const readline = require('readline');
3
+ const fs = require('fs');
6
4
  const path = require('path');
7
-
8
- const { findUnusedDeps } = require('../analyzers/unused-deps');
9
- const { findOutdatedDeps } = require('../analyzers/outdated');
10
- const { checkEcosystemAlerts } = require('../alerts');
11
- const { getSeverityDisplay } = require('../alerts/formatter');
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+ const ProgressTracker = require('../utils/progress-tracker');
8
+ const FixReport = require('../utils/fix-report');
9
+ const BackupManager = require('../utils/backup-manager');
12
10
  const { clearCache } = require('../cache/manager');
13
11
 
14
- async function fix(options) {
12
+ async function fix(options = {}) {
15
13
  const projectPath = options.path || process.cwd();
16
-
17
- console.log('\n');
18
- console.log(chalk.cyan.bold('šŸ”§ DevCompass Fix') + ' - Analyzing and fixing your project...\n');
19
-
20
- const spinner = ora({
21
- text: 'Analyzing project...',
22
- color: 'cyan'
23
- }).start();
24
-
14
+ const autoApply = options.yes || options.y || false;
15
+ const dryRun = options.dryRun || options.dry || false;
16
+
17
+ console.log(chalk.bold.cyan('\nšŸ”§ DevCompass Fix\n'));
18
+
19
+ if (dryRun) {
20
+ console.log(chalk.yellow('šŸ“‹ DRY RUN MODE - No changes will be made\n'));
21
+ }
22
+
23
+ // Check if package.json exists
24
+ const packageJsonPath = path.join(projectPath, 'package.json');
25
+ if (!fs.existsSync(packageJsonPath)) {
26
+ console.error(chalk.red('āŒ No package.json found in this directory'));
27
+ process.exit(1);
28
+ }
29
+
30
+ // Initialize report and backup
31
+ const report = new FixReport();
32
+ const backupManager = new BackupManager(projectPath);
33
+
25
34
  try {
26
- const fs = require('fs');
27
- const packageJsonPath = path.join(projectPath, 'package.json');
28
-
29
- if (!fs.existsSync(packageJsonPath)) {
30
- spinner.fail(chalk.red('No package.json found'));
31
- process.exit(1);
35
+ // Step 1: Analyze what needs fixing
36
+ console.log(chalk.bold('Step 1: Analyzing issues...\n'));
37
+ const spinner = ora('Scanning project...').start();
38
+
39
+ const { alerts, unused, outdated, security } = await analyzeProject(projectPath);
40
+
41
+ spinner.succeed('Analysis complete');
42
+
43
+ // Calculate total fixes needed
44
+ const totalFixes = calculateTotalFixes(alerts, unused, outdated, security);
45
+
46
+ if (totalFixes === 0) {
47
+ console.log(chalk.green('\n✨ No issues to fix! Your project is healthy.\n'));
48
+ return;
32
49
  }
50
+
51
+ // Step 2: Show what will be fixed
52
+ console.log(chalk.bold('\nStep 2: Planned fixes\n'));
53
+ displayPlannedFixes(alerts, unused, outdated, security, dryRun);
54
+
55
+ // Step 3: Get confirmation (unless auto-apply or dry-run)
56
+ if (!dryRun && !autoApply) {
57
+ console.log(chalk.bold('\n' + '='.repeat(70)));
58
+ const readline = require('readline').createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout
61
+ });
62
+
63
+ const answer = await new Promise(resolve => {
64
+ readline.question(chalk.yellow('āš ļø Apply these fixes? (y/N): '), resolve);
65
+ });
66
+ readline.close();
67
+
68
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
69
+ console.log(chalk.gray('\nFix cancelled by user.\n'));
70
+ return;
71
+ }
72
+ }
73
+
74
+ if (dryRun) {
75
+ console.log(chalk.cyan('\nāœ“ Dry run complete. No changes were made.\n'));
76
+ return;
77
+ }
78
+
79
+ // Step 4: Create backup
80
+ console.log(chalk.bold('\nStep 4: Creating backup...\n'));
81
+ const backupPath = await backupManager.createBackup();
82
+ if (backupPath) {
83
+ console.log(chalk.green(`āœ“ Backup created: ${path.basename(backupPath)}\n`));
84
+ }
85
+
86
+ // Step 5: Apply fixes with progress tracking
87
+ console.log(chalk.bold('Step 5: Applying fixes...\n'));
33
88
 
34
- const projectPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
35
- const dependencies = {
36
- ...(projectPackageJson.dependencies || {}),
37
- ...(projectPackageJson.devDependencies || {})
38
- };
39
-
40
- // Check for critical alerts first
41
- spinner.text = 'Checking for critical issues...';
42
- const alerts = await checkEcosystemAlerts(projectPath, dependencies);
43
- const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
44
-
45
- // Find unused dependencies
46
- spinner.text = 'Finding unused dependencies...';
47
- const unusedDeps = await findUnusedDeps(projectPath, dependencies);
48
-
49
- // Find outdated packages
50
- spinner.text = 'Checking for updates...';
51
- const outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
52
-
53
- spinner.succeed(chalk.green('Analysis complete!\n'));
54
-
55
- // Show what will be fixed
56
- await showFixPlan(criticalAlerts, unusedDeps, outdatedDeps, options, projectPath);
57
-
89
+ const progress = new ProgressTracker(totalFixes);
90
+ progress.start('Starting fixes...');
91
+
92
+ // Fix critical security issues first
93
+ if (security.metadata.critical > 0 || security.metadata.high > 0) {
94
+ progress.update('Fixing security vulnerabilities...');
95
+ await fixSecurityIssues(projectPath, report, progress);
96
+ }
97
+
98
+ // Fix ecosystem alerts
99
+ if (alerts.length > 0) {
100
+ for (const alert of alerts) {
101
+ if (alert.severity === 'critical' || alert.severity === 'high') {
102
+ progress.update(`Fixing ${alert.package}...`);
103
+ await fixAlert(alert, projectPath, report, progress);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Remove unused dependencies
109
+ if (unused.length > 0) {
110
+ for (const dep of unused) {
111
+ progress.update(`Removing ${dep}...`);
112
+ await removeUnusedDependency(dep, projectPath, report, progress);
113
+ }
114
+ }
115
+
116
+ // Update outdated packages (only patch/minor)
117
+ if (outdated.length > 0) {
118
+ for (const pkg of outdated) {
119
+ if (pkg.versionsBehind !== 'major') {
120
+ progress.update(`Updating ${pkg.name}...`);
121
+ await updatePackage(pkg, projectPath, report, progress);
122
+ }
123
+ }
124
+ }
125
+
126
+ progress.succeed('All fixes applied!');
127
+
128
+ // Step 6: Clear cache
129
+ console.log(chalk.bold('\nStep 6: Clearing cache...\n'));
130
+ clearCache(projectPath);
131
+ console.log(chalk.green('āœ“ Cache cleared\n'));
132
+
133
+ // Step 7: Generate and display report
134
+ report.finalize();
135
+ report.display();
136
+
137
+ // Save report to file
138
+ const reportPath = await report.save(projectPath);
139
+ if (reportPath) {
140
+ console.log(chalk.cyan(`šŸ“„ Full report saved to: ${path.basename(reportPath)}\n`));
141
+ }
142
+
143
+ // Final summary
144
+ const summary = report.getSummary();
145
+ if (summary.totalFixes > 0) {
146
+ console.log(chalk.green.bold(`āœ“ Successfully applied ${summary.totalFixes} fix(es)!\n`));
147
+ console.log(chalk.gray('šŸ’” TIP: Run'), chalk.cyan('devcompass analyze'), chalk.gray('to verify improvements\n'));
148
+ }
149
+
150
+ if (summary.totalErrors > 0) {
151
+ console.log(chalk.yellow(`āš ļø ${summary.totalErrors} error(s) occurred during fix\n`));
152
+ }
153
+
58
154
  } catch (error) {
59
- spinner.fail(chalk.red('Analysis failed'));
60
- console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
155
+ console.error(chalk.red('\nāŒ Fix failed:'), error.message);
156
+ console.log(chalk.yellow('\nšŸ’” TIP: Your backup is available in .devcompass-backups/\n'));
61
157
  process.exit(1);
62
158
  }
63
159
  }
64
160
 
65
- async function showFixPlan(criticalAlerts, unusedDeps, outdatedDeps, options, projectPath) {
66
- const actions = [];
67
-
68
- // Critical alerts
161
+ // Helper functions
162
+
163
+ async function analyzeProject(projectPath) {
164
+ // Load existing analyzers
165
+ const alerts = require('../alerts');
166
+ const unusedDeps = require('../analyzers/unused-deps');
167
+ const outdated = require('../analyzers/outdated');
168
+ const security = require('../analyzers/security');
169
+
170
+ const packageJson = JSON.parse(
171
+ fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8')
172
+ );
173
+
174
+ const dependencies = {
175
+ ...packageJson.dependencies,
176
+ ...packageJson.devDependencies
177
+ };
178
+
179
+ // Run analyses
180
+ const alertsList = await alerts.checkEcosystemAlerts(projectPath, dependencies);
181
+ const unusedList = await unusedDeps.findUnusedDeps(projectPath, dependencies);
182
+ const outdatedList = await outdated.findOutdatedDeps(projectPath, dependencies);
183
+ const securityData = await security.checkSecurity(projectPath);
184
+
185
+ return {
186
+ alerts: alertsList,
187
+ unused: unusedList,
188
+ outdated: outdatedList,
189
+ security: securityData
190
+ };
191
+ }
192
+
193
+ function calculateTotalFixes(alerts, unused, outdated, security) {
194
+ let total = 0;
195
+
196
+ // Count security fixes
197
+ if (security.metadata.critical > 0 || security.metadata.high > 0) {
198
+ total += 1; // npm audit fix counts as one operation
199
+ }
200
+
201
+ // Count critical/high alerts
202
+ total += alerts.filter(a => a.severity === 'critical' || a.severity === 'high').length;
203
+
204
+ // Count unused deps
205
+ total += unused.length;
206
+
207
+ // Count safe updates (patch/minor only)
208
+ total += outdated.filter(pkg => pkg.versionsBehind !== 'major').length;
209
+
210
+ return total;
211
+ }
212
+
213
+ function displayPlannedFixes(alerts, unused, outdated, security, dryRun) {
214
+ let fixCount = 0;
215
+
216
+ // Security fixes
217
+ if (security.metadata.critical > 0 || security.metadata.high > 0) {
218
+ console.log(chalk.red.bold('šŸ”“ CRITICAL SECURITY FIXES'));
219
+ console.log(` ${chalk.cyan('→')} Run npm audit fix to resolve ${security.metadata.critical + security.metadata.high} vulnerabilities`);
220
+ fixCount++;
221
+ }
222
+
223
+ // Ecosystem alerts
224
+ const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
69
225
  if (criticalAlerts.length > 0) {
70
- console.log(chalk.red.bold('šŸ”“ CRITICAL ISSUES TO FIX:\n'));
71
-
226
+ console.log(chalk.red.bold('\nšŸ”“ CRITICAL PACKAGE ISSUES'));
72
227
  criticalAlerts.forEach(alert => {
73
- const display = getSeverityDisplay(alert.severity);
74
- console.log(`${display.emoji} ${chalk.bold(alert.package)}@${alert.version}`);
75
- console.log(` ${chalk.gray('Issue:')} ${alert.title}`);
76
-
77
- if (alert.fix && /^\d+\.\d+/.test(alert.fix)) {
78
- console.log(` ${chalk.green('Fix:')} Upgrade to ${alert.fix}\n`);
79
- actions.push({
80
- type: 'upgrade',
81
- package: alert.package,
82
- version: alert.fix,
83
- reason: 'Critical security/stability issue'
84
- });
85
- } else {
86
- console.log(` ${chalk.yellow('Fix:')} ${alert.fix}\n`);
87
- }
228
+ console.log(` ${chalk.cyan(alert.package)}`);
229
+ console.log(` ${chalk.gray('→')} ${alert.title}`);
230
+ console.log(` ${chalk.gray('Fix:')} ${alert.fix}`);
231
+ fixCount++;
88
232
  });
89
-
90
- console.log('━'.repeat(70) + '\n');
91
233
  }
92
-
234
+
93
235
  // Unused dependencies
94
- if (unusedDeps.length > 0) {
95
- console.log(chalk.yellow.bold('🧹 UNUSED DEPENDENCIES TO REMOVE:\n'));
96
-
97
- unusedDeps.forEach(dep => {
98
- console.log(` ${chalk.red('ā—')} ${dep.name}`);
99
- actions.push({
100
- type: 'uninstall',
101
- package: dep.name,
102
- reason: 'Not used in project'
103
- });
236
+ if (unused.length > 0) {
237
+ console.log(chalk.yellow.bold('\n🟔 UNUSED DEPENDENCIES'));
238
+ unused.forEach(dep => {
239
+ console.log(` ${chalk.cyan(dep.name)}`);
240
+ console.log(` ${chalk.gray('→')} Will be removed`);
241
+ fixCount++;
104
242
  });
105
-
106
- console.log('\n' + '━'.repeat(70) + '\n');
107
243
  }
108
-
109
- // Safe updates (patch/minor only)
110
- const safeUpdates = outdatedDeps.filter(dep =>
111
- dep.versionsBehind === 'patch update' || dep.versionsBehind === 'minor update'
112
- );
113
-
244
+
245
+ // Safe updates
246
+ const safeUpdates = outdated.filter(pkg => pkg.versionsBehind !== 'major');
114
247
  if (safeUpdates.length > 0) {
115
- console.log(chalk.cyan.bold('ā¬†ļø SAFE UPDATES (patch/minor):\n'));
116
-
117
- safeUpdates.forEach(dep => {
118
- console.log(` ${dep.name}: ${chalk.yellow(dep.current)} → ${chalk.green(dep.latest)} ${chalk.gray(`(${dep.versionsBehind})`)}`);
119
- actions.push({
120
- type: 'update',
121
- package: dep.name,
122
- version: dep.latest,
123
- reason: dep.versionsBehind
124
- });
248
+ console.log(chalk.cyan.bold('\nšŸ”µ SAFE UPDATES (patch/minor)'));
249
+ safeUpdates.forEach(pkg => {
250
+ console.log(` ${chalk.cyan(pkg.name)}`);
251
+ console.log(` ${chalk.gray('→')} ${pkg.current} → ${pkg.latest}`);
252
+ fixCount++;
125
253
  });
126
-
127
- console.log('\n' + '━'.repeat(70) + '\n');
128
254
  }
129
-
130
- // Major updates (show but don't auto-apply)
131
- const majorUpdates = outdatedDeps.filter(dep => dep.versionsBehind === 'major update');
132
-
255
+
256
+ // Major updates (will be skipped)
257
+ const majorUpdates = outdated.filter(pkg => pkg.versionsBehind === 'major');
133
258
  if (majorUpdates.length > 0) {
134
- console.log(chalk.gray.bold('āš ļø MAJOR UPDATES (skipped - may have breaking changes):\n'));
135
-
136
- majorUpdates.forEach(dep => {
137
- console.log(` ${chalk.gray(dep.name)}: ${dep.current} → ${dep.latest}`);
259
+ console.log(chalk.gray.bold('\n⚪ SKIPPED (major updates - manual review required)'));
260
+ majorUpdates.forEach(pkg => {
261
+ console.log(` ${chalk.gray(pkg.name)}`);
262
+ console.log(` ${chalk.gray('→')} ${pkg.current} → ${pkg.latest} (breaking changes possible)`);
138
263
  });
139
-
140
- console.log(chalk.gray('\n Run these manually after reviewing changelog:\n'));
141
- majorUpdates.forEach(dep => {
142
- console.log(chalk.gray(` npm install ${dep.name}@${dep.latest}`));
143
- });
144
-
145
- console.log('\n' + '━'.repeat(70) + '\n');
146
264
  }
265
+
266
+ console.log(chalk.bold('\n' + '='.repeat(70)));
267
+ console.log(chalk.bold(`Total fixes to apply: ${chalk.cyan(fixCount)}`));
147
268
 
148
- if (actions.length === 0) {
149
- console.log(chalk.green('✨ Everything looks good! No fixes needed.\n'));
150
- return;
269
+ if (dryRun) {
270
+ console.log(chalk.yellow('(Dry run - no changes will be made)'));
151
271
  }
152
-
153
- // Summary
154
- console.log(chalk.bold('šŸ“Š FIX SUMMARY:\n'));
155
- console.log(` Critical fixes: ${criticalAlerts.length}`);
156
- console.log(` Remove unused: ${unusedDeps.length}`);
157
- console.log(` Safe updates: ${safeUpdates.length}`);
158
- console.log(` Skipped major: ${majorUpdates.length}\n`);
159
-
160
- console.log('━'.repeat(70) + '\n');
161
-
162
- // Confirm
163
- if (options.yes) {
164
- await applyFixes(actions, projectPath);
165
- } else {
166
- const confirmed = await askConfirmation('\nā“ Apply these fixes?');
167
-
168
- if (confirmed) {
169
- await applyFixes(actions, projectPath);
170
- } else {
171
- console.log(chalk.yellow('\nāš ļø Fix cancelled. No changes made.\n'));
172
- }
272
+ }
273
+
274
+ async function fixSecurityIssues(projectPath, report, progress) {
275
+ try {
276
+ execSync('npm audit fix', {
277
+ cwd: projectPath,
278
+ stdio: 'pipe'
279
+ });
280
+ report.addFix('security', 'npm audit', 'Fixed security vulnerabilities');
281
+ } catch (error) {
282
+ report.addError('npm audit', error);
283
+ progress.warn('Some security issues could not be auto-fixed');
173
284
  }
174
285
  }
175
286
 
176
- async function applyFixes(actions, projectPath) {
177
- console.log(chalk.cyan.bold('\nšŸ”§ Applying fixes...\n'));
178
-
179
- const spinner = ora('Processing...').start();
180
-
287
+ async function fixAlert(alert, projectPath, report, progress) {
181
288
  try {
182
- // Group by type
183
- const toUninstall = actions.filter(a => a.type === 'uninstall').map(a => a.package);
184
- const toUpgrade = actions.filter(a => a.type === 'upgrade');
185
- const toUpdate = actions.filter(a => a.type === 'update');
186
-
187
- // Uninstall unused
188
- if (toUninstall.length > 0) {
189
- spinner.text = `Removing ${toUninstall.length} unused packages...`;
190
-
191
- const cmd = `npm uninstall ${toUninstall.join(' ')}`;
192
- execSync(cmd, { stdio: 'pipe' });
193
-
194
- spinner.succeed(chalk.green(`āœ… Removed ${toUninstall.length} unused packages`));
195
- spinner.start();
196
- }
197
-
198
- // Upgrade critical packages
199
- for (const action of toUpgrade) {
200
- spinner.text = `Fixing ${action.package}@${action.version}...`;
201
-
202
- const cmd = `npm install ${action.package}@${action.version}`;
203
- execSync(cmd, { stdio: 'pipe' });
204
-
205
- spinner.succeed(chalk.green(`āœ… Fixed ${action.package}@${action.version}`));
206
- spinner.start();
207
- }
208
-
209
- // Update safe packages
210
- if (toUpdate.length > 0) {
211
- spinner.text = `Updating ${toUpdate.length} packages...`;
212
-
213
- for (const action of toUpdate) {
214
- const cmd = `npm install ${action.package}@${action.version}`;
215
- execSync(cmd, { stdio: 'pipe' });
216
- }
217
-
218
- spinner.succeed(chalk.green(`āœ… Updated ${toUpdate.length} packages`));
219
- } else {
220
- spinner.stop();
221
- }
222
-
223
- console.log(chalk.green.bold('\n✨ All fixes applied successfully!\n'));
224
- console.log(chalk.cyan('šŸ’” Run') + chalk.bold(' devcompass analyze ') + chalk.cyan('to see the new health score.\n'));
225
-
226
- // Clear cache after fixes - ADDED
227
- spinner.text = 'Clearing cache...';
228
- clearCache(projectPath);
229
- spinner.succeed(chalk.gray('Cache cleared'));
230
-
289
+ const pkg = alert.package.split('@')[0];
290
+ const version = alert.fix;
291
+
292
+ execSync(`npm install ${pkg}@${version}`, {
293
+ cwd: projectPath,
294
+ stdio: 'pipe'
295
+ });
296
+
297
+ report.addFix('alert', pkg, `Updated to ${version}`, {
298
+ from: alert.package.split('@')[1],
299
+ to: version
300
+ });
231
301
  } catch (error) {
232
- spinner.fail(chalk.red('Fix failed'));
233
- console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
234
- console.log(chalk.yellow('šŸ’” You may need to fix this manually.\n'));
235
- process.exit(1);
302
+ report.addError(alert.package, error);
236
303
  }
237
304
  }
238
305
 
239
- function askConfirmation(question) {
240
- const rl = readline.createInterface({
241
- input: process.stdin,
242
- output: process.stdout
243
- });
244
-
245
- return new Promise(resolve => {
246
- rl.question(chalk.cyan(question) + chalk.gray(' (y/N): '), answer => {
247
- rl.close();
248
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
306
+ async function removeUnusedDependency(dep, projectPath, report, progress) {
307
+ try {
308
+ execSync(`npm uninstall ${dep.name}`, {
309
+ cwd: projectPath,
310
+ stdio: 'pipe'
311
+ });
312
+
313
+ report.addFix('unused', dep.name, 'Removed unused dependency');
314
+ } catch (error) {
315
+ report.addError(dep.name, error);
316
+ }
317
+ }
318
+
319
+ async function updatePackage(pkg, projectPath, report, progress) {
320
+ try {
321
+ execSync(`npm install ${pkg.name}@${pkg.latest}`, {
322
+ cwd: projectPath,
323
+ stdio: 'pipe'
249
324
  });
250
- });
325
+
326
+ report.addFix('update', pkg.name, `Updated to ${pkg.latest}`, {
327
+ from: pkg.current,
328
+ to: pkg.latest
329
+ });
330
+ } catch (error) {
331
+ report.addError(pkg.name, error);
332
+ }
251
333
  }
252
334
 
253
- module.exports = { fix };
335
+ module.exports = fix;
@@ -0,0 +1,113 @@
1
+ // src/utils/backup-manager.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ class BackupManager {
7
+ constructor(projectPath) {
8
+ this.projectPath = projectPath;
9
+ this.backupDir = path.join(projectPath, '.devcompass-backups');
10
+ }
11
+
12
+ async createBackup() {
13
+ try {
14
+ // Ensure backup directory exists
15
+ if (!fs.existsSync(this.backupDir)) {
16
+ fs.mkdirSync(this.backupDir, { recursive: true });
17
+ }
18
+
19
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
20
+ const backupPath = path.join(this.backupDir, `backup-${timestamp}`);
21
+
22
+ // Create backup subdirectory
23
+ fs.mkdirSync(backupPath, { recursive: true });
24
+
25
+ // Backup package.json
26
+ const packageJsonPath = path.join(this.projectPath, 'package.json');
27
+ if (fs.existsSync(packageJsonPath)) {
28
+ fs.copyFileSync(
29
+ packageJsonPath,
30
+ path.join(backupPath, 'package.json')
31
+ );
32
+ }
33
+
34
+ // Backup package-lock.json
35
+ const packageLockPath = path.join(this.projectPath, 'package-lock.json');
36
+ if (fs.existsSync(packageLockPath)) {
37
+ fs.copyFileSync(
38
+ packageLockPath,
39
+ path.join(backupPath, 'package-lock.json')
40
+ );
41
+ }
42
+
43
+ // Save metadata
44
+ const metadata = {
45
+ timestamp: new Date().toISOString(),
46
+ path: backupPath
47
+ };
48
+ fs.writeFileSync(
49
+ path.join(backupPath, 'metadata.json'),
50
+ JSON.stringify(metadata, null, 2)
51
+ );
52
+
53
+ // Clean old backups (keep last 5)
54
+ this.cleanOldBackups();
55
+
56
+ return backupPath;
57
+ } catch (error) {
58
+ console.error(chalk.yellow('Warning: Failed to create backup:'), error.message);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ cleanOldBackups() {
64
+ try {
65
+ if (!fs.existsSync(this.backupDir)) return;
66
+
67
+ const backups = fs.readdirSync(this.backupDir)
68
+ .filter(file => file.startsWith('backup-'))
69
+ .map(file => ({
70
+ name: file,
71
+ path: path.join(this.backupDir, file),
72
+ time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
73
+ }))
74
+ .sort((a, b) => b.time - a.time);
75
+
76
+ // Keep only the 5 most recent backups
77
+ const backupsToDelete = backups.slice(5);
78
+ backupsToDelete.forEach(backup => {
79
+ fs.rmSync(backup.path, { recursive: true, force: true });
80
+ });
81
+ } catch (error) {
82
+ // Silently fail - backup cleanup is not critical
83
+ }
84
+ }
85
+
86
+ listBackups() {
87
+ try {
88
+ if (!fs.existsSync(this.backupDir)) return [];
89
+
90
+ return fs.readdirSync(this.backupDir)
91
+ .filter(file => file.startsWith('backup-'))
92
+ .map(file => {
93
+ const metadataPath = path.join(this.backupDir, file, 'metadata.json');
94
+ let metadata = { timestamp: 'Unknown' };
95
+
96
+ if (fs.existsSync(metadataPath)) {
97
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
98
+ }
99
+
100
+ return {
101
+ name: file,
102
+ path: path.join(this.backupDir, file),
103
+ timestamp: metadata.timestamp
104
+ };
105
+ })
106
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
107
+ } catch (error) {
108
+ return [];
109
+ }
110
+ }
111
+ }
112
+
113
+ module.exports = BackupManager;