devcompass 2.1.0 → 2.3.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 +412 -135
- package/bin/devcompass.js +4 -0
- package/package.json +9 -2
- package/src/alerts/formatter.js +1 -0
- package/src/alerts/index.js +1 -0
- package/src/alerts/matcher.js +1 -0
- package/src/alerts/predictive.js +54 -0
- package/src/alerts/resolver.js +1 -0
- package/src/analyzers/bundle-size.js +85 -0
- package/src/analyzers/licenses.js +107 -0
- package/src/analyzers/outdated.js +2 -0
- package/src/analyzers/scoring.js +19 -4
- package/src/analyzers/security.js +111 -0
- package/src/analyzers/unused-deps.js +1 -0
- package/src/cache/manager.js +90 -0
- package/src/commands/analyze.js +268 -32
- package/src/commands/fix.js +1 -0
- package/src/config/loader.js +46 -3
- package/src/utils/ci-handler.js +33 -0
- package/src/utils/json-formatter.js +78 -0
- package/src/utils/logger.js +1 -0
package/bin/devcompass.js
CHANGED
|
@@ -30,6 +30,9 @@ program
|
|
|
30
30
|
.command('analyze')
|
|
31
31
|
.description('Analyze your project dependencies')
|
|
32
32
|
.option('-p, --path <path>', 'Project path', process.cwd())
|
|
33
|
+
.option('--json', 'Output results as JSON')
|
|
34
|
+
.option('--ci', 'CI mode - exit with error code if score below threshold')
|
|
35
|
+
.option('--silent', 'Silent mode - no output')
|
|
33
36
|
.action(analyze);
|
|
34
37
|
|
|
35
38
|
program
|
|
@@ -40,3 +43,4 @@ program
|
|
|
40
43
|
.action(fix);
|
|
41
44
|
|
|
42
45
|
program.parse();
|
|
46
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devcompass",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Dependency health checker with ecosystem intelligence for JavaScript/TypeScript projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,14 @@
|
|
|
29
29
|
"dependency-analysis",
|
|
30
30
|
"security",
|
|
31
31
|
"ecosystem",
|
|
32
|
-
"alerts"
|
|
32
|
+
"alerts",
|
|
33
|
+
"ci-cd",
|
|
34
|
+
"automation",
|
|
35
|
+
"caching",
|
|
36
|
+
"json-output",
|
|
37
|
+
"npm-audit",
|
|
38
|
+
"bundle-size",
|
|
39
|
+
"license-checker"
|
|
33
40
|
],
|
|
34
41
|
"author": "Ajay Thorat <ajaythorat988@gmail.com>",
|
|
35
42
|
"license": "MIT",
|
package/src/alerts/formatter.js
CHANGED
package/src/alerts/index.js
CHANGED
package/src/alerts/matcher.js
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/alerts/predictive.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analyze package health trends
|
|
5
|
+
* NOTE: This is a simplified version without GitHub API
|
|
6
|
+
* For production, integrate with GitHub Issues API
|
|
7
|
+
*/
|
|
8
|
+
async function analyzeTrends(packageName) {
|
|
9
|
+
// Placeholder for future GitHub API integration
|
|
10
|
+
// For now, return basic analysis
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
package: packageName,
|
|
14
|
+
recentIssues: 0,
|
|
15
|
+
trend: 'stable',
|
|
16
|
+
recommendation: null
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calculate risk score based on trends
|
|
22
|
+
*/
|
|
23
|
+
function calculateRiskScore(trends) {
|
|
24
|
+
let risk = 0;
|
|
25
|
+
|
|
26
|
+
// High issue activity = higher risk
|
|
27
|
+
if (trends.recentIssues > 20) {
|
|
28
|
+
risk += 3;
|
|
29
|
+
} else if (trends.recentIssues > 10) {
|
|
30
|
+
risk += 2;
|
|
31
|
+
} else if (trends.recentIssues > 5) {
|
|
32
|
+
risk += 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return risk;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate predictive warnings
|
|
40
|
+
*/
|
|
41
|
+
function generatePredictiveWarnings(packages) {
|
|
42
|
+
const warnings = [];
|
|
43
|
+
|
|
44
|
+
// This is a placeholder
|
|
45
|
+
// In production, this would analyze GitHub activity
|
|
46
|
+
|
|
47
|
+
return warnings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
analyzeTrends,
|
|
52
|
+
calculateRiskScore,
|
|
53
|
+
generatePredictiveWarnings
|
|
54
|
+
};
|
package/src/alerts/resolver.js
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/analyzers/bundle-size.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get package sizes from node_modules
|
|
7
|
+
*/
|
|
8
|
+
async function analyzeBundleSizes(projectPath, dependencies) {
|
|
9
|
+
const sizes = [];
|
|
10
|
+
|
|
11
|
+
for (const packageName of Object.keys(dependencies)) {
|
|
12
|
+
try {
|
|
13
|
+
const packagePath = path.join(projectPath, 'node_modules', packageName);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(packagePath)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const size = await getDirectorySize(packagePath);
|
|
20
|
+
const sizeInKB = Math.round(size / 1024);
|
|
21
|
+
|
|
22
|
+
sizes.push({
|
|
23
|
+
name: packageName,
|
|
24
|
+
size: sizeInKB,
|
|
25
|
+
sizeFormatted: formatSize(sizeInKB)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Skip packages we can't measure
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sort by size (largest first)
|
|
35
|
+
sizes.sort((a, b) => b.size - a.size);
|
|
36
|
+
|
|
37
|
+
return sizes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get total size of directory recursively
|
|
42
|
+
*/
|
|
43
|
+
function getDirectorySize(dirPath) {
|
|
44
|
+
let totalSize = 0;
|
|
45
|
+
|
|
46
|
+
const items = fs.readdirSync(dirPath);
|
|
47
|
+
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
const itemPath = path.join(dirPath, item);
|
|
50
|
+
const stats = fs.statSync(itemPath);
|
|
51
|
+
|
|
52
|
+
if (stats.isFile()) {
|
|
53
|
+
totalSize += stats.size;
|
|
54
|
+
} else if (stats.isDirectory()) {
|
|
55
|
+
totalSize += getDirectorySize(itemPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return totalSize;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format size in human-readable format
|
|
64
|
+
*/
|
|
65
|
+
function formatSize(kb) {
|
|
66
|
+
if (kb < 1024) {
|
|
67
|
+
return `${kb} KB`;
|
|
68
|
+
} else {
|
|
69
|
+
const mb = (kb / 1024).toFixed(1);
|
|
70
|
+
return `${mb} MB`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Identify heavy packages (> 1MB)
|
|
76
|
+
*/
|
|
77
|
+
function findHeavyPackages(sizes) {
|
|
78
|
+
return sizes.filter(pkg => pkg.size > 1024); // > 1MB
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
analyzeBundleSizes,
|
|
83
|
+
findHeavyPackages,
|
|
84
|
+
formatSize
|
|
85
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/analyzers/licenses.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Restrictive licenses that might cause issues
|
|
6
|
+
const RESTRICTIVE_LICENSES = [
|
|
7
|
+
'GPL',
|
|
8
|
+
'GPL-2.0',
|
|
9
|
+
'GPL-3.0',
|
|
10
|
+
'AGPL',
|
|
11
|
+
'AGPL-3.0',
|
|
12
|
+
'LGPL',
|
|
13
|
+
'LGPL-2.1',
|
|
14
|
+
'LGPL-3.0'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Permissive licenses (usually safe)
|
|
18
|
+
const PERMISSIVE_LICENSES = [
|
|
19
|
+
'MIT',
|
|
20
|
+
'Apache-2.0',
|
|
21
|
+
'BSD-2-Clause',
|
|
22
|
+
'BSD-3-Clause',
|
|
23
|
+
'ISC',
|
|
24
|
+
'CC0-1.0',
|
|
25
|
+
'Unlicense'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check licenses of all dependencies
|
|
30
|
+
*/
|
|
31
|
+
async function checkLicenses(projectPath, dependencies) {
|
|
32
|
+
const licenses = [];
|
|
33
|
+
|
|
34
|
+
for (const [packageName, version] of Object.entries(dependencies)) {
|
|
35
|
+
try {
|
|
36
|
+
const packageJsonPath = path.join(
|
|
37
|
+
projectPath,
|
|
38
|
+
'node_modules',
|
|
39
|
+
packageName,
|
|
40
|
+
'package.json'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
48
|
+
const license = packageJson.license || 'UNKNOWN';
|
|
49
|
+
|
|
50
|
+
licenses.push({
|
|
51
|
+
package: packageName,
|
|
52
|
+
license: license,
|
|
53
|
+
type: getLicenseType(license)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Skip packages we can't read
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return licenses;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determine license type
|
|
67
|
+
*/
|
|
68
|
+
function getLicenseType(license) {
|
|
69
|
+
const licenseStr = String(license).toUpperCase();
|
|
70
|
+
|
|
71
|
+
// Check for restrictive licenses
|
|
72
|
+
for (const restrictive of RESTRICTIVE_LICENSES) {
|
|
73
|
+
if (licenseStr.includes(restrictive)) {
|
|
74
|
+
return 'restrictive';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for permissive licenses
|
|
79
|
+
for (const permissive of PERMISSIVE_LICENSES) {
|
|
80
|
+
if (licenseStr.includes(permissive)) {
|
|
81
|
+
return 'permissive';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Unknown or custom license
|
|
86
|
+
if (licenseStr === 'UNKNOWN' || licenseStr === 'UNLICENSED') {
|
|
87
|
+
return 'unknown';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return 'other';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find problematic licenses
|
|
95
|
+
*/
|
|
96
|
+
function findProblematicLicenses(licenses) {
|
|
97
|
+
return licenses.filter(pkg =>
|
|
98
|
+
pkg.type === 'restrictive' || pkg.type === 'unknown'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
checkLicenses,
|
|
104
|
+
findProblematicLicenses,
|
|
105
|
+
RESTRICTIVE_LICENSES,
|
|
106
|
+
PERMISSIVE_LICENSES
|
|
107
|
+
};
|
package/src/analyzers/scoring.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// src/analyzers/scoring.js
|
|
2
|
+
function calculateScore(
|
|
3
|
+
totalDeps,
|
|
4
|
+
unusedCount,
|
|
5
|
+
outdatedCount,
|
|
6
|
+
alertsCount = 0,
|
|
7
|
+
alertPenalty = 0,
|
|
8
|
+
securityPenalty = 0 // NEW
|
|
9
|
+
) {
|
|
2
10
|
let score = 10;
|
|
3
11
|
|
|
4
12
|
if (totalDeps === 0) {
|
|
@@ -8,9 +16,11 @@ function calculateScore(totalDeps, unusedCount, outdatedCount, alertsCount = 0,
|
|
|
8
16
|
unused: 0,
|
|
9
17
|
outdated: 0,
|
|
10
18
|
alerts: 0,
|
|
19
|
+
security: 0,
|
|
11
20
|
unusedPenalty: 0,
|
|
12
21
|
outdatedPenalty: 0,
|
|
13
|
-
alertsPenalty: 0
|
|
22
|
+
alertsPenalty: 0,
|
|
23
|
+
securityPenalty: 0
|
|
14
24
|
}
|
|
15
25
|
};
|
|
16
26
|
}
|
|
@@ -25,9 +35,12 @@ function calculateScore(totalDeps, unusedCount, outdatedCount, alertsCount = 0,
|
|
|
25
35
|
const outdatedPenalty = outdatedRatio * 3;
|
|
26
36
|
score -= outdatedPenalty;
|
|
27
37
|
|
|
28
|
-
// Ecosystem alerts penalty
|
|
38
|
+
// Ecosystem alerts penalty
|
|
29
39
|
score -= alertPenalty;
|
|
30
40
|
|
|
41
|
+
// Security vulnerabilities penalty (NEW)
|
|
42
|
+
score -= securityPenalty;
|
|
43
|
+
|
|
31
44
|
// Ensure score is between 0 and 10
|
|
32
45
|
score = Math.max(0, Math.min(10, score));
|
|
33
46
|
|
|
@@ -37,9 +50,11 @@ function calculateScore(totalDeps, unusedCount, outdatedCount, alertsCount = 0,
|
|
|
37
50
|
unused: unusedCount,
|
|
38
51
|
outdated: outdatedCount,
|
|
39
52
|
alerts: alertsCount,
|
|
53
|
+
security: securityPenalty > 0 ? 1 : 0, // Binary indicator
|
|
40
54
|
unusedPenalty: parseFloat(unusedPenalty.toFixed(1)),
|
|
41
55
|
outdatedPenalty: parseFloat(outdatedPenalty.toFixed(1)),
|
|
42
|
-
alertsPenalty: parseFloat(alertPenalty.toFixed(1))
|
|
56
|
+
alertsPenalty: parseFloat(alertPenalty.toFixed(1)),
|
|
57
|
+
securityPenalty: parseFloat(securityPenalty.toFixed(1))
|
|
43
58
|
}
|
|
44
59
|
};
|
|
45
60
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/analyzers/security.js
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run npm audit and parse results
|
|
6
|
+
*/
|
|
7
|
+
async function checkSecurity(projectPath) {
|
|
8
|
+
try {
|
|
9
|
+
// Run npm audit in JSON mode
|
|
10
|
+
const auditOutput = execSync('npm audit --json', {
|
|
11
|
+
cwd: projectPath,
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const auditData = JSON.parse(auditOutput);
|
|
17
|
+
|
|
18
|
+
// Extract vulnerabilities
|
|
19
|
+
const vulnerabilities = [];
|
|
20
|
+
|
|
21
|
+
if (auditData.vulnerabilities) {
|
|
22
|
+
Object.entries(auditData.vulnerabilities).forEach(([packageName, vuln]) => {
|
|
23
|
+
vulnerabilities.push({
|
|
24
|
+
package: packageName,
|
|
25
|
+
severity: vuln.severity, // critical, high, moderate, low
|
|
26
|
+
title: vuln.via[0]?.title || 'Security vulnerability',
|
|
27
|
+
range: vuln.range,
|
|
28
|
+
fixAvailable: vuln.fixAvailable,
|
|
29
|
+
cve: vuln.via[0]?.cve || null,
|
|
30
|
+
url: vuln.via[0]?.url || null
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
vulnerabilities,
|
|
37
|
+
metadata: {
|
|
38
|
+
total: auditData.metadata?.vulnerabilities?.total || 0,
|
|
39
|
+
critical: auditData.metadata?.vulnerabilities?.critical || 0,
|
|
40
|
+
high: auditData.metadata?.vulnerabilities?.high || 0,
|
|
41
|
+
moderate: auditData.metadata?.vulnerabilities?.moderate || 0,
|
|
42
|
+
low: auditData.metadata?.vulnerabilities?.low || 0
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// npm audit returns non-zero exit code when vulnerabilities found
|
|
48
|
+
// Try to parse the error output
|
|
49
|
+
try {
|
|
50
|
+
const auditData = JSON.parse(error.stdout);
|
|
51
|
+
|
|
52
|
+
const vulnerabilities = [];
|
|
53
|
+
|
|
54
|
+
if (auditData.vulnerabilities) {
|
|
55
|
+
Object.entries(auditData.vulnerabilities).forEach(([packageName, vuln]) => {
|
|
56
|
+
vulnerabilities.push({
|
|
57
|
+
package: packageName,
|
|
58
|
+
severity: vuln.severity,
|
|
59
|
+
title: vuln.via[0]?.title || 'Security vulnerability',
|
|
60
|
+
range: vuln.range,
|
|
61
|
+
fixAvailable: vuln.fixAvailable,
|
|
62
|
+
cve: vuln.via[0]?.cve || null,
|
|
63
|
+
url: vuln.via[0]?.url || null
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
vulnerabilities,
|
|
70
|
+
metadata: {
|
|
71
|
+
total: auditData.metadata?.vulnerabilities?.total || 0,
|
|
72
|
+
critical: auditData.metadata?.vulnerabilities?.critical || 0,
|
|
73
|
+
high: auditData.metadata?.vulnerabilities?.high || 0,
|
|
74
|
+
moderate: auditData.metadata?.vulnerabilities?.moderate || 0,
|
|
75
|
+
low: auditData.metadata?.vulnerabilities?.low || 0
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
} catch (parseError) {
|
|
79
|
+
// If we can't parse, return empty
|
|
80
|
+
return {
|
|
81
|
+
vulnerabilities: [],
|
|
82
|
+
metadata: {
|
|
83
|
+
total: 0,
|
|
84
|
+
critical: 0,
|
|
85
|
+
high: 0,
|
|
86
|
+
moderate: 0,
|
|
87
|
+
low: 0
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate security penalty for health score
|
|
96
|
+
*/
|
|
97
|
+
function calculateSecurityPenalty(metadata) {
|
|
98
|
+
let penalty = 0;
|
|
99
|
+
|
|
100
|
+
penalty += metadata.critical * 2.5; // Critical: -2.5 points each
|
|
101
|
+
penalty += metadata.high * 1.5; // High: -1.5 points each
|
|
102
|
+
penalty += metadata.moderate * 0.5; // Moderate: -0.5 points each
|
|
103
|
+
penalty += metadata.low * 0.2; // Low: -0.2 points each
|
|
104
|
+
|
|
105
|
+
return Math.min(penalty, 5.0); // Cap at 5 points
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
checkSecurity,
|
|
110
|
+
calculateSecurityPenalty
|
|
111
|
+
};
|
|
@@ -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
|
+
};
|