devcompass 1.0.5 â 2.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 +472 -27
- package/bin/devcompass.js +13 -1
- package/data/issues-db.json +60 -0
- package/package.json +14 -5
- package/src/alerts/formatter.js +69 -0
- package/src/alerts/index.js +32 -0
- package/src/alerts/matcher.js +51 -0
- package/src/alerts/resolver.js +46 -0
- package/src/analyzers/outdated.js +2 -0
- package/src/analyzers/scoring.js +14 -3
- package/src/analyzers/unused-deps.js +1 -0
- package/src/cache/manager.js +90 -0
- package/src/commands/analyze.js +200 -31
- package/src/commands/fix.js +247 -0
- package/src/config/loader.js +72 -0
- package/src/utils/ci-handler.js +33 -0
- package/src/utils/json-formatter.js +44 -0
- package/src/utils/logger.js +1 -0
- package/src/index.js +0 -0
package/src/commands/analyze.js
CHANGED
|
@@ -7,19 +7,37 @@ const fs = require('fs');
|
|
|
7
7
|
const { findUnusedDeps } = require('../analyzers/unused-deps');
|
|
8
8
|
const { findOutdatedDeps } = require('../analyzers/outdated');
|
|
9
9
|
const { calculateScore } = require('../analyzers/scoring');
|
|
10
|
+
const { checkEcosystemAlerts } = require('../alerts');
|
|
11
|
+
const {
|
|
12
|
+
formatAlerts,
|
|
13
|
+
getSeverityDisplay,
|
|
14
|
+
calculateAlertPenalty
|
|
15
|
+
} = require('../alerts/formatter');
|
|
10
16
|
const {
|
|
11
17
|
log,
|
|
12
18
|
logSection,
|
|
13
19
|
logDivider,
|
|
14
20
|
getScoreColor
|
|
15
21
|
} = require('../utils/logger');
|
|
22
|
+
const { loadConfig, filterAlerts } = require('../config/loader');
|
|
23
|
+
const { getCached, setCache } = require('../cache/manager');
|
|
24
|
+
const { formatAsJson } = require('../utils/json-formatter');
|
|
25
|
+
const { handleCiMode } = require('../utils/ci-handler');
|
|
16
26
|
const packageJson = require('../../package.json');
|
|
17
27
|
|
|
18
28
|
async function analyze(options) {
|
|
19
29
|
const projectPath = options.path || process.cwd();
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
// Load config
|
|
32
|
+
const config = loadConfig(projectPath);
|
|
33
|
+
|
|
34
|
+
// Handle output modes
|
|
35
|
+
const outputMode = options.json ? 'json' : (options.ci ? 'ci' : (options.silent ? 'silent' : 'normal'));
|
|
36
|
+
|
|
37
|
+
if (outputMode !== 'silent') {
|
|
38
|
+
console.log('\n');
|
|
39
|
+
log(chalk.cyan.bold(`đ DevCompass v${packageJson.version}`) + ' - Analyzing your project...\n');
|
|
40
|
+
}
|
|
23
41
|
|
|
24
42
|
const spinner = ora({
|
|
25
43
|
text: 'Loading project...',
|
|
@@ -58,35 +76,101 @@ async function analyze(options) {
|
|
|
58
76
|
process.exit(0);
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
// Check for ecosystem alerts (with cache)
|
|
80
|
+
spinner.text = 'Checking ecosystem alerts...';
|
|
81
|
+
let alerts = [];
|
|
82
|
+
|
|
83
|
+
if (config.cache) {
|
|
84
|
+
alerts = getCached(projectPath, 'alerts');
|
|
85
|
+
}
|
|
62
86
|
|
|
87
|
+
if (!alerts) {
|
|
88
|
+
try {
|
|
89
|
+
alerts = await checkEcosystemAlerts(projectPath, dependencies);
|
|
90
|
+
if (config.cache) {
|
|
91
|
+
setCache(projectPath, 'alerts', alerts);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (outputMode !== 'silent') {
|
|
95
|
+
console.log(chalk.yellow('\nâ ď¸ Could not check ecosystem alerts'));
|
|
96
|
+
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
97
|
+
}
|
|
98
|
+
alerts = [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Filter alerts based on config
|
|
103
|
+
alerts = filterAlerts(alerts, config);
|
|
104
|
+
|
|
105
|
+
// Unused dependencies
|
|
106
|
+
spinner.text = 'Detecting unused dependencies...';
|
|
63
107
|
let unusedDeps = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(chalk.yellow('\nâ ď¸ Could not detect unused dependencies'));
|
|
68
|
-
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
108
|
+
|
|
109
|
+
if (config.cache) {
|
|
110
|
+
unusedDeps = getCached(projectPath, 'unused');
|
|
69
111
|
}
|
|
70
112
|
|
|
71
|
-
|
|
113
|
+
if (!unusedDeps) {
|
|
114
|
+
try {
|
|
115
|
+
unusedDeps = await findUnusedDeps(projectPath, dependencies);
|
|
116
|
+
if (config.cache) {
|
|
117
|
+
setCache(projectPath, 'unused', unusedDeps);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (outputMode !== 'silent') {
|
|
121
|
+
console.log(chalk.yellow('\nâ ď¸ Could not detect unused dependencies'));
|
|
122
|
+
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
123
|
+
}
|
|
124
|
+
unusedDeps = [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
72
127
|
|
|
128
|
+
// Outdated packages
|
|
129
|
+
spinner.text = 'Checking for outdated packages...';
|
|
73
130
|
let outdatedDeps = [];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
131
|
+
|
|
132
|
+
if (config.cache) {
|
|
133
|
+
outdatedDeps = getCached(projectPath, 'outdated');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!outdatedDeps) {
|
|
137
|
+
try {
|
|
138
|
+
outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
|
|
139
|
+
if (config.cache) {
|
|
140
|
+
setCache(projectPath, 'outdated', outdatedDeps);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (outputMode !== 'silent') {
|
|
144
|
+
console.log(chalk.yellow('\nâ ď¸ Could not check for outdated packages'));
|
|
145
|
+
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
146
|
+
}
|
|
147
|
+
outdatedDeps = [];
|
|
148
|
+
}
|
|
79
149
|
}
|
|
80
150
|
|
|
151
|
+
const alertPenalty = calculateAlertPenalty(alerts);
|
|
81
152
|
const score = calculateScore(
|
|
82
153
|
totalDeps,
|
|
83
154
|
unusedDeps.length,
|
|
84
|
-
outdatedDeps.length
|
|
155
|
+
outdatedDeps.length,
|
|
156
|
+
alerts.length,
|
|
157
|
+
alertPenalty
|
|
85
158
|
);
|
|
86
159
|
|
|
87
160
|
spinner.succeed(chalk.green(`Scanned ${totalDeps} dependencies in project`));
|
|
88
161
|
|
|
89
|
-
|
|
162
|
+
// Handle different output modes
|
|
163
|
+
if (outputMode === 'json') {
|
|
164
|
+
const jsonOutput = formatAsJson(alerts, unusedDeps, outdatedDeps, score, totalDeps);
|
|
165
|
+
console.log(jsonOutput);
|
|
166
|
+
} else if (outputMode === 'ci') {
|
|
167
|
+
displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps);
|
|
168
|
+
handleCiMode(score, config, alerts, unusedDeps);
|
|
169
|
+
} else if (outputMode === 'silent') {
|
|
170
|
+
// Silent mode - no output
|
|
171
|
+
} else {
|
|
172
|
+
displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps);
|
|
173
|
+
}
|
|
90
174
|
|
|
91
175
|
} catch (error) {
|
|
92
176
|
spinner.fail(chalk.red('Analysis failed'));
|
|
@@ -98,13 +182,53 @@ async function analyze(options) {
|
|
|
98
182
|
}
|
|
99
183
|
}
|
|
100
184
|
|
|
101
|
-
function displayResults(unusedDeps, outdatedDeps, score, totalDeps) {
|
|
185
|
+
function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
|
|
102
186
|
logDivider();
|
|
103
187
|
|
|
188
|
+
// ECOSYSTEM ALERTS (NEW SECTION)
|
|
189
|
+
if (alerts.length > 0) {
|
|
190
|
+
logSection('đ¨ ECOSYSTEM ALERTS', alerts.length);
|
|
191
|
+
|
|
192
|
+
const grouped = formatAlerts(alerts);
|
|
193
|
+
|
|
194
|
+
Object.entries(grouped).forEach(([packageName, packageAlerts]) => {
|
|
195
|
+
const firstAlert = packageAlerts[0];
|
|
196
|
+
const display = getSeverityDisplay(firstAlert.severity);
|
|
197
|
+
|
|
198
|
+
log(`\n${display.emoji} ${display.color(display.label)}`);
|
|
199
|
+
log(` ${chalk.bold(packageName)}${chalk.gray('@' + firstAlert.version)}`);
|
|
200
|
+
|
|
201
|
+
packageAlerts.forEach((alert, index) => {
|
|
202
|
+
if (index > 0) {
|
|
203
|
+
const alertDisplay = getSeverityDisplay(alert.severity);
|
|
204
|
+
log(`\n ${alertDisplay.emoji} ${alertDisplay.color(alertDisplay.label)}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
log(` ${chalk.yellow('Issue:')} ${alert.title}`);
|
|
208
|
+
log(` ${chalk.gray('Affected:')} ${alert.affected}`);
|
|
209
|
+
|
|
210
|
+
if (alert.fix) {
|
|
211
|
+
log(` ${chalk.green('Fix:')} ${alert.fix}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (alert.source) {
|
|
215
|
+
log(` ${chalk.gray('Source:')} ${alert.source}`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
log('');
|
|
221
|
+
} else {
|
|
222
|
+
logSection('â
ECOSYSTEM ALERTS');
|
|
223
|
+
log(chalk.green(' No known issues detected!\n'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
logDivider();
|
|
227
|
+
|
|
228
|
+
// UNUSED DEPENDENCIES
|
|
104
229
|
if (unusedDeps.length > 0) {
|
|
105
230
|
logSection('đ´ UNUSED DEPENDENCIES', unusedDeps.length);
|
|
106
231
|
|
|
107
|
-
// Show ALL unused deps
|
|
108
232
|
unusedDeps.forEach(dep => {
|
|
109
233
|
log(` ${chalk.red('â')} ${dep.name}`);
|
|
110
234
|
});
|
|
@@ -120,10 +244,10 @@ function displayResults(unusedDeps, outdatedDeps, score, totalDeps) {
|
|
|
120
244
|
|
|
121
245
|
logDivider();
|
|
122
246
|
|
|
247
|
+
// OUTDATED PACKAGES
|
|
123
248
|
if (outdatedDeps.length > 0) {
|
|
124
249
|
logSection('đĄ OUTDATED PACKAGES', outdatedDeps.length);
|
|
125
250
|
|
|
126
|
-
// Show ALL outdated packages
|
|
127
251
|
outdatedDeps.forEach(dep => {
|
|
128
252
|
const nameCol = dep.name.padEnd(20);
|
|
129
253
|
const currentVer = chalk.yellow(dep.current);
|
|
@@ -142,39 +266,84 @@ function displayResults(unusedDeps, outdatedDeps, score, totalDeps) {
|
|
|
142
266
|
|
|
143
267
|
logDivider();
|
|
144
268
|
|
|
269
|
+
// PROJECT HEALTH
|
|
145
270
|
logSection('đ PROJECT HEALTH');
|
|
146
271
|
|
|
147
272
|
const scoreColor = getScoreColor(score.total);
|
|
148
273
|
log(` Overall Score: ${scoreColor(score.total + '/10')}`);
|
|
149
274
|
log(` Total Dependencies: ${chalk.cyan(totalDeps)}`);
|
|
275
|
+
|
|
276
|
+
if (alerts.length > 0) {
|
|
277
|
+
log(` Ecosystem Alerts: ${chalk.red(alerts.length)}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
150
280
|
log(` Unused: ${chalk.red(unusedDeps.length)}`);
|
|
151
281
|
log(` Outdated: ${chalk.yellow(outdatedDeps.length)}\n`);
|
|
152
282
|
|
|
153
283
|
logDivider();
|
|
154
284
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
285
|
+
// QUICK WINS
|
|
286
|
+
displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
|
|
290
|
+
const hasCriticalAlerts = alerts.some(a => a.severity === 'critical' || a.severity === 'high');
|
|
291
|
+
|
|
292
|
+
if (hasCriticalAlerts || unusedDeps.length > 0) {
|
|
293
|
+
logSection('đĄ QUICK WINS');
|
|
162
294
|
|
|
163
|
-
|
|
295
|
+
// Fix critical alerts first
|
|
296
|
+
if (hasCriticalAlerts) {
|
|
297
|
+
const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
|
|
298
|
+
|
|
299
|
+
log(' đ´ Fix critical issues:\n');
|
|
300
|
+
|
|
301
|
+
criticalAlerts.forEach(alert => {
|
|
302
|
+
if (alert.fix && alert.fix.includes('.')) {
|
|
303
|
+
// It's a version number
|
|
304
|
+
log(chalk.cyan(` npm install ${alert.package}@${alert.fix}`));
|
|
305
|
+
} else {
|
|
306
|
+
log(chalk.gray(` ${alert.package}: ${alert.fix}`));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
log('');
|
|
311
|
+
}
|
|
164
312
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
313
|
+
// Remove unused deps
|
|
314
|
+
if (unusedDeps.length > 0) {
|
|
315
|
+
log(' đ§š Clean up unused dependencies:\n');
|
|
316
|
+
|
|
317
|
+
const packagesToRemove = unusedDeps.map(d => d.name).join(' ');
|
|
318
|
+
log(chalk.cyan(` npm uninstall ${packagesToRemove}\n`));
|
|
319
|
+
}
|
|
168
320
|
|
|
321
|
+
// Show expected impact
|
|
322
|
+
const alertPenalty = calculateAlertPenalty(alerts.filter(a => a.severity !== 'critical' && a.severity !== 'high'));
|
|
169
323
|
const improvedScore = calculateScore(
|
|
170
324
|
totalDeps - unusedDeps.length,
|
|
171
325
|
0,
|
|
172
|
-
outdatedDeps.length
|
|
326
|
+
outdatedDeps.length,
|
|
327
|
+
alerts.length - alerts.filter(a => a.severity === 'critical' || a.severity === 'high').length,
|
|
328
|
+
alertPenalty
|
|
173
329
|
);
|
|
174
330
|
|
|
331
|
+
log(' Expected impact:');
|
|
332
|
+
|
|
333
|
+
if (hasCriticalAlerts) {
|
|
334
|
+
log(` ${chalk.green('â')} Resolve critical security/stability issues`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (unusedDeps.length > 0) {
|
|
338
|
+
log(` ${chalk.green('â')} Remove ${unusedDeps.length} unused package${unusedDeps.length > 1 ? 's' : ''}`);
|
|
339
|
+
log(` ${chalk.green('â')} Reduce node_modules size`);
|
|
340
|
+
}
|
|
341
|
+
|
|
175
342
|
const improvedScoreColor = getScoreColor(improvedScore.total);
|
|
176
343
|
log(` ${chalk.green('â')} Improve health score â ${improvedScoreColor(improvedScore.total + '/10')}\n`);
|
|
177
344
|
|
|
345
|
+
log(chalk.cyan('đĄ TIP: Run') + chalk.bold(' devcompass fix ') + chalk.cyan('to apply these fixes automatically!\n'));
|
|
346
|
+
|
|
178
347
|
logDivider();
|
|
179
348
|
}
|
|
180
349
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/commands/fix.js
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
const { findUnusedDeps } = require('../analyzers/unused-deps');
|
|
8
|
+
const { findOutdatedDeps } = require('../analyzers/outdated');
|
|
9
|
+
const { checkEcosystemAlerts } = require('../alerts');
|
|
10
|
+
const { getSeverityDisplay } = require('../alerts/formatter');
|
|
11
|
+
|
|
12
|
+
async function fix(options) {
|
|
13
|
+
const projectPath = options.path || process.cwd();
|
|
14
|
+
|
|
15
|
+
console.log('\n');
|
|
16
|
+
console.log(chalk.cyan.bold('đ§ DevCompass Fix') + ' - Analyzing and fixing your project...\n');
|
|
17
|
+
|
|
18
|
+
const spinner = ora({
|
|
19
|
+
text: 'Analyzing project...',
|
|
20
|
+
color: 'cyan'
|
|
21
|
+
}).start();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
29
|
+
spinner.fail(chalk.red('No package.json found'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projectPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
34
|
+
const dependencies = {
|
|
35
|
+
...(projectPackageJson.dependencies || {}),
|
|
36
|
+
...(projectPackageJson.devDependencies || {})
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Check for critical alerts first
|
|
40
|
+
spinner.text = 'Checking for critical issues...';
|
|
41
|
+
const alerts = await checkEcosystemAlerts(projectPath, dependencies);
|
|
42
|
+
const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
|
|
43
|
+
|
|
44
|
+
// Find unused dependencies
|
|
45
|
+
spinner.text = 'Finding unused dependencies...';
|
|
46
|
+
const unusedDeps = await findUnusedDeps(projectPath, dependencies);
|
|
47
|
+
|
|
48
|
+
// Find outdated packages
|
|
49
|
+
spinner.text = 'Checking for updates...';
|
|
50
|
+
const outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
|
|
51
|
+
|
|
52
|
+
spinner.succeed(chalk.green('Analysis complete!\n'));
|
|
53
|
+
|
|
54
|
+
// Show what will be fixed
|
|
55
|
+
await showFixPlan(criticalAlerts, unusedDeps, outdatedDeps, options);
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
spinner.fail(chalk.red('Analysis failed'));
|
|
59
|
+
console.log(chalk.red(`\nâ Error: ${error.message}\n`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function showFixPlan(criticalAlerts, unusedDeps, outdatedDeps, options) {
|
|
65
|
+
const actions = [];
|
|
66
|
+
|
|
67
|
+
// Critical alerts
|
|
68
|
+
if (criticalAlerts.length > 0) {
|
|
69
|
+
console.log(chalk.red.bold('đ´ CRITICAL ISSUES TO FIX:\n'));
|
|
70
|
+
|
|
71
|
+
criticalAlerts.forEach(alert => {
|
|
72
|
+
const display = getSeverityDisplay(alert.severity);
|
|
73
|
+
console.log(`${display.emoji} ${chalk.bold(alert.package)}@${alert.version}`);
|
|
74
|
+
console.log(` ${chalk.gray('Issue:')} ${alert.title}`);
|
|
75
|
+
|
|
76
|
+
if (alert.fix && /^\d+\.\d+/.test(alert.fix)) {
|
|
77
|
+
console.log(` ${chalk.green('Fix:')} Upgrade to ${alert.fix}\n`);
|
|
78
|
+
actions.push({
|
|
79
|
+
type: 'upgrade',
|
|
80
|
+
package: alert.package,
|
|
81
|
+
version: alert.fix,
|
|
82
|
+
reason: 'Critical security/stability issue'
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
console.log(` ${chalk.yellow('Fix:')} ${alert.fix}\n`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log('â'.repeat(70) + '\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Unused dependencies
|
|
93
|
+
if (unusedDeps.length > 0) {
|
|
94
|
+
console.log(chalk.yellow.bold('đ§š UNUSED DEPENDENCIES TO REMOVE:\n'));
|
|
95
|
+
|
|
96
|
+
unusedDeps.forEach(dep => {
|
|
97
|
+
console.log(` ${chalk.red('â')} ${dep.name}`);
|
|
98
|
+
actions.push({
|
|
99
|
+
type: 'uninstall',
|
|
100
|
+
package: dep.name,
|
|
101
|
+
reason: 'Not used in project'
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log('\n' + 'â'.repeat(70) + '\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Safe updates (patch/minor only)
|
|
109
|
+
const safeUpdates = outdatedDeps.filter(dep =>
|
|
110
|
+
dep.versionsBehind === 'patch update' || dep.versionsBehind === 'minor update'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (safeUpdates.length > 0) {
|
|
114
|
+
console.log(chalk.cyan.bold('âŹď¸ SAFE UPDATES (patch/minor):\n'));
|
|
115
|
+
|
|
116
|
+
safeUpdates.forEach(dep => {
|
|
117
|
+
console.log(` ${dep.name}: ${chalk.yellow(dep.current)} â ${chalk.green(dep.latest)} ${chalk.gray(`(${dep.versionsBehind})`)}`);
|
|
118
|
+
actions.push({
|
|
119
|
+
type: 'update',
|
|
120
|
+
package: dep.name,
|
|
121
|
+
version: dep.latest,
|
|
122
|
+
reason: dep.versionsBehind
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('\n' + 'â'.repeat(70) + '\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Major updates (show but don't auto-apply)
|
|
130
|
+
const majorUpdates = outdatedDeps.filter(dep => dep.versionsBehind === 'major update');
|
|
131
|
+
|
|
132
|
+
if (majorUpdates.length > 0) {
|
|
133
|
+
console.log(chalk.gray.bold('â ď¸ MAJOR UPDATES (skipped - may have breaking changes):\n'));
|
|
134
|
+
|
|
135
|
+
majorUpdates.forEach(dep => {
|
|
136
|
+
console.log(` ${chalk.gray(dep.name)}: ${dep.current} â ${dep.latest}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log(chalk.gray('\n Run these manually after reviewing changelog:\n'));
|
|
140
|
+
majorUpdates.forEach(dep => {
|
|
141
|
+
console.log(chalk.gray(` npm install ${dep.name}@${dep.latest}`));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log('\n' + 'â'.repeat(70) + '\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (actions.length === 0) {
|
|
148
|
+
console.log(chalk.green('⨠Everything looks good! No fixes needed.\n'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Summary
|
|
153
|
+
console.log(chalk.bold('đ FIX SUMMARY:\n'));
|
|
154
|
+
console.log(` Critical fixes: ${criticalAlerts.length}`);
|
|
155
|
+
console.log(` Remove unused: ${unusedDeps.length}`);
|
|
156
|
+
console.log(` Safe updates: ${safeUpdates.length}`);
|
|
157
|
+
console.log(` Skipped major: ${majorUpdates.length}\n`);
|
|
158
|
+
|
|
159
|
+
console.log('â'.repeat(70) + '\n');
|
|
160
|
+
|
|
161
|
+
// Confirm
|
|
162
|
+
if (options.yes) {
|
|
163
|
+
await applyFixes(actions);
|
|
164
|
+
} else {
|
|
165
|
+
const confirmed = await askConfirmation('\nâ Apply these fixes?');
|
|
166
|
+
|
|
167
|
+
if (confirmed) {
|
|
168
|
+
await applyFixes(actions);
|
|
169
|
+
} else {
|
|
170
|
+
console.log(chalk.yellow('\nâ ď¸ Fix cancelled. No changes made.\n'));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function applyFixes(actions) {
|
|
176
|
+
console.log(chalk.cyan.bold('\nđ§ Applying fixes...\n'));
|
|
177
|
+
|
|
178
|
+
const spinner = ora('Processing...').start();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Group by type
|
|
182
|
+
const toUninstall = actions.filter(a => a.type === 'uninstall').map(a => a.package);
|
|
183
|
+
const toUpgrade = actions.filter(a => a.type === 'upgrade');
|
|
184
|
+
const toUpdate = actions.filter(a => a.type === 'update');
|
|
185
|
+
|
|
186
|
+
// Uninstall unused
|
|
187
|
+
if (toUninstall.length > 0) {
|
|
188
|
+
spinner.text = `Removing ${toUninstall.length} unused packages...`;
|
|
189
|
+
|
|
190
|
+
const cmd = `npm uninstall ${toUninstall.join(' ')}`;
|
|
191
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
192
|
+
|
|
193
|
+
spinner.succeed(chalk.green(`â
Removed ${toUninstall.length} unused packages`));
|
|
194
|
+
spinner.start();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Upgrade critical packages
|
|
198
|
+
for (const action of toUpgrade) {
|
|
199
|
+
spinner.text = `Fixing ${action.package}@${action.version}...`;
|
|
200
|
+
|
|
201
|
+
const cmd = `npm install ${action.package}@${action.version}`;
|
|
202
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
203
|
+
|
|
204
|
+
spinner.succeed(chalk.green(`â
Fixed ${action.package}@${action.version}`));
|
|
205
|
+
spinner.start();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Update safe packages
|
|
209
|
+
if (toUpdate.length > 0) {
|
|
210
|
+
spinner.text = `Updating ${toUpdate.length} packages...`;
|
|
211
|
+
|
|
212
|
+
for (const action of toUpdate) {
|
|
213
|
+
const cmd = `npm install ${action.package}@${action.version}`;
|
|
214
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
spinner.succeed(chalk.green(`â
Updated ${toUpdate.length} packages`));
|
|
218
|
+
} else {
|
|
219
|
+
spinner.stop();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(chalk.green.bold('\n⨠All fixes applied successfully!\n'));
|
|
223
|
+
console.log(chalk.cyan('đĄ Run') + chalk.bold(' devcompass analyze ') + chalk.cyan('to see the new health score.\n'));
|
|
224
|
+
|
|
225
|
+
} catch (error) {
|
|
226
|
+
spinner.fail(chalk.red('Fix failed'));
|
|
227
|
+
console.log(chalk.red(`\nâ Error: ${error.message}\n`));
|
|
228
|
+
console.log(chalk.yellow('đĄ You may need to fix this manually.\n'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function askConfirmation(question) {
|
|
234
|
+
const rl = readline.createInterface({
|
|
235
|
+
input: process.stdin,
|
|
236
|
+
output: process.stdout
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return new Promise(resolve => {
|
|
240
|
+
rl.question(chalk.cyan(question) + chalk.gray(' (y/N): '), answer => {
|
|
241
|
+
rl.close();
|
|
242
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { fix };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/config/loader.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function loadConfig(projectPath) {
|
|
6
|
+
const configPath = path.join(projectPath, 'devcompass.config.json');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(configPath)) {
|
|
9
|
+
return getDefaultConfig();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
14
|
+
return { ...getDefaultConfig(), ...config };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.warn('Warning: Could not parse devcompass.config.json, using defaults');
|
|
17
|
+
return getDefaultConfig();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getDefaultConfig() {
|
|
22
|
+
return {
|
|
23
|
+
ignore: [],
|
|
24
|
+
ignoreSeverity: [], // e.g., ["low", "medium"]
|
|
25
|
+
minSeverity: null, // e.g., "medium" - only show medium+ alerts
|
|
26
|
+
minScore: 0,
|
|
27
|
+
cache: true,
|
|
28
|
+
outputMode: 'normal' // normal, json, silent, ci
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if alert should be ignored based on config
|
|
34
|
+
*/
|
|
35
|
+
function shouldIgnoreAlert(alert, config) {
|
|
36
|
+
// Check if package is in ignore list
|
|
37
|
+
if (config.ignore.includes(alert.package)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if severity is ignored
|
|
42
|
+
if (config.ignoreSeverity.includes(alert.severity)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check minimum severity level
|
|
47
|
+
if (config.minSeverity) {
|
|
48
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
49
|
+
const minLevel = severityOrder[config.minSeverity];
|
|
50
|
+
const alertLevel = severityOrder[alert.severity];
|
|
51
|
+
|
|
52
|
+
if (alertLevel > minLevel) {
|
|
53
|
+
return true; // Alert is below minimum severity
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filter alerts based on config
|
|
62
|
+
*/
|
|
63
|
+
function filterAlerts(alerts, config) {
|
|
64
|
+
return alerts.filter(alert => !shouldIgnoreAlert(alert, config));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
loadConfig,
|
|
69
|
+
getDefaultConfig,
|
|
70
|
+
shouldIgnoreAlert,
|
|
71
|
+
filterAlerts
|
|
72
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/utils/ci-handler.js
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle CI mode - exit with error code if score below threshold
|
|
6
|
+
*/
|
|
7
|
+
function handleCiMode(score, config, alerts, unusedDeps) {
|
|
8
|
+
const minScore = config.minScore || 7;
|
|
9
|
+
|
|
10
|
+
if (score.total < minScore) {
|
|
11
|
+
console.log(chalk.red(`\nâ CI CHECK FAILED`));
|
|
12
|
+
console.log(chalk.red(`Health score ${score.total}/10 is below minimum ${minScore}/10\n`));
|
|
13
|
+
|
|
14
|
+
// Show critical issues
|
|
15
|
+
const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
|
|
16
|
+
|
|
17
|
+
if (criticalAlerts.length > 0) {
|
|
18
|
+
console.log(chalk.red(`Critical issues: ${criticalAlerts.length}`));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (unusedDeps.length > 0) {
|
|
22
|
+
console.log(chalk.yellow(`Unused dependencies: ${unusedDeps.length}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.exit(1); // Fail CI
|
|
26
|
+
} else {
|
|
27
|
+
console.log(chalk.green(`\nâ
CI CHECK PASSED`));
|
|
28
|
+
console.log(chalk.green(`Health score ${score.total}/10 meets minimum ${minScore}/10\n`));
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { handleCiMode };
|