devcompass 2.7.1 → 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.
- package/README.md +475 -129
- package/bin/devcompass.js +3 -1
- package/package.json +8 -3
- package/src/commands/fix.js +295 -213
- package/src/utils/backup-manager.js +113 -0
- package/src/utils/fix-report.js +129 -0
- package/src/utils/progress-tracker.js +79 -0
|
@@ -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;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/utils/fix-report.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
class FixReport {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.fixes = [];
|
|
9
|
+
this.errors = [];
|
|
10
|
+
this.skipped = [];
|
|
11
|
+
this.startTime = Date.now();
|
|
12
|
+
this.endTime = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
addFix(type, packageName, action, details = {}) {
|
|
16
|
+
this.fixes.push({
|
|
17
|
+
type,
|
|
18
|
+
package: packageName,
|
|
19
|
+
action,
|
|
20
|
+
details,
|
|
21
|
+
timestamp: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addError(packageName, error, details = {}) {
|
|
26
|
+
this.errors.push({
|
|
27
|
+
package: packageName,
|
|
28
|
+
error: error.message || error,
|
|
29
|
+
details,
|
|
30
|
+
timestamp: new Date().toISOString()
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addSkipped(packageName, reason, details = {}) {
|
|
35
|
+
this.skipped.push({
|
|
36
|
+
package: packageName,
|
|
37
|
+
reason,
|
|
38
|
+
details,
|
|
39
|
+
timestamp: new Date().toISOString()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
finalize() {
|
|
44
|
+
this.endTime = Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getSummary() {
|
|
48
|
+
const duration = ((this.endTime || Date.now()) - this.startTime) / 1000;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
totalFixes: this.fixes.length,
|
|
52
|
+
totalErrors: this.errors.length,
|
|
53
|
+
totalSkipped: this.skipped.length,
|
|
54
|
+
duration: `${duration.toFixed(2)}s`,
|
|
55
|
+
timestamp: new Date().toISOString()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async save(projectPath) {
|
|
60
|
+
try {
|
|
61
|
+
const reportPath = path.join(projectPath, 'devcompass-fix-report.json');
|
|
62
|
+
const report = {
|
|
63
|
+
summary: this.getSummary(),
|
|
64
|
+
fixes: this.fixes,
|
|
65
|
+
errors: this.errors,
|
|
66
|
+
skipped: this.skipped
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
70
|
+
return reportPath;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('Failed to save fix report:'), error.message);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
display() {
|
|
78
|
+
console.log('\n' + chalk.bold('━'.repeat(70)));
|
|
79
|
+
console.log(chalk.bold.cyan('📊 FIX REPORT'));
|
|
80
|
+
console.log(chalk.bold('━'.repeat(70)) + '\n');
|
|
81
|
+
|
|
82
|
+
const summary = this.getSummary();
|
|
83
|
+
|
|
84
|
+
// Summary
|
|
85
|
+
console.log(chalk.bold('Summary:'));
|
|
86
|
+
console.log(` ${chalk.green('✓')} Fixes Applied: ${chalk.cyan(summary.totalFixes)}`);
|
|
87
|
+
if (summary.totalErrors > 0) {
|
|
88
|
+
console.log(` ${chalk.red('✗')} Errors: ${chalk.red(summary.totalErrors)}`);
|
|
89
|
+
}
|
|
90
|
+
if (summary.totalSkipped > 0) {
|
|
91
|
+
console.log(` ${chalk.yellow('⊘')} Skipped: ${chalk.yellow(summary.totalSkipped)}`);
|
|
92
|
+
}
|
|
93
|
+
console.log(` ${chalk.gray('⏱')} Duration: ${chalk.cyan(summary.duration)}`);
|
|
94
|
+
|
|
95
|
+
// Fixes
|
|
96
|
+
if (this.fixes.length > 0) {
|
|
97
|
+
console.log('\n' + chalk.bold('Fixes Applied:'));
|
|
98
|
+
this.fixes.forEach((fix, index) => {
|
|
99
|
+
console.log(` ${index + 1}. ${chalk.cyan(fix.package)}`);
|
|
100
|
+
console.log(` ${chalk.gray('→')} ${fix.action}`);
|
|
101
|
+
if (fix.details.from && fix.details.to) {
|
|
102
|
+
console.log(` ${chalk.gray('Version:')} ${fix.details.from} → ${chalk.green(fix.details.to)}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Errors
|
|
108
|
+
if (this.errors.length > 0) {
|
|
109
|
+
console.log('\n' + chalk.bold.red('Errors:'));
|
|
110
|
+
this.errors.forEach((error, index) => {
|
|
111
|
+
console.log(` ${index + 1}. ${chalk.red(error.package)}`);
|
|
112
|
+
console.log(` ${chalk.gray('→')} ${error.error}`);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Skipped
|
|
117
|
+
if (this.skipped.length > 0) {
|
|
118
|
+
console.log('\n' + chalk.bold.yellow('Skipped:'));
|
|
119
|
+
this.skipped.forEach((skip, index) => {
|
|
120
|
+
console.log(` ${index + 1}. ${chalk.yellow(skip.package)}`);
|
|
121
|
+
console.log(` ${chalk.gray('→')} ${skip.reason}`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('\n' + chalk.bold('━'.repeat(70)) + '\n');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = FixReport;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/utils/progress-tracker.js
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
class ProgressTracker {
|
|
6
|
+
constructor(totalSteps) {
|
|
7
|
+
this.totalSteps = totalSteps;
|
|
8
|
+
this.currentStep = 0;
|
|
9
|
+
this.spinner = null;
|
|
10
|
+
this.startTime = Date.now();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
start(message) {
|
|
14
|
+
this.spinner = ora({
|
|
15
|
+
text: this.formatMessage(message),
|
|
16
|
+
spinner: 'dots'
|
|
17
|
+
}).start();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
update(message) {
|
|
21
|
+
this.currentStep++;
|
|
22
|
+
if (this.spinner) {
|
|
23
|
+
this.spinner.text = this.formatMessage(message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
succeed(message) {
|
|
28
|
+
if (this.spinner) {
|
|
29
|
+
this.spinner.succeed(this.formatMessage(message || 'Complete'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fail(message) {
|
|
34
|
+
if (this.spinner) {
|
|
35
|
+
this.spinner.fail(chalk.red(message));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
warn(message) {
|
|
40
|
+
if (this.spinner) {
|
|
41
|
+
this.spinner.warn(chalk.yellow(message));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info(message) {
|
|
46
|
+
if (this.spinner) {
|
|
47
|
+
this.spinner.info(chalk.cyan(message));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
formatMessage(message) {
|
|
52
|
+
const percentage = Math.round((this.currentStep / this.totalSteps) * 100);
|
|
53
|
+
const elapsed = this.getElapsedTime();
|
|
54
|
+
const eta = this.getETA();
|
|
55
|
+
|
|
56
|
+
return `${message} ${chalk.gray(`[${this.currentStep}/${this.totalSteps}]`)} ${chalk.cyan(`${percentage}%`)} ${chalk.gray(`• ${elapsed}s elapsed`)}${eta ? chalk.gray(` • ETA: ${eta}s`) : ''}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getElapsedTime() {
|
|
60
|
+
return ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getETA() {
|
|
64
|
+
if (this.currentStep === 0) return null;
|
|
65
|
+
const elapsed = Date.now() - this.startTime;
|
|
66
|
+
const avgTimePerStep = elapsed / this.currentStep;
|
|
67
|
+
const remainingSteps = this.totalSteps - this.currentStep;
|
|
68
|
+
const eta = (avgTimePerStep * remainingSteps) / 1000;
|
|
69
|
+
return eta > 0 ? eta.toFixed(1) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stop() {
|
|
73
|
+
if (this.spinner) {
|
|
74
|
+
this.spinner.stop();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = ProgressTracker;
|