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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devcompass",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Dependency health checker with ecosystem intelligence for JavaScript/TypeScript projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"devcompass": "./bin/devcompass.js"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"src/",
|
|
12
|
+
"data/",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -25,7 +26,14 @@
|
|
|
25
26
|
"cli",
|
|
26
27
|
"devtools",
|
|
27
28
|
"package-manager",
|
|
28
|
-
"dependency-analysis"
|
|
29
|
+
"dependency-analysis",
|
|
30
|
+
"security",
|
|
31
|
+
"ecosystem",
|
|
32
|
+
"alerts",
|
|
33
|
+
"ci-cd",
|
|
34
|
+
"automation",
|
|
35
|
+
"caching",
|
|
36
|
+
"json-output"
|
|
29
37
|
],
|
|
30
38
|
"author": "Ajay Thorat <ajaythorat988@gmail.com>",
|
|
31
39
|
"license": "MIT",
|
|
@@ -34,7 +42,8 @@
|
|
|
34
42
|
"commander": "^11.1.0",
|
|
35
43
|
"depcheck": "^1.4.7",
|
|
36
44
|
"npm-check-updates": "^16.14.12",
|
|
37
|
-
"ora": "^5.4.1"
|
|
45
|
+
"ora": "^5.4.1",
|
|
46
|
+
"semver": "^7.6.0"
|
|
38
47
|
},
|
|
39
48
|
"engines": {
|
|
40
49
|
"node": ">=14.0.0"
|
|
@@ -47,4 +56,4 @@
|
|
|
47
56
|
"url": "https://github.com/AjayBThorat-20/devcompass/issues"
|
|
48
57
|
},
|
|
49
58
|
"homepage": "https://github.com/AjayBThorat-20/devcompass#readme"
|
|
50
|
-
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/alerts/formatter.js
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format alerts for terminal output
|
|
6
|
+
*/
|
|
7
|
+
function formatAlerts(alerts) {
|
|
8
|
+
if (alerts.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Group alerts by package
|
|
13
|
+
const grouped = {};
|
|
14
|
+
|
|
15
|
+
alerts.forEach(alert => {
|
|
16
|
+
if (!grouped[alert.package]) {
|
|
17
|
+
grouped[alert.package] = [];
|
|
18
|
+
}
|
|
19
|
+
grouped[alert.package].push(alert);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return grouped;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get severity emoji and color
|
|
27
|
+
*/
|
|
28
|
+
function getSeverityDisplay(severity) {
|
|
29
|
+
const displays = {
|
|
30
|
+
critical: { emoji: '🔴', color: chalk.red.bold, label: 'CRITICAL' },
|
|
31
|
+
high: { emoji: '🟠', color: chalk.red, label: 'HIGH' },
|
|
32
|
+
medium: { emoji: '🟡', color: chalk.yellow, label: 'MEDIUM' },
|
|
33
|
+
low: { emoji: '⚪', color: chalk.gray, label: 'LOW' }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return displays[severity] || displays.medium;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate alert impact on health score
|
|
41
|
+
*/
|
|
42
|
+
function calculateAlertPenalty(alerts) {
|
|
43
|
+
let penalty = 0;
|
|
44
|
+
|
|
45
|
+
alerts.forEach(alert => {
|
|
46
|
+
switch (alert.severity) {
|
|
47
|
+
case 'critical':
|
|
48
|
+
penalty += 2.0;
|
|
49
|
+
break;
|
|
50
|
+
case 'high':
|
|
51
|
+
penalty += 1.5;
|
|
52
|
+
break;
|
|
53
|
+
case 'medium':
|
|
54
|
+
penalty += 0.5;
|
|
55
|
+
break;
|
|
56
|
+
case 'low':
|
|
57
|
+
penalty += 0.2;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return Math.min(penalty, 5.0); // Cap at 5 points max
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
formatAlerts,
|
|
67
|
+
getSeverityDisplay,
|
|
68
|
+
calculateAlertPenalty
|
|
69
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/alerts/index.js
|
|
2
|
+
const { matchIssues } = require('./matcher');
|
|
3
|
+
const { formatAlerts } = require('./formatter');
|
|
4
|
+
const { resolveInstalledVersions } = require('./resolver');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
async function checkEcosystemAlerts(projectPath, dependencies) {
|
|
9
|
+
try {
|
|
10
|
+
// Load issues database
|
|
11
|
+
const issuesDbPath = path.join(__dirname, '../../data/issues-db.json');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(issuesDbPath)) {
|
|
14
|
+
return []; // No alerts if database doesn't exist
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const issuesDb = JSON.parse(fs.readFileSync(issuesDbPath, 'utf8'));
|
|
18
|
+
|
|
19
|
+
// Resolve installed versions from node_modules
|
|
20
|
+
const installedVersions = await resolveInstalledVersions(projectPath, dependencies);
|
|
21
|
+
|
|
22
|
+
// Match issues against installed versions
|
|
23
|
+
const alerts = matchIssues(installedVersions, issuesDb);
|
|
24
|
+
|
|
25
|
+
return alerts;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Error in checkEcosystemAlerts:', error.message);
|
|
28
|
+
return []; // Return empty array on error, don't break analysis
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { checkEcosystemAlerts };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/alerts/matcher.js
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Match installed packages against known issues database
|
|
6
|
+
*/
|
|
7
|
+
function matchIssues(installedVersions, issuesDb) {
|
|
8
|
+
const alerts = [];
|
|
9
|
+
|
|
10
|
+
for (const [packageName, versionInfo] of Object.entries(installedVersions)) {
|
|
11
|
+
// Check if this package has known issues
|
|
12
|
+
if (!issuesDb[packageName]) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const packageIssues = issuesDb[packageName];
|
|
17
|
+
const installedVersion = versionInfo.version;
|
|
18
|
+
|
|
19
|
+
// Check each issue for this package
|
|
20
|
+
for (const issue of packageIssues) {
|
|
21
|
+
try {
|
|
22
|
+
// Use semver to check if installed version is affected
|
|
23
|
+
if (semver.satisfies(installedVersion, issue.affected)) {
|
|
24
|
+
alerts.push({
|
|
25
|
+
package: packageName,
|
|
26
|
+
version: installedVersion,
|
|
27
|
+
severity: issue.severity,
|
|
28
|
+
title: issue.title,
|
|
29
|
+
affected: issue.affected,
|
|
30
|
+
fix: issue.fix || null,
|
|
31
|
+
source: issue.source || null,
|
|
32
|
+
reported: issue.reported || null
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Skip if semver parsing fails
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Sort by severity: critical > high > medium > low
|
|
43
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
44
|
+
alerts.sort((a, b) => {
|
|
45
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return alerts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { matchIssues };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/alerts/resolver.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve actual installed versions from node_modules
|
|
7
|
+
* This is CRITICAL - we need installed version, not package.json version
|
|
8
|
+
*/
|
|
9
|
+
async function resolveInstalledVersions(projectPath, dependencies) {
|
|
10
|
+
const installedVersions = {};
|
|
11
|
+
|
|
12
|
+
for (const [packageName, declaredVersion] of Object.entries(dependencies)) {
|
|
13
|
+
try {
|
|
14
|
+
const packageJsonPath = path.join(
|
|
15
|
+
projectPath,
|
|
16
|
+
'node_modules',
|
|
17
|
+
packageName,
|
|
18
|
+
'package.json'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
22
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
23
|
+
installedVersions[packageName] = {
|
|
24
|
+
name: packageName,
|
|
25
|
+
version: packageJson.version, // Clean version like "1.6.0"
|
|
26
|
+
declaredVersion: declaredVersion // What's in package.json like "^1.6.0"
|
|
27
|
+
};
|
|
28
|
+
} else {
|
|
29
|
+
// Fallback: use declared version (strip prefixes)
|
|
30
|
+
const cleanVersion = declaredVersion.replace(/^[\^~>=<]/, '');
|
|
31
|
+
installedVersions[packageName] = {
|
|
32
|
+
name: packageName,
|
|
33
|
+
version: cleanVersion,
|
|
34
|
+
declaredVersion: declaredVersion
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Skip packages that can't be resolved
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return installedVersions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { resolveInstalledVersions };
|
package/src/analyzers/scoring.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// src/analyzers/scoring.js
|
|
2
|
+
function calculateScore(totalDeps, unusedCount, outdatedCount, alertsCount = 0, alertPenalty = 0) {
|
|
2
3
|
let score = 10;
|
|
3
4
|
|
|
4
5
|
if (totalDeps === 0) {
|
|
@@ -7,20 +8,28 @@ function calculateScore(totalDeps, unusedCount, outdatedCount) {
|
|
|
7
8
|
breakdown: {
|
|
8
9
|
unused: 0,
|
|
9
10
|
outdated: 0,
|
|
11
|
+
alerts: 0,
|
|
10
12
|
unusedPenalty: 0,
|
|
11
|
-
outdatedPenalty: 0
|
|
13
|
+
outdatedPenalty: 0,
|
|
14
|
+
alertsPenalty: 0
|
|
12
15
|
}
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
// Unused dependencies penalty
|
|
16
20
|
const unusedRatio = unusedCount / totalDeps;
|
|
17
21
|
const unusedPenalty = unusedRatio * 4;
|
|
18
22
|
score -= unusedPenalty;
|
|
19
23
|
|
|
24
|
+
// Outdated packages penalty
|
|
20
25
|
const outdatedRatio = outdatedCount / totalDeps;
|
|
21
26
|
const outdatedPenalty = outdatedRatio * 3;
|
|
22
27
|
score -= outdatedPenalty;
|
|
23
28
|
|
|
29
|
+
// Ecosystem alerts penalty (from formatter.calculateAlertPenalty)
|
|
30
|
+
score -= alertPenalty;
|
|
31
|
+
|
|
32
|
+
// Ensure score is between 0 and 10
|
|
24
33
|
score = Math.max(0, Math.min(10, score));
|
|
25
34
|
|
|
26
35
|
return {
|
|
@@ -28,8 +37,10 @@ function calculateScore(totalDeps, unusedCount, outdatedCount) {
|
|
|
28
37
|
breakdown: {
|
|
29
38
|
unused: unusedCount,
|
|
30
39
|
outdated: outdatedCount,
|
|
40
|
+
alerts: alertsCount,
|
|
31
41
|
unusedPenalty: parseFloat(unusedPenalty.toFixed(1)),
|
|
32
|
-
outdatedPenalty: parseFloat(outdatedPenalty.toFixed(1))
|
|
42
|
+
outdatedPenalty: parseFloat(outdatedPenalty.toFixed(1)),
|
|
43
|
+
alertsPenalty: parseFloat(alertPenalty.toFixed(1))
|
|
33
44
|
}
|
|
34
45
|
};
|
|
35
46
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/cache/manager.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const CACHE_FILE = '.devcompass-cache.json';
|
|
6
|
+
const CACHE_DURATION = 3600000; // 1 hour in milliseconds
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load cache from disk
|
|
10
|
+
*/
|
|
11
|
+
function loadCache(projectPath) {
|
|
12
|
+
try {
|
|
13
|
+
const cachePath = path.join(projectPath, CACHE_FILE);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(cachePath)) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
20
|
+
return cacheData;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Save cache to disk
|
|
28
|
+
*/
|
|
29
|
+
function saveCache(projectPath, cacheData) {
|
|
30
|
+
try {
|
|
31
|
+
const cachePath = path.join(projectPath, CACHE_FILE);
|
|
32
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf8');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Silent fail - caching is not critical
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get cached data if still valid
|
|
40
|
+
*/
|
|
41
|
+
function getCached(projectPath, key) {
|
|
42
|
+
const cache = loadCache(projectPath);
|
|
43
|
+
|
|
44
|
+
if (!cache[key]) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cached = cache[key];
|
|
49
|
+
const age = Date.now() - cached.timestamp;
|
|
50
|
+
|
|
51
|
+
if (age > CACHE_DURATION) {
|
|
52
|
+
return null; // Expired
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return cached.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set cache entry
|
|
60
|
+
*/
|
|
61
|
+
function setCache(projectPath, key, data) {
|
|
62
|
+
const cache = loadCache(projectPath);
|
|
63
|
+
|
|
64
|
+
cache[key] = {
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
data: data
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
saveCache(projectPath, cache);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all cache
|
|
74
|
+
*/
|
|
75
|
+
function clearCache(projectPath) {
|
|
76
|
+
try {
|
|
77
|
+
const cachePath = path.join(projectPath, CACHE_FILE);
|
|
78
|
+
if (fs.existsSync(cachePath)) {
|
|
79
|
+
fs.unlinkSync(cachePath);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Silent fail
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
getCached,
|
|
88
|
+
setCache,
|
|
89
|
+
clearCache
|
|
90
|
+
};
|