devcompass 2.6.0 → 2.7.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.
@@ -0,0 +1,57 @@
1
+ {
2
+ "malicious_packages": [
3
+ "epress",
4
+ "expres",
5
+ "expresss",
6
+ "reqest",
7
+ "requet",
8
+ "lodas",
9
+ "loadsh",
10
+ "axois",
11
+ "axioss",
12
+ "webpak",
13
+ "webpackk",
14
+ "reactt",
15
+ "vuee",
16
+ "angularr"
17
+ ],
18
+ "typosquat_patterns": {
19
+ "express": ["epress", "expres", "expresss", "exprss"],
20
+ "request": ["reqest", "requet", "requets"],
21
+ "lodash": ["lodas", "loadsh", "lodahs", "lodsh"],
22
+ "axios": ["axois", "axioss", "axos", "axious"],
23
+ "webpack": ["webpak", "webpackk", "wepback"],
24
+ "react": ["reactt", "reakt", "raect"],
25
+ "vue": ["vuee", "veu", "vuw"],
26
+ "angular": ["angularr", "anguler", "angulr"],
27
+ "next": ["nextt", "nxt", "nex"],
28
+ "typescript": ["typscript", "typescrpt", "typescrip"],
29
+ "eslint": ["esslint", "elint", "eslnt"],
30
+ "prettier": ["pretier", "prettir", "pretter"],
31
+ "jest": ["jst", "jestt", "jест"],
32
+ "mocha": ["mocha", "mоcha", "mосha"],
33
+ "chai": ["chаi", "сhai", "chаi"]
34
+ },
35
+ "suspicious_patterns": {
36
+ "install_scripts": [
37
+ "curl",
38
+ "wget",
39
+ "powershell",
40
+ "eval",
41
+ "exec",
42
+ "child_process",
43
+ "/bin/sh",
44
+ "/bin/bash",
45
+ "http://",
46
+ "https://"
47
+ ],
48
+ "suspicious_dependencies": [
49
+ "bitcoin",
50
+ "cryptocurrency",
51
+ "mining",
52
+ "miner",
53
+ "keylogger",
54
+ "backdoor"
55
+ ]
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devcompass",
3
- "version": "2.6.0",
4
- "description": "Dependency health checker with ecosystem intelligence and real-time GitHub issue tracking for 500+ popular npm packages. Features parallel processing for 80% faster analysis.",
3
+ "version": "2.7.0",
4
+ "description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, parallel processing, supply chain security analysis, and advanced license risk detection.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "devcompass": "./bin/devcompass.js"
@@ -45,7 +45,15 @@
45
45
  "package-health",
46
46
  "top-500-packages",
47
47
  "parallel-processing",
48
- "performance-optimization"
48
+ "performance-optimization",
49
+ "supply-chain-security",
50
+ "typosquatting-detection",
51
+ "malicious-packages",
52
+ "license-compliance",
53
+ "license-risk",
54
+ "package-quality",
55
+ "security-recommendations",
56
+ "dependency-quality"
49
57
  ],
50
58
  "author": "Ajay Thorat <ajaythorat988@gmail.com>",
51
59
  "license": "MIT",
@@ -0,0 +1,225 @@
1
+ // src/analyzers/license-risk.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * License risk levels and compatibility
7
+ */
8
+ const LICENSE_RISKS = {
9
+ // High Risk - Restrictive/Copyleft
10
+ 'GPL-1.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
11
+ 'GPL-2.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
12
+ 'GPL-3.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
13
+ 'AGPL-1.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
14
+ 'AGPL-3.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
15
+ 'LGPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
16
+ 'LGPL-2.1': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
17
+ 'LGPL-3.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
18
+
19
+ // Medium Risk
20
+ 'MPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
21
+ 'MPL-1.1': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
22
+ 'MPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
23
+ 'EPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
24
+ 'EPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
25
+ 'CDDL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
26
+
27
+ // Low Risk - Permissive
28
+ 'MIT': { risk: 'low', type: 'permissive', business: 'Very permissive' },
29
+ 'Apache-2.0': { risk: 'low', type: 'permissive', business: 'Permissive with patent grant' },
30
+ 'BSD-2-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
31
+ 'BSD-3-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
32
+ 'ISC': { risk: 'low', type: 'permissive', business: 'Very permissive' },
33
+ 'CC0-1.0': { risk: 'low', type: 'public-domain', business: 'Public domain' },
34
+ 'Unlicense': { risk: 'low', type: 'public-domain', business: 'Public domain' },
35
+ '0BSD': { risk: 'low', type: 'permissive', business: 'Very permissive' },
36
+
37
+ // Unknown/Special
38
+ 'UNLICENSED': { risk: 'critical', type: 'unknown', business: 'No license - all rights reserved' },
39
+ 'SEE LICENSE IN': { risk: 'high', type: 'unknown', business: 'Custom license - review required' },
40
+ 'CUSTOM': { risk: 'high', type: 'unknown', business: 'Custom license - review required' }
41
+ };
42
+
43
+ /**
44
+ * License compatibility matrix
45
+ * Can license A be combined with license B?
46
+ */
47
+ const LICENSE_COMPATIBILITY = {
48
+ 'MIT': ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0', 'GPL-3.0', 'LGPL-2.1', 'LGPL-3.0'],
49
+ 'Apache-2.0': ['Apache-2.0', 'GPL-3.0', 'LGPL-3.0'],
50
+ 'GPL-2.0': ['GPL-2.0', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
51
+ 'GPL-3.0': ['GPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
52
+ 'LGPL-2.1': ['LGPL-2.1', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0'],
53
+ 'LGPL-3.0': ['LGPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-3.0']
54
+ };
55
+
56
+ /**
57
+ * Normalize license name
58
+ */
59
+ function normalizeLicense(license) {
60
+ if (!license) return 'UNLICENSED';
61
+
62
+ const normalized = license
63
+ .replace(/\s+/g, '-')
64
+ .replace(/[()]/g, '')
65
+ .toUpperCase();
66
+
67
+ // Handle common variations
68
+ if (normalized.includes('MIT')) return 'MIT';
69
+ if (normalized.includes('APACHE-2')) return 'Apache-2.0';
70
+ if (normalized.includes('BSD-2')) return 'BSD-2-Clause';
71
+ if (normalized.includes('BSD-3')) return 'BSD-3-Clause';
72
+ if (normalized.includes('ISC')) return 'ISC';
73
+ if (normalized.includes('GPL-2')) return 'GPL-2.0';
74
+ if (normalized.includes('GPL-3')) return 'GPL-3.0';
75
+ if (normalized.includes('LGPL-2')) return 'LGPL-2.1';
76
+ if (normalized.includes('LGPL-3')) return 'LGPL-3.0';
77
+ if (normalized.includes('AGPL')) return 'AGPL-3.0';
78
+ if (normalized.includes('MPL')) return 'MPL-2.0';
79
+ if (normalized.includes('SEE LICENSE')) return 'SEE LICENSE IN';
80
+ if (normalized === 'UNLICENSED') return 'UNLICENSED';
81
+
82
+ return license;
83
+ }
84
+
85
+ /**
86
+ * Get license risk information
87
+ */
88
+ function getLicenseRisk(license) {
89
+ const normalized = normalizeLicense(license);
90
+ return LICENSE_RISKS[normalized] || {
91
+ risk: 'high',
92
+ type: 'unknown',
93
+ business: 'Unknown license - review required'
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Check license compatibility
99
+ */
100
+ function checkLicenseCompatibility(projectLicense, dependencyLicenses) {
101
+ const conflicts = [];
102
+ const normalized = normalizeLicense(projectLicense);
103
+ const compatible = LICENSE_COMPATIBILITY[normalized] || [];
104
+
105
+ for (const [pkg, license] of Object.entries(dependencyLicenses)) {
106
+ const depNormalized = normalizeLicense(license);
107
+ const depRisk = getLicenseRisk(license);
108
+
109
+ // Check if copyleft license conflicts with permissive project
110
+ if (depRisk.type === 'copyleft' && !compatible.includes(depNormalized)) {
111
+ conflicts.push({
112
+ package: pkg,
113
+ license: license,
114
+ projectLicense: projectLicense,
115
+ severity: 'high',
116
+ issue: 'License incompatibility',
117
+ message: `${license} dependency may conflict with ${projectLicense} project license`,
118
+ recommendation: 'Review license compatibility with legal team'
119
+ });
120
+ }
121
+ }
122
+
123
+ return conflicts;
124
+ }
125
+
126
+ /**
127
+ * Analyze license risks for all dependencies
128
+ */
129
+ async function analyzeLicenseRisks(projectPath, licenses) {
130
+ const warnings = [];
131
+ const stats = {
132
+ total: 0,
133
+ critical: 0,
134
+ high: 0,
135
+ medium: 0,
136
+ low: 0,
137
+ copyleft: 0,
138
+ permissive: 0,
139
+ unknown: 0
140
+ };
141
+
142
+ // Get project license
143
+ let projectLicense = 'MIT'; // Default
144
+ try {
145
+ const projectPkgPath = path.join(projectPath, 'package.json');
146
+ if (fs.existsSync(projectPkgPath)) {
147
+ const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
148
+ projectLicense = projectPkg.license || 'MIT';
149
+ }
150
+ } catch (error) {
151
+ // Use default
152
+ }
153
+
154
+ const dependencyLicenses = {};
155
+
156
+ // Analyze each license
157
+ for (const pkg of licenses) {
158
+ stats.total++;
159
+
160
+ const risk = getLicenseRisk(pkg.license);
161
+ dependencyLicenses[pkg.package] = pkg.license;
162
+
163
+ // Count by type
164
+ if (risk.type === 'copyleft' || risk.type === 'weak-copyleft') {
165
+ stats.copyleft++;
166
+ } else if (risk.type === 'permissive' || risk.type === 'public-domain') {
167
+ stats.permissive++;
168
+ } else {
169
+ stats.unknown++;
170
+ }
171
+
172
+ // Add warnings for high-risk licenses
173
+ if (risk.risk === 'critical' || risk.risk === 'high') {
174
+ stats[risk.risk]++;
175
+
176
+ warnings.push({
177
+ package: pkg.package,
178
+ license: pkg.license,
179
+ severity: risk.risk,
180
+ type: risk.type,
181
+ issue: 'High-risk license',
182
+ message: `${pkg.license}: ${risk.business}`,
183
+ recommendation: risk.risk === 'critical'
184
+ ? 'Replace with permissive alternative immediately'
185
+ : 'Consider replacing with MIT/Apache alternative'
186
+ });
187
+ } else if (risk.risk === 'medium') {
188
+ stats.medium++;
189
+ } else {
190
+ stats.low++;
191
+ }
192
+ }
193
+
194
+ // Check license compatibility
195
+ const conflicts = checkLicenseCompatibility(projectLicense, dependencyLicenses);
196
+ warnings.push(...conflicts);
197
+
198
+ return {
199
+ warnings,
200
+ stats,
201
+ projectLicense
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Get license risk score (0-10)
207
+ */
208
+ function getLicenseRiskScore(stats) {
209
+ let score = 10;
210
+
211
+ score -= stats.critical * 3;
212
+ score -= stats.high * 2;
213
+ score -= stats.medium * 0.5;
214
+
215
+ return Math.max(0, score);
216
+ }
217
+
218
+ module.exports = {
219
+ analyzeLicenseRisks,
220
+ getLicenseRisk,
221
+ checkLicenseCompatibility,
222
+ normalizeLicense,
223
+ getLicenseRiskScore,
224
+ LICENSE_RISKS
225
+ };
@@ -0,0 +1,368 @@
1
+ // src/analyzers/package-quality.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const https = require('https');
5
+
6
+ /**
7
+ * Fetch package metadata from npm registry
8
+ */
9
+ function fetchNpmPackageInfo(packageName) {
10
+ return new Promise((resolve, reject) => {
11
+ const options = {
12
+ hostname: 'registry.npmjs.org',
13
+ path: `/${packageName}`,
14
+ method: 'GET',
15
+ headers: {
16
+ 'User-Agent': 'DevCompass',
17
+ 'Accept': 'application/json'
18
+ }
19
+ };
20
+
21
+ const req = https.request(options, (res) => {
22
+ let data = '';
23
+
24
+ res.on('data', (chunk) => {
25
+ data += chunk;
26
+ });
27
+
28
+ res.on('end', () => {
29
+ if (res.statusCode === 200) {
30
+ try {
31
+ resolve(JSON.parse(data));
32
+ } catch (error) {
33
+ reject(new Error('Failed to parse npm response'));
34
+ }
35
+ } else {
36
+ reject(new Error(`npm registry returned ${res.statusCode}`));
37
+ }
38
+ });
39
+ });
40
+
41
+ req.on('error', reject);
42
+ req.setTimeout(5000, () => {
43
+ req.destroy();
44
+ reject(new Error('Request timeout'));
45
+ });
46
+ req.end();
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Calculate days since last publish
52
+ */
53
+ function daysSincePublish(dateString) {
54
+ const publishDate = new Date(dateString);
55
+ const now = new Date();
56
+ const diffTime = Math.abs(now - publishDate);
57
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
58
+ return diffDays;
59
+ }
60
+
61
+ /**
62
+ * Calculate package health score (0-10)
63
+ */
64
+ function calculateHealthScore(packageData, githubData = null) {
65
+ let score = 10;
66
+
67
+ // Get latest version info
68
+ const latestVersion = packageData['dist-tags']?.latest;
69
+ const versionData = packageData.versions?.[latestVersion];
70
+ const time = packageData.time?.[latestVersion];
71
+
72
+ if (!versionData || !time) {
73
+ return 5; // Default score if data missing
74
+ }
75
+
76
+ // 1. Age factor (max -2 points)
77
+ const daysSince = daysSincePublish(time);
78
+ if (daysSince > 365 * 3) {
79
+ score -= 2; // 3+ years old
80
+ } else if (daysSince > 365 * 2) {
81
+ score -= 1.5; // 2-3 years old
82
+ } else if (daysSince > 365) {
83
+ score -= 1; // 1-2 years old
84
+ } else if (daysSince > 180) {
85
+ score -= 0.5; // 6-12 months old
86
+ }
87
+
88
+ // 2. Maintenance frequency (max -2 points)
89
+ const versions = Object.keys(packageData.versions || {});
90
+ const recentVersions = versions.filter(v => {
91
+ const vTime = packageData.time?.[v];
92
+ if (!vTime) return false;
93
+ return daysSincePublish(vTime) <= 365;
94
+ });
95
+
96
+ if (recentVersions.length === 0) {
97
+ score -= 2; // No updates in a year
98
+ } else if (recentVersions.length < 3) {
99
+ score -= 1; // Less than 3 updates in a year
100
+ }
101
+
102
+ // 3. GitHub activity (if available) (max -2 points)
103
+ if (githubData) {
104
+ const { totalIssues, last7Days, last30Days } = githubData;
105
+
106
+ // High issue count with low activity is bad
107
+ if (totalIssues > 100 && last30Days < 5) {
108
+ score -= 1.5; // Many issues, low maintenance
109
+ } else if (totalIssues > 50 && last30Days < 3) {
110
+ score -= 1; // Medium issues, low maintenance
111
+ }
112
+
113
+ // Very high recent activity might indicate problems
114
+ if (last7Days > 30) {
115
+ score -= 0.5; // Unusually high activity
116
+ }
117
+ }
118
+
119
+ // 4. Dependencies count (max -1 point)
120
+ const deps = versionData.dependencies || {};
121
+ const depCount = Object.keys(deps).length;
122
+
123
+ if (depCount > 50) {
124
+ score -= 1; // Too many dependencies
125
+ } else if (depCount > 30) {
126
+ score -= 0.5;
127
+ }
128
+
129
+ // 5. Has description and repository (max -1 point)
130
+ if (!packageData.description) {
131
+ score -= 0.5;
132
+ }
133
+ if (!packageData.repository) {
134
+ score -= 0.5;
135
+ }
136
+
137
+ // 6. Deprecated packages (automatic 0)
138
+ if (versionData.deprecated) {
139
+ return 0;
140
+ }
141
+
142
+ return Math.max(0, Math.min(10, score));
143
+ }
144
+
145
+ /**
146
+ * Determine package status based on health score
147
+ */
148
+ function getPackageStatus(score, daysSince) {
149
+ if (score === 0) {
150
+ return {
151
+ status: 'deprecated',
152
+ color: 'red',
153
+ severity: 'critical',
154
+ label: 'DEPRECATED'
155
+ };
156
+ } else if (score < 3 || daysSince > 365 * 3) {
157
+ return {
158
+ status: 'abandoned',
159
+ color: 'red',
160
+ severity: 'critical',
161
+ label: 'ABANDONED'
162
+ };
163
+ } else if (score < 5 || daysSince > 365 * 2) {
164
+ return {
165
+ status: 'stale',
166
+ color: 'yellow',
167
+ severity: 'high',
168
+ label: 'STALE'
169
+ };
170
+ } else if (score < 7) {
171
+ return {
172
+ status: 'needs_attention',
173
+ color: 'yellow',
174
+ severity: 'medium',
175
+ label: 'NEEDS ATTENTION'
176
+ };
177
+ } else {
178
+ return {
179
+ status: 'healthy',
180
+ color: 'green',
181
+ severity: 'low',
182
+ label: 'HEALTHY'
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Get maintainer activity status
189
+ */
190
+ function getMaintainerStatus(packageData) {
191
+ const maintainers = packageData.maintainers || [];
192
+ const latestVersion = packageData['dist-tags']?.latest;
193
+ const time = packageData.time?.[latestVersion];
194
+
195
+ if (!time) {
196
+ return 'unknown';
197
+ }
198
+
199
+ const daysSince = daysSincePublish(time);
200
+
201
+ if (daysSince > 365 * 2) {
202
+ return 'inactive';
203
+ } else if (daysSince > 365) {
204
+ return 'low_activity';
205
+ } else if (daysSince > 180) {
206
+ return 'moderate_activity';
207
+ } else {
208
+ return 'active';
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Analyze package quality for all dependencies
214
+ */
215
+ async function analyzePackageQuality(dependencies, githubData = []) {
216
+ const results = [];
217
+ const stats = {
218
+ total: 0,
219
+ healthy: 0,
220
+ needsAttention: 0,
221
+ stale: 0,
222
+ abandoned: 0,
223
+ deprecated: 0
224
+ };
225
+
226
+ // Create GitHub data lookup
227
+ const githubLookup = {};
228
+ for (const data of githubData) {
229
+ githubLookup[data.package] = data;
230
+ }
231
+
232
+ // Analyze each package (limit to prevent rate limiting)
233
+ const packages = Object.keys(dependencies).slice(0, 20); // Analyze first 20
234
+
235
+ for (const packageName of packages) {
236
+ try {
237
+ stats.total++;
238
+
239
+ // Fetch package info from npm
240
+ const packageData = await fetchNpmPackageInfo(packageName);
241
+
242
+ // Calculate health score
243
+ const github = githubLookup[packageName];
244
+ const healthScore = calculateHealthScore(packageData, github);
245
+
246
+ // Get latest version info
247
+ const latestVersion = packageData['dist-tags']?.latest;
248
+ const time = packageData.time?.[latestVersion];
249
+ const daysSince = time ? daysSincePublish(time) : 0;
250
+
251
+ // Determine status
252
+ const status = getPackageStatus(healthScore, daysSince);
253
+ const maintainerStatus = getMaintainerStatus(packageData);
254
+
255
+ // Count by status
256
+ if (status.status === 'healthy') {
257
+ stats.healthy++;
258
+ } else if (status.status === 'needs_attention') {
259
+ stats.needsAttention++;
260
+ } else if (status.status === 'stale') {
261
+ stats.stale++;
262
+ } else if (status.status === 'abandoned') {
263
+ stats.abandoned++;
264
+ } else if (status.status === 'deprecated') {
265
+ stats.deprecated++;
266
+ }
267
+
268
+ // Get repository info
269
+ const repository = packageData.repository?.url || '';
270
+ const hasGithub = repository.includes('github.com');
271
+
272
+ // Build result
273
+ const result = {
274
+ package: packageName,
275
+ version: dependencies[packageName],
276
+ healthScore: Number(healthScore.toFixed(1)),
277
+ status: status.status,
278
+ severity: status.severity,
279
+ label: status.label,
280
+ lastPublish: time ? new Date(time).toISOString().split('T')[0] : 'unknown',
281
+ daysSincePublish: daysSince,
282
+ maintainerStatus: maintainerStatus,
283
+ hasRepository: !!packageData.repository,
284
+ hasGithub: hasGithub,
285
+ totalVersions: Object.keys(packageData.versions || {}).length,
286
+ description: packageData.description || '',
287
+ deprecated: packageData.versions?.[latestVersion]?.deprecated || false
288
+ };
289
+
290
+ // Add GitHub metrics if available
291
+ if (github) {
292
+ result.githubMetrics = {
293
+ totalIssues: github.totalIssues,
294
+ recentIssues: github.last30Days,
295
+ trend: github.trend
296
+ };
297
+ }
298
+
299
+ results.push(result);
300
+
301
+ // Small delay to respect npm registry rate limits
302
+ await new Promise(resolve => setTimeout(resolve, 100));
303
+
304
+ } catch (error) {
305
+ console.error(`Error analyzing ${packageName}:`, error.message);
306
+ // Continue with next package
307
+ }
308
+ }
309
+
310
+ return {
311
+ results,
312
+ stats
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Get quality recommendations for a package
318
+ */
319
+ function getQualityRecommendation(packageResult) {
320
+ const { status, healthScore, daysSincePublish, maintainerStatus } = packageResult;
321
+
322
+ if (status === 'deprecated') {
323
+ return {
324
+ action: 'critical',
325
+ message: 'Package is deprecated',
326
+ recommendation: 'Find an actively maintained alternative immediately'
327
+ };
328
+ }
329
+
330
+ if (status === 'abandoned') {
331
+ return {
332
+ action: 'high',
333
+ message: `Last updated ${Math.floor(daysSincePublish / 365)} years ago`,
334
+ recommendation: 'Migrate to an actively maintained alternative'
335
+ };
336
+ }
337
+
338
+ if (status === 'stale') {
339
+ return {
340
+ action: 'medium',
341
+ message: `Not updated in ${Math.floor(daysSincePublish / 30)} months`,
342
+ recommendation: 'Consider finding a more actively maintained package'
343
+ };
344
+ }
345
+
346
+ if (status === 'needs_attention') {
347
+ return {
348
+ action: 'low',
349
+ message: `Health score: ${healthScore}/10`,
350
+ recommendation: 'Monitor for updates and potential alternatives'
351
+ };
352
+ }
353
+
354
+ return {
355
+ action: 'none',
356
+ message: 'Package is healthy',
357
+ recommendation: 'No action needed'
358
+ };
359
+ }
360
+
361
+ module.exports = {
362
+ analyzePackageQuality,
363
+ calculateHealthScore,
364
+ getPackageStatus,
365
+ getMaintainerStatus,
366
+ getQualityRecommendation,
367
+ fetchNpmPackageInfo
368
+ };