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/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.1.0",
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",
@@ -1,3 +1,4 @@
1
+ // src/alerts/formatter.js
1
2
  const chalk = require('chalk');
2
3
 
3
4
  /**
@@ -1,3 +1,4 @@
1
+ // src/alerts/index.js
1
2
  const { matchIssues } = require('./matcher');
2
3
  const { formatAlerts } = require('./formatter');
3
4
  const { resolveInstalledVersions } = require('./resolver');
@@ -1,3 +1,4 @@
1
+ // src/alerts/matcher.js
1
2
  const semver = require('semver');
2
3
 
3
4
  /**
@@ -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
+ };
@@ -1,3 +1,4 @@
1
+ // src/alerts/resolver.js
1
2
  const path = require('path');
2
3
  const fs = require('fs');
3
4
 
@@ -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
+ };
@@ -1,3 +1,5 @@
1
+ // src/analyzers/outdated.js
2
+
1
3
  const ncu = require('npm-check-updates');
2
4
  const path = require('path');
3
5
 
@@ -1,4 +1,12 @@
1
- function calculateScore(totalDeps, unusedCount, outdatedCount, alertsCount = 0, alertPenalty = 0) {
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 (from formatter.calculateAlertPenalty)
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
+ };
@@ -1,3 +1,4 @@
1
+ // src/analyzers/unused-deps.js
1
2
  const depcheck = require('depcheck');
2
3
 
3
4
  async function findUnusedDeps(projectPath, dependencies) {
@@ -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
+ };