Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

devcompass

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

devcompass - npm Package Compare versions

Comparing version
3.1.3
to
3.1.4
+269
src/services/dynamic-license.js
// src/services/dynamic-license.js
const registryClient = require('./registry-client');
// License risk levels
const LICENSE_RISKS = {
// Critical - Viral copyleft, requires entire codebase to be open source
'AGPL-3.0': { level: 'critical', risk: 'Requires disclosing source code of network services' },
'AGPL-3.0-only': { level: 'critical', risk: 'Requires disclosing source code of network services' },
'AGPL-3.0-or-later': { level: 'critical', risk: 'Requires disclosing source code of network services' },
// High - Strong copyleft
'GPL-3.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
'GPL-3.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
'GPL-3.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
'GPL-2.0': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
'GPL-2.0-only': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
'GPL-2.0-or-later': { level: 'high', risk: 'Requires derivative works to be GPL licensed' },
// Medium - Weak copyleft
'LGPL-3.0': { level: 'medium', risk: 'Modifications to the library must be shared' },
'LGPL-3.0-only': { level: 'medium', risk: 'Modifications to the library must be shared' },
'LGPL-3.0-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' },
'LGPL-2.1': { level: 'medium', risk: 'Modifications to the library must be shared' },
'LGPL-2.1-only': { level: 'medium', risk: 'Modifications to the library must be shared' },
'LGPL-2.1-or-later': { level: 'medium', risk: 'Modifications to the library must be shared' },
'MPL-2.0': { level: 'medium', risk: 'File-level copyleft, changes to MPL files must be shared' },
'EPL-1.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' },
'EPL-2.0': { level: 'medium', risk: 'Weak copyleft, modifications must be shared' },
'CDDL-1.0': { level: 'medium', risk: 'File-level copyleft' },
// Low - Permissive licenses (safe for commercial use)
'MIT': { level: 'low', risk: 'Permissive - commercial friendly' },
'ISC': { level: 'low', risk: 'Permissive - commercial friendly' },
'BSD-2-Clause': { level: 'low', risk: 'Permissive - commercial friendly' },
'BSD-3-Clause': { level: 'low', risk: 'Permissive - commercial friendly' },
'Apache-2.0': { level: 'low', risk: 'Permissive with patent grant' },
'0BSD': { level: 'low', risk: 'Public domain equivalent' },
'Unlicense': { level: 'low', risk: 'Public domain' },
'CC0-1.0': { level: 'low', risk: 'Public domain' },
'WTFPL': { level: 'low', risk: 'Permissive' }
};
// Known alternatives for GPL packages
const GPL_ALTERNATIVES = {
'readline-sync': { replacement: 'prompts', reason: 'MIT licensed alternative' },
'gnu-which': { replacement: 'which', reason: 'ISC licensed alternative' },
'node-forge': { replacement: 'forge', reason: 'BSD-3-Clause licensed' },
'gpl-package': { replacement: 'mit-alternative', reason: 'MIT licensed alternative' }
};
function normalizeLicense(license) {
if (!license) return 'UNKNOWN';
// Handle common variations
const normalized = license
.replace(/\s+/g, '-')
.replace(/^Apache 2\.0$/i, 'Apache-2.0')
.replace(/^Apache-2$/i, 'Apache-2.0')
.replace(/^MIT$/i, 'MIT')
.replace(/^ISC$/i, 'ISC')
.replace(/^BSD$/i, 'BSD-3-Clause')
.replace(/^GPLv3$/i, 'GPL-3.0')
.replace(/^GPLv2$/i, 'GPL-2.0')
.replace(/^LGPLv3$/i, 'LGPL-3.0')
.replace(/^LGPLv2\.1$/i, 'LGPL-2.1')
.replace(/^AGPLv3$/i, 'AGPL-3.0');
return normalized;
}
function getLicenseRisk(license) {
const normalized = normalizeLicense(license);
if (LICENSE_RISKS[normalized]) {
return {
license: normalized,
...LICENSE_RISKS[normalized]
};
}
// Check for partial matches
for (const [key, value] of Object.entries(LICENSE_RISKS)) {
if (normalized.toUpperCase().includes(key.toUpperCase())) {
return { license: normalized, ...value };
}
}
// Unknown license
return {
license: normalized,
level: 'unknown',
risk: 'Unknown license - manual review recommended'
};
}
async function analyzePackage(packageName) {
const result = {
name: packageName,
license: null,
riskLevel: 'unknown',
riskMessage: null,
hasIssue: false,
alternative: null,
source: 'live'
};
try {
const data = await registryClient.fetchPackage(packageName);
if (!data) {
result.license = 'UNKNOWN';
result.riskMessage = 'Package not found';
return result;
}
// Get license from latest version
const latestVersion = data['dist-tags']?.latest;
const latestData = latestVersion ? data.versions?.[latestVersion] : null;
const license = latestData?.license || data.license;
if (!license) {
result.license = 'UNLICENSED';
result.riskLevel = 'high';
result.riskMessage = 'No license specified';
result.hasIssue = true;
return result;
}
// Analyze the license
const riskInfo = getLicenseRisk(license);
result.license = riskInfo.license;
result.riskLevel = riskInfo.level;
result.riskMessage = riskInfo.risk;
result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level);
// Check for known alternatives
if (GPL_ALTERNATIVES[packageName]) {
result.alternative = GPL_ALTERNATIVES[packageName].replacement;
}
return result;
} catch (error) {
result.license = 'UNKNOWN';
result.riskMessage = 'Failed to fetch license info';
return result;
}
}
async function analyzeBatch(packageNames) {
if (!Array.isArray(packageNames)) {
return new Map();
}
const results = new Map();
const packageData = await registryClient.fetchBatch(packageNames);
for (const name of packageNames) {
if (!name || typeof name !== 'string') continue;
const data = packageData.get(name);
const result = {
name,
license: null,
riskLevel: 'unknown',
riskMessage: null,
hasIssue: false,
alternative: GPL_ALTERNATIVES[name]?.replacement || null,
source: data ? 'live' : 'none'
};
if (data) {
const latestVersion = data['dist-tags']?.latest;
const latestData = latestVersion ? data.versions?.[latestVersion] : null;
const license = latestData?.license || data.license;
if (!license) {
result.license = 'UNLICENSED';
result.riskLevel = 'high';
result.riskMessage = 'No license specified';
result.hasIssue = true;
} else {
const riskInfo = getLicenseRisk(license);
result.license = riskInfo.license;
result.riskLevel = riskInfo.level;
result.riskMessage = riskInfo.risk;
result.hasIssue = ['critical', 'high', 'medium'].includes(riskInfo.level);
}
} else {
result.license = 'UNKNOWN';
result.riskMessage = 'Package not found';
}
results.set(name, result);
}
return results;
}
async function getLicenseConflicts(packageNames, projectLicense = 'MIT') {
const results = await analyzeBatch(packageNames);
const conflicts = {
total: packageNames.length,
clean: 0,
critical: [],
high: [],
medium: [],
unknown: []
};
for (const [name, data] of results) {
switch (data.riskLevel) {
case 'critical':
conflicts.critical.push({
package: name,
license: data.license,
message: data.riskMessage,
alternative: data.alternative
});
break;
case 'high':
conflicts.high.push({
package: name,
license: data.license,
message: data.riskMessage,
alternative: data.alternative
});
break;
case 'medium':
conflicts.medium.push({
package: name,
license: data.license,
message: data.riskMessage,
alternative: data.alternative
});
break;
case 'unknown':
conflicts.unknown.push({
package: name,
license: data.license,
message: data.riskMessage
});
break;
default:
conflicts.clean++;
}
}
return conflicts;
}
function isCommercialCompatible(license) {
const risk = getLicenseRisk(license);
return risk.level === 'low';
}
module.exports = {
analyzePackage,
analyzeBatch,
getLicenseConflicts,
getLicenseRisk,
isCommercialCompatible,
LICENSE_RISKS,
GPL_ALTERNATIVES
};
// src/services/dynamic-quality.js
const registryClient = require('./registry-client');
// Minimal fallback alternatives for offline mode
// Only most common replacements - live data covers everything else
const FALLBACK_ALTERNATIVES = {
'request': { replacement: 'axios', reason: 'request is deprecated' },
'moment': { replacement: 'dayjs', reason: 'moment is in maintenance mode' },
'underscore': { replacement: 'lodash', reason: 'lodash is more actively maintained' },
'colors': { replacement: 'chalk', reason: 'colors had a malicious release' },
'faker': { replacement: '@faker-js/faker', reason: 'faker was corrupted, use community fork' },
'left-pad': { replacement: 'string.prototype.padstart', reason: 'Use native String methods' },
'node-uuid': { replacement: 'uuid', reason: 'node-uuid is deprecated' },
'querystring': { replacement: 'qs', reason: 'querystring is legacy' },
'node-fetch': { replacement: 'undici', reason: 'undici is faster and maintained by Node.js' }
};
// Thresholds for maintenance status
const ABANDONED_THRESHOLD_MONTHS = 36;
const STALE_THRESHOLD_MONTHS = 24;
function monthsSince(date) {
if (!date) return Infinity;
const then = new Date(date);
const now = new Date();
return Math.floor((now - then) / (1000 * 60 * 60 * 24 * 30));
}
async function analyzePackage(packageName) {
const result = {
name: packageName,
status: 'HEALTHY',
deprecated: false,
abandoned: false,
stale: false,
lastPublish: null,
monthsSinceUpdate: null,
deprecationMessage: null,
alternative: null,
source: 'live' // 'live' or 'fallback'
};
try {
const data = await registryClient.fetchPackage(packageName);
if (!data) {
// Package not found - check fallback
if (FALLBACK_ALTERNATIVES[packageName]) {
const fallback = FALLBACK_ALTERNATIVES[packageName];
result.status = 'DEPRECATED';
result.deprecated = true;
result.alternative = fallback.replacement;
result.deprecationMessage = fallback.reason;
result.source = 'fallback';
}
return result;
}
// Check deprecation status from registry
const latestVersion = data['dist-tags']?.latest;
const latestData = latestVersion ? data.versions?.[latestVersion] : null;
if (latestData?.deprecated || data.deprecated) {
result.deprecated = true;
result.status = 'DEPRECATED';
result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated';
}
// Check maintenance status
const timeData = data.time || {};
const lastModified = timeData.modified || timeData[latestVersion];
if (lastModified) {
result.lastPublish = lastModified;
result.monthsSinceUpdate = monthsSince(lastModified);
if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) {
result.abandoned = true;
if (result.status === 'HEALTHY') {
result.status = 'ABANDONED';
}
} else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) {
result.stale = true;
if (result.status === 'HEALTHY') {
result.status = 'STALE';
}
}
}
// Check fallback for known alternatives
if (FALLBACK_ALTERNATIVES[packageName]) {
result.alternative = FALLBACK_ALTERNATIVES[packageName].replacement;
}
return result;
} catch (error) {
// Network error - check fallback
if (FALLBACK_ALTERNATIVES[packageName]) {
const fallback = FALLBACK_ALTERNATIVES[packageName];
result.status = 'DEPRECATED';
result.deprecated = true;
result.alternative = fallback.replacement;
result.deprecationMessage = fallback.reason;
result.source = 'fallback';
}
return result;
}
}
async function analyzeBatch(packageNames) {
if (!Array.isArray(packageNames)) {
return new Map();
}
const results = new Map();
// Fetch all packages in parallel
const packageData = await registryClient.fetchBatch(packageNames);
// Analyze each package
for (const name of packageNames) {
if (!name || typeof name !== 'string') continue;
const data = packageData.get(name);
if (data) {
// We have live data
const result = {
name,
status: 'HEALTHY',
deprecated: false,
abandoned: false,
stale: false,
lastPublish: null,
monthsSinceUpdate: null,
deprecationMessage: null,
alternative: FALLBACK_ALTERNATIVES[name]?.replacement || null,
source: 'live'
};
// Check deprecation
const latestVersion = data['dist-tags']?.latest;
const latestData = latestVersion ? data.versions?.[latestVersion] : null;
if (latestData?.deprecated || data.deprecated) {
result.deprecated = true;
result.status = 'DEPRECATED';
result.deprecationMessage = latestData?.deprecated || data.deprecated || 'Package is deprecated';
}
// Check maintenance
const timeData = data.time || {};
const lastModified = timeData.modified || timeData[latestVersion];
if (lastModified) {
result.lastPublish = lastModified;
result.monthsSinceUpdate = monthsSince(lastModified);
if (result.monthsSinceUpdate >= ABANDONED_THRESHOLD_MONTHS) {
result.abandoned = true;
if (result.status === 'HEALTHY') result.status = 'ABANDONED';
} else if (result.monthsSinceUpdate >= STALE_THRESHOLD_MONTHS) {
result.stale = true;
if (result.status === 'HEALTHY') result.status = 'STALE';
}
}
results.set(name, result);
} else {
// Check fallback
if (FALLBACK_ALTERNATIVES[name]) {
const fallback = FALLBACK_ALTERNATIVES[name];
results.set(name, {
name,
status: 'DEPRECATED',
deprecated: true,
abandoned: false,
stale: false,
lastPublish: null,
monthsSinceUpdate: null,
deprecationMessage: fallback.reason,
alternative: fallback.replacement,
source: 'fallback'
});
} else {
// Unknown package
results.set(name, {
name,
status: 'UNKNOWN',
deprecated: false,
abandoned: false,
stale: false,
lastPublish: null,
monthsSinceUpdate: null,
deprecationMessage: null,
alternative: null,
source: 'none'
});
}
}
}
return results;
}
async function getProjectQualitySummary(packageNames) {
const results = await analyzeBatch(packageNames);
const summary = {
total: packageNames.length,
healthy: 0,
deprecated: 0,
abandoned: 0,
stale: 0,
unknown: 0,
issues: []
};
for (const [name, data] of results) {
switch (data.status) {
case 'HEALTHY':
summary.healthy++;
break;
case 'DEPRECATED':
summary.deprecated++;
summary.issues.push({
package: name,
type: 'deprecated',
message: data.deprecationMessage,
alternative: data.alternative
});
break;
case 'ABANDONED':
summary.abandoned++;
summary.issues.push({
package: name,
type: 'abandoned',
message: `No updates in ${data.monthsSinceUpdate} months`,
alternative: data.alternative
});
break;
case 'STALE':
summary.stale++;
summary.issues.push({
package: name,
type: 'stale',
message: `No updates in ${data.monthsSinceUpdate} months`,
alternative: data.alternative
});
break;
default:
summary.unknown++;
}
}
return summary;
}
function getAlternative(packageName) {
return FALLBACK_ALTERNATIVES[packageName]?.replacement || null;
}
module.exports = {
analyzePackage,
analyzeBatch,
getProjectQualitySummary,
getAlternative,
FALLBACK_ALTERNATIVES
};
// src/services/dynamic-security.js
const { execSync } = require('child_process');
const path = require('path');
// Popular packages for typosquatting comparison
const POPULAR_PACKAGES = [
'express', 'react', 'react-dom', 'lodash', 'axios', 'moment', 'webpack',
'typescript', 'jquery', 'vue', 'angular', 'next', 'gatsby', 'nuxt',
'babel', 'eslint', 'prettier', 'jest', 'mocha', 'chai', 'karma',
'underscore', 'request', 'async', 'bluebird', 'chalk', 'commander',
'inquirer', 'ora', 'yargs', 'minimist', 'glob', 'mkdirp', 'rimraf',
'fs-extra', 'uuid', 'semver', 'debug', 'dotenv', 'cors', 'helmet',
'mongoose', 'sequelize', 'knex', 'prisma', 'graphql', 'apollo',
'socket.io', 'redis', 'mysql', 'pg', 'sqlite3', 'mongodb',
'aws-sdk', 'firebase', 'stripe', 'paypal', 'twilio',
'nodemailer', 'pug', 'ejs', 'handlebars', 'mustache',
'body-parser', 'cookie-parser', 'multer', 'passport', 'bcrypt',
'jsonwebtoken', 'validator', 'yup', 'joi', 'zod'
];
// Whitelist - legitimate packages that might look suspicious
const WHITELIST = new Set([
'colors', 'chalk', 'ora', 'inquirer', 'prompts',
'cross-env', 'cross-spawn', 'execa', 'shelljs',
'node-fetch', 'axios', 'got', 'request', 'superagent',
'nodemailer', 'sendgrid', 'mailgun',
'bcrypt', 'bcryptjs', 'argon2', 'crypto-js',
'jsonwebtoken', 'jose', 'passport',
'puppeteer', 'playwright', 'selenium-webdriver',
'sharp', 'jimp', 'canvas', 'pdf-lib',
'esbuild', 'rollup', 'vite', 'parcel',
'husky', 'lint-staged', 'commitlint'
]);
// Suspicious patterns in install scripts
const SUSPICIOUS_PATTERNS = [
/curl\s+[^\|]+\|\s*sh/i, // curl | sh
/wget\s+[^\|]+\|\s*sh/i, // wget | sh
/eval\s*\(\s*["']?http/i, // eval with http
/base64\s*-d/i, // base64 decode
/\\x[0-9a-f]{2}/i, // hex escapes
/child_process.*exec/i, // exec with http
/require\s*\(\s*['"]https?:/i, // require http
/\.env|process\.env\.(AWS|STRIPE|API_KEY|SECRET|PASSWORD|TOKEN)/i, // credential access
/exfiltrate|steal|harvest/i, // obvious malicious
/bitcoin|btc|wallet|miner/i, // crypto mining
/keylog|screenshot|record/i // spyware
];
function levenshteinDistance(a, b) {
if (!a || !b) return Infinity;
const matrix = Array(b.length + 1).fill(null).map(() =>
Array(a.length + 1).fill(null)
);
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
for (let j = 1; j <= b.length; j++) {
for (let i = 1; i <= a.length; i++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1, // insertion
matrix[j - 1][i] + 1, // deletion
matrix[j - 1][i - 1] + cost // substitution
);
}
}
return matrix[b.length][a.length];
}
function checkTyposquatting(packageName, threshold = 2) {
if (!packageName || WHITELIST.has(packageName)) return null;
const name = packageName.toLowerCase().replace(/^@[^/]+\//, ''); // Remove scope
for (const popular of POPULAR_PACKAGES) {
const distance = levenshteinDistance(name, popular);
// Exact match is fine
if (distance === 0) return null;
// Within threshold and not too short
if (distance <= threshold && name.length >= 3 && popular.length >= 3) {
// Additional heuristics
const isSuspicious = (
name.includes(popular) || // lodash-extra
popular.includes(name) || // lod (subset)
name.replace(/[-_]/g, '') === popular.replace(/[-_]/g, '') || // lodash_js vs lodashjs
name.startsWith(popular.slice(0, 3)) || // Same prefix
name.endsWith(popular.slice(-3)) // Same suffix
);
if (isSuspicious || distance === 1) {
return {
package: packageName,
similarTo: popular,
distance,
warning: `Package name "${packageName}" is similar to popular package "${popular}"`
};
}
}
}
return null;
}
function checkInstallScripts(packageData) {
if (!packageData) return null;
const scripts = packageData.scripts || {};
const installScripts = ['preinstall', 'install', 'postinstall'];
const suspicious = [];
for (const scriptName of installScripts) {
const script = scripts[scriptName];
if (!script) continue;
for (const pattern of SUSPICIOUS_PATTERNS) {
if (pattern.test(script)) {
suspicious.push({
script: scriptName,
command: script.slice(0, 100) + (script.length > 100 ? '...' : ''),
pattern: pattern.source
});
break; // One match per script is enough
}
}
}
return suspicious.length > 0 ? {
package: packageData.name,
suspicious,
warning: `Package has suspicious install scripts`
} : null;
}
function runNpmAudit(projectPath) {
try {
// Run npm audit in JSON mode
const result = execSync('npm audit --json 2>/dev/null', {
cwd: projectPath,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB
});
const audit = JSON.parse(result);
return parseAuditResult(audit);
} catch (error) {
// npm audit exits with non-zero if vulnerabilities found
if (error.stdout) {
try {
const audit = JSON.parse(error.stdout);
return parseAuditResult(audit);
} catch (e) {
// Silent fail
}
}
return {
vulnerabilities: [],
summary: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 }
};
}
}
function parseAuditResult(audit) {
const vulnerabilities = [];
const summary = {
total: 0,
critical: 0,
high: 0,
moderate: 0,
low: 0,
info: 0
};
// Handle npm v7+ format
if (audit.vulnerabilities) {
for (const [name, data] of Object.entries(audit.vulnerabilities)) {
if (!data.via || !Array.isArray(data.via)) continue;
for (const via of data.via) {
if (typeof via === 'object' && via.title) {
vulnerabilities.push({
package: name,
severity: via.severity || 'unknown',
title: via.title,
url: via.url,
range: via.range || data.range
});
const sev = (via.severity || 'low').toLowerCase();
if (summary[sev] !== undefined) summary[sev]++;
summary.total++;
}
}
}
}
// Handle npm v6 format
if (audit.advisories) {
for (const [id, advisory] of Object.entries(audit.advisories)) {
vulnerabilities.push({
package: advisory.module_name,
severity: advisory.severity,
title: advisory.title,
url: advisory.url,
range: advisory.vulnerable_versions
});
const sev = (advisory.severity || 'low').toLowerCase();
if (summary[sev] !== undefined) summary[sev]++;
summary.total++;
}
}
return { vulnerabilities, summary };
}
async function analyzeProject(projectPath, dependencies = []) {
const result = {
typosquatting: [],
vulnerabilities: [],
suspiciousScripts: [],
summary: {
typosquattingCount: 0,
vulnerabilityCount: 0,
suspiciousScriptCount: 0,
criticalCount: 0,
highCount: 0
}
};
// Check for typosquatting
for (const dep of dependencies) {
if (!dep || WHITELIST.has(dep)) continue;
const typosquat = checkTyposquatting(dep);
if (typosquat) {
result.typosquatting.push(typosquat);
result.summary.typosquattingCount++;
}
}
// Run npm audit
if (projectPath) {
const audit = runNpmAudit(projectPath);
result.vulnerabilities = audit.vulnerabilities;
result.summary.vulnerabilityCount = audit.summary.total;
result.summary.criticalCount = audit.summary.critical;
result.summary.highCount = audit.summary.high;
}
return result;
}
function addToWhitelist(packageName) {
WHITELIST.add(packageName);
}
function removeFromWhitelist(packageName) {
WHITELIST.delete(packageName);
}
function isWhitelisted(packageName) {
return WHITELIST.has(packageName);
}
module.exports = {
checkTyposquatting,
checkInstallScripts,
runNpmAudit,
analyzeProject,
addToWhitelist,
removeFromWhitelist,
isWhitelisted,
POPULAR_PACKAGES,
WHITELIST
};
// src/services/index.js
const registryClient = require('./registry-client');
const dynamicQuality = require('./dynamic-quality');
const dynamicLicense = require('./dynamic-license');
const dynamicSecurity = require('./dynamic-security');
/**
* DynamicAnalyzer class - unified interface for all dynamic services
*/
class DynamicAnalyzer {
constructor() {
this.quality = dynamicQuality;
this.license = dynamicLicense;
this.security = dynamicSecurity;
this.registry = registryClient;
}
async analyzePackage(packageName) {
const [qualityResult, licenseResult] = await Promise.all([
this.quality.analyzePackage(packageName),
this.license.analyzePackage(packageName)
]);
const typosquatResult = this.security.checkTyposquatting(packageName);
return {
name: packageName,
quality: qualityResult,
license: licenseResult,
security: {
typosquatting: typosquatResult,
isWhitelisted: this.security.isWhitelisted(packageName)
},
hasIssues: (
qualityResult.status !== 'HEALTHY' ||
licenseResult.hasIssue ||
typosquatResult !== null
)
};
}
async analyzePackages(packageNames) {
const results = new Map();
const [qualityResults, licenseResults] = await Promise.all([
this.quality.analyzeBatch(packageNames),
this.license.analyzeBatch(packageNames)
]);
for (const name of packageNames) {
const quality = qualityResults.get(name) || { status: 'UNKNOWN' };
const license = licenseResults.get(name) || { hasIssue: false };
const typosquat = this.security.checkTyposquatting(name);
results.set(name, {
name,
quality,
license,
security: {
typosquatting: typosquat,
isWhitelisted: this.security.isWhitelisted(name)
},
hasIssues: (
quality.status !== 'HEALTHY' ||
license.hasIssue ||
typosquat !== null
)
});
}
return results;
}
async analyzeProject(projectPath, packageJson = {}) {
const deps = [
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.devDependencies || {})
];
const [packageResults, securityResults, qualitySummary, licenseConflicts] = await Promise.all([
this.analyzePackages(deps),
this.security.analyzeProject(projectPath, deps),
this.quality.getProjectQualitySummary(deps),
this.license.getLicenseConflicts(deps)
]);
// Compile warnings
const warnings = [];
// Quality warnings
for (const issue of qualitySummary.issues) {
warnings.push({
type: issue.type.toUpperCase(),
package: issue.package,
message: issue.message,
alternative: issue.alternative,
category: 'quality'
});
}
// License warnings
for (const conflict of [...licenseConflicts.critical, ...licenseConflicts.high]) {
warnings.push({
type: 'LICENSE',
package: conflict.package,
message: `${conflict.license}: ${conflict.message}`,
alternative: conflict.alternative,
category: 'license'
});
}
// Security warnings
for (const typosquat of securityResults.typosquatting) {
warnings.push({
type: 'TYPOSQUAT',
package: typosquat.package,
message: typosquat.warning,
similarTo: typosquat.similarTo,
category: 'security'
});
}
for (const vuln of securityResults.vulnerabilities) {
warnings.push({
type: vuln.severity.toUpperCase(),
package: vuln.package,
message: vuln.title,
url: vuln.url,
category: 'security'
});
}
return {
projectPath,
totalDependencies: deps.length,
summary: {
quality: qualitySummary,
license: licenseConflicts,
security: securityResults.summary
},
warnings,
details: packageResults
};
}
getAutofixRecommendations(analysisResult) {
const recommendations = [];
for (const warning of analysisResult.warnings || []) {
if (warning.alternative) {
recommendations.push({
action: 'replace',
package: warning.package,
replacement: warning.alternative,
reason: warning.message,
category: warning.category,
priority: warning.type === 'CRITICAL' ? 1 :
warning.type === 'HIGH' ? 2 :
warning.type === 'DEPRECATED' ? 3 : 4
});
} else if (warning.type === 'TYPOSQUAT') {
recommendations.push({
action: 'remove',
package: warning.package,
reason: warning.message,
category: 'security',
priority: 1
});
} else if (warning.type === 'CRITICAL' || warning.type === 'HIGH') {
recommendations.push({
action: 'update',
package: warning.package,
reason: warning.message,
category: 'security',
priority: warning.type === 'CRITICAL' ? 1 : 2
});
}
}
// Sort by priority
recommendations.sort((a, b) => a.priority - b.priority);
return recommendations;
}
/**
* Clear all caches
*/
clearCache() {
this.registry.clearCache();
}
/**
* Get cache statistics
* @returns {Object}
*/
getCacheStats() {
return this.registry.getCacheStats();
}
}
// Export singleton instance
const analyzer = new DynamicAnalyzer();
module.exports = {
DynamicAnalyzer,
analyzer,
// Re-export individual services
registryClient,
dynamicQuality,
dynamicLicense,
dynamicSecurity
};
//src/services/registry-client.js
const https = require('https');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Cache configuration
const CACHE_DIR = path.join(os.homedir(), '.depcompass', 'cache');
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
const MAX_CONCURRENCY = 5;
const REQUEST_TIMEOUT = 10000; // 10 seconds
const MAX_RETRIES = 3;
const INITIAL_RETRY_DELAY = 1000;
// Memory cache for current session
const memoryCache = new Map();
/**
* Ensure cache directory exists
*/
function ensureCacheDir() {
try {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
} catch (error) {
// Silent fail - cache is optional
}
}
function getCachePath(packageName) {
// Sanitize package name for filesystem
const safeName = packageName.replace(/\//g, '__').replace(/@/g, '_at_');
return path.join(CACHE_DIR, `${safeName}.json`);
}
function readFromDiskCache(packageName) {
try {
const cachePath = getCachePath(packageName);
if (!fs.existsSync(cachePath)) return null;
const stats = fs.statSync(cachePath);
const ageMs = Date.now() - stats.mtimeMs;
// Check if cache is expired
if (ageMs > CACHE_TTL_MS) {
fs.unlinkSync(cachePath); // Delete expired cache
return null;
}
const data = fs.readFileSync(cachePath, 'utf8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
function writeToDiskCache(packageName, data) {
try {
ensureCacheDir();
const cachePath = getCachePath(packageName);
fs.writeFileSync(cachePath, JSON.stringify(data), 'utf8');
} catch (error) {
// Silent fail - cache is optional
}
}
function httpsGet(url, retryCount = 0) {
return new Promise((resolve, reject) => {
const request = https.get(url, {
timeout: REQUEST_TIMEOUT,
headers: {
'Accept': 'application/json',
'User-Agent': 'depcompass-cli'
}
}, (response) => {
// Handle rate limiting (429)
if (response.statusCode === 429) {
if (retryCount < MAX_RETRIES) {
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
setTimeout(() => {
httpsGet(url, retryCount + 1).then(resolve).catch(reject);
}, delay);
return;
}
reject(new Error('Rate limited'));
return;
}
// Handle 404 (package not found)
if (response.statusCode === 404) {
resolve(null);
return;
}
// Handle other errors
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Invalid JSON response'));
}
});
});
request.on('error', (error) => {
if (retryCount < MAX_RETRIES) {
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
setTimeout(() => {
httpsGet(url, retryCount + 1).then(resolve).catch(reject);
}, delay);
return;
}
reject(error);
});
request.on('timeout', () => {
request.destroy();
if (retryCount < MAX_RETRIES) {
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
setTimeout(() => {
httpsGet(url, retryCount + 1).then(resolve).catch(reject);
}, delay);
return;
}
reject(new Error('Request timeout'));
});
});
}
async function fetchPackage(packageName, useCache = true) {
if (!packageName || typeof packageName !== 'string') {
return null;
}
// Check memory cache first
if (useCache && memoryCache.has(packageName)) {
return memoryCache.get(packageName);
}
// Check disk cache
if (useCache) {
const cached = readFromDiskCache(packageName);
if (cached) {
memoryCache.set(packageName, cached);
return cached;
}
}
try {
// Encode package name for URL (handles scoped packages like @org/pkg)
const encodedName = encodeURIComponent(packageName).replace('%40', '@');
const url = `https://registry.npmjs.org/${encodedName}`;
const data = await httpsGet(url);
if (data) {
// Cache the result
memoryCache.set(packageName, data);
writeToDiskCache(packageName, data);
}
return data;
} catch (error) {
// Return null on error (package may not exist or network issue)
return null;
}
}
async function fetchBatch(packageNames, useCache = true) {
if (!Array.isArray(packageNames)) {
return new Map();
}
const results = new Map();
const uniqueNames = [...new Set(packageNames.filter(n => n && typeof n === 'string'))];
// Process in batches to limit concurrency
for (let i = 0; i < uniqueNames.length; i += MAX_CONCURRENCY) {
const batch = uniqueNames.slice(i, i + MAX_CONCURRENCY);
const batchResults = await Promise.all(
batch.map(name => fetchPackage(name, useCache).then(data => ({ name, data })))
);
for (const { name, data } of batchResults) {
if (data) {
results.set(name, data);
}
}
}
return results;
}
/**
* Clear all caches (memory and disk)
*/
function clearCache() {
memoryCache.clear();
try {
if (fs.existsSync(CACHE_DIR)) {
const files = fs.readdirSync(CACHE_DIR);
for (const file of files) {
if (file.endsWith('.json')) {
fs.unlinkSync(path.join(CACHE_DIR, file));
}
}
}
} catch (error) {
// Silent fail
}
}
/**
* Get cache statistics
* @returns {Object}
*/
function getCacheStats() {
let diskCount = 0;
let diskSize = 0;
try {
if (fs.existsSync(CACHE_DIR)) {
const files = fs.readdirSync(CACHE_DIR);
for (const file of files) {
if (file.endsWith('.json')) {
diskCount++;
const stats = fs.statSync(path.join(CACHE_DIR, file));
diskSize += stats.size;
}
}
}
} catch (error) {
// Silent fail
}
return {
memoryCount: memoryCache.size,
diskCount,
diskSizeMB: (diskSize / (1024 * 1024)).toFixed(2),
cacheDir: CACHE_DIR
};
}
module.exports = {
fetchPackage,
fetchBatch,
clearCache,
getCacheStats
};
+16
-4
{
"name": "devcompass",
"version": "3.1.3",
"description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, advanced interactive dependency graph visualization with multiple layouts, dynamic issue detection, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, search & filter capabilities, and interactive dependency exploration.",
"version": "3.1.4",
"description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, unified interactive dependency graph with dynamic layout switching, real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.",
"main": "src/index.js",

@@ -84,2 +84,6 @@ "bin": {

"graph-visualization",
"unified-graph",
"interactive-graph",
"dynamic-layout-switching",
"real-time-filtering",
"force-directed-graph",

@@ -89,3 +93,2 @@ "radial-layout",

"conflict-view",
"interactive-graph",
"graph-export",

@@ -97,2 +100,11 @@ "graph-search",

"visual-analysis",
"zoom-controls",
"fit-to-screen",
"center-view",
"export-png",
"export-json",
"fullscreen-mode",
"single-file-graph",
"no-page-reload",
"live-statistics",
"dynamic-issues",

@@ -122,2 +134,2 @@ "real-time-vulnerabilities"

"homepage": "https://github.com/AjayBThorat-20/devcompass#readme"
}
}
+403
-421
# 🧭 DevCompass
**Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, advanced interactive dependency graph visualization with multiple layouts, dynamic issue detection, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes with granular control, backup & rollback, parallel processing, search & filter capabilities, and enhanced fix command with dry-run mode, progress tracking, and automatic backups.**
**Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, unified interactive dependency graph with dynamic layout switching, real-time filtering, advanced zoom controls, supply chain security with auto-fix, license conflict resolution with auto-fix, package quality auto-fix, batch fix modes with granular control, backup & rollback, and professional dependency exploration - all in a single interactive HTML file.**

@@ -9,327 +9,338 @@ [![npm version](https://img.shields.io/npm/v/devcompass.svg)](https://www.npmjs.com/package/devcompass)

Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 500+ packages**, **visualize dependency graphs with multiple interactive layouts**, **search and filter packages**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes.
Analyze your JavaScript projects to find unused dependencies, outdated packages, **detect security vulnerabilities**, **monitor GitHub issues in real-time for 500+ packages**, **visualize dependency graphs with unified interactive interface**, **instant layout switching**, **real-time filtering**, **advanced zoom controls**, **check bundle sizes**, **verify licenses**, **detect and auto-fix supply chain attacks**, **resolve license conflicts automatically**, **replace abandoned/deprecated packages automatically**, **analyze package quality**, **batch fix with granular control**, **manage backups and rollback changes**, and **automatically fix issues with dry-run, progress tracking, and backups**. Perfect for **CI/CD pipelines** with JSON output and exit codes.
> **✨ LATEST v3.1.2:** Graph Layout Fixes & Dynamic Issues - Tree/Radial layouts fixed, real-time vulnerability detection! 🎯
> **🛡️ PREVIOUS v3.1.1:** Production Safety & Stability - Comprehensive bug fixes and hardening! 🛡️
> **🎨 v3.1.0:** Advanced Graph Visualization - Force-directed layouts, radial views, search & filter! 🎨
> **🎨 LATEST v3.1.4:** Unified Interactive Graph System - 40+ files → 1 file with dynamic controls! 🎨
> **✨ PREVIOUS v3.1.3:** Cleanup & Code Improvements - Removed unused dependencies! 🧹
> **🎯 v3.1.2:** Graph Layout Fixes & Dynamic Issues - Tree/Radial layouts fixed! 🎯
## 🎉 Latest Release: v3.1.2 (2026-04-17)
## 🎉 Latest Release: v3.1.4 (2026-04-20)
**Graph layout fixes and dynamic issue detection!**
**Unified Interactive Graph System - Complete Redesign!**
### What's New in v3.1.2:
### 🌟 What's New in v3.1.4:
- 🌳 **Tree Layout Fix** - Proper horizontal spreading (was vertical line)
- 🎯 **Radial Layout Fix** - Labels positioned outside nodes (no overlap)
- 📊 **Panel Separation** - Controls/Statistics panels no longer overlap
- 🔄 **Dynamic Issues Analyzer** - Real-time npm audit integration (replaces hardcoded issues-db.json)
- ⚡ **Async Graph Generation** - Proper async/await handling
- 🎨 **Conflict Layout Improved** - Card-based UI organized by severity
- 🔗 **Unified Visualizer** - Single entry point for all layout types
#### **97% File Reduction - Revolutionary Simplification!**
### Critical Fixes:
**Before v3.1.4:**
```bash
# v3.1.1 (Had Layout Issues)
❌ Tree layout: All nodes in single vertical line
❌ Radial layout: Labels overlapping nodes
❌ Tree layout: Controls/Statistics panels overlapping
❌ Issues: Hardcoded issues-db.json (only 5 packages)
❌ Graph generation: Missing await on async generate()
40+ separate HTML files:
# v3.1.2 (All Fixed) ⭐
✅ Tree layout: Proper horizontal spreading with D3.js d3.tree()
✅ Tree layout: Curved links with d3.linkHorizontal()
✅ Tree layout: Correct sibling separation
✅ Radial layout: Smart label positioning outside nodes
✅ Radial layout: Text truncation (15 char max)
✅ Radial layout: Staggered angles per depth level
✅ Panel layout: Right-sidebar with flexbox (16px gap)
✅ Dynamic issues: Real-time npm audit + registry checks
✅ Dynamic issues: Works for ANY package (not just hardcoded list)
✅ Conflict layout: Card-based UI with collapsible sections
✅ Async fix: Proper await on generator.generate()
```
graph-tree.html
graph-force.html
graph-radial.html
graph-filter-vulnerable.html
combo-tree-vulnerable.html
combo-force-outdated.html
... (34+ more files)
Total: ~4-5 MB
### Upgrade Now:
**After v3.1.4:**
```bash
npm install -g devcompass@3.1.2
```
1 unified HTML file:
### Migration from v3.1.1:
dependency-graph.html (107 KB)
✨ Contains ALL:
**No changes required - drop-in replacement!** All existing functionality works exactly the same, with fixed layouts and dynamic issue detection.
4 layouts (switchable)
5 filters (switchable)
Depth control
Search
Export options
Total: 107 KB (97% reduction!)
---
## 🎨 Graph Visualization Features
DevCompass v3.1.0+ includes **powerful, interactive dependency graph visualization** with multiple layouts!
### 🎯 Available Layouts
#### 1. **Tree Layout** (Default) - FIXED in v3.1.2! 🆕
Classic hierarchical view with clear parent-child relationships.
#### **🎨 Unified Interactive Features**
```bash
devcompass graph --layout tree
```
- **Dynamic Layout Switcher** - Tree/Force/Radial/Conflict buttons (no page reload!)
- **Real-time Filters** - All/Vulnerable/Outdated/Deprecated/Unused (instant updates)
- **Depth Slider** - 1-10 with ∞ option (live filtering)
- **Live Search** - Instant package name filtering
- **Advanced Zoom Controls:**
- 🔍+ Zoom In (1.3x magnification)
- 🔍− Zoom Out (0.7x reduction)
- ⟲ Reset Zoom (return to 1:1)
- ⛶ Fit to Screen (auto-scale entire graph)
- ⊙ Center View (smart bounding box centering)
- **Export Capabilities:**
- 📸 Save as PNG (download current view)
- 💾 Save as JSON (export filtered data)
- **🖵 Fullscreen Mode** - Immersive graph exploration
- **📊 Live Statistics** - Real-time node/link counts
- ✅ Best for: Understanding dependency hierarchy
- ✅ **FIXED:** Proper horizontal spreading (root left, children right)
- ✅ **FIXED:** Curved links with d3.linkHorizontal()
- ✅ **FIXED:** Correct sibling vertical separation
- ✅ **FIXED:** Controls/Statistics panels separated
#### **🔧 Enhanced Tree Layout**
#### 2. **Force-Directed Layout**
- **Fixed label overlap** - Intelligent positioning (above parents, below leaves)
- **Increased spacing** - 1.5x-2x separation for clarity
- **Auto-fit on render** - Graph scales automatically
- **Text truncation** - 15 character limit with ellipsis
- **Better readability** - Improved font sizing
Interactive physics-based network visualization with ultimate UI!
#### **⚡ Performance Improvements**
| Operation | Time | Speed |
|-----------|------|-------|
| Layout switch | <100ms | No page reload! |
| Filter update | <50ms | Real-time |
| Search | <20ms | Instant |
| Initial render | <100ms | Lightning fast |
| Export PNG | ~1-2s | Professional quality |
### 🚀 Upgrade Now:
```bash
devcompass graph --layout force --open
npm install -g devcompass@3.1.4
```
- ✅ Best for: Exploring complex relationships
- ✅ Drag individual nodes to reposition
- ✅ Natural clustering of related packages
- ✅ Real-time physics simulation
- ✅ Zoom controls (buttons + keyboard + mouse)
- ✅ Click to highlight connections
- ✅ Search and filter functionality
- ✅ Fullscreen mode
- ✅ Modern dark theme
- ✅ Interactive and engaging
### 📈 Migration from v3.1.3:
#### 3. **Radial/Circular Layout** - FIXED in v3.1.2! 🆕
**No changes required - drop-in replacement!** All existing commands work identically. The unified graph is now the default behavior.
Concentric circles based on dependency depth.
**Workflow Comparison:**
**Before v3.1.4:**
```bash
devcompass graph --layout radial
# Generate multiple files
devcompass graph --layout tree --output graph-tree.html
devcompass graph --layout force --output graph-force.html
devcompass graph --filter vulnerable --output graph-vulnerable.html
# Result: 3 commands, 3 files, 3 browser tabs
```
- ✅ Best for: Understanding dependency levels
- ✅ Root package at center
- ✅ Dependencies radiating outward
- ✅ **FIXED:** Labels positioned OUTSIDE nodes
- ✅ **FIXED:** Smart text-anchor based on angle
- ✅ **FIXED:** Long names truncated (15 char max)
- ✅ **FIXED:** Staggered angles to avoid overlap
- ✅ Concentric depth circles visible
#### 4. **Conflict-Only View** - IMPROVED in v3.1.2! 🆕
Shows only packages with issues - instantly identify problems!
**After v3.1.4:**
```bash
devcompass graph --layout conflict --open
# Generate one unified file
devcompass graph
# Result: 1 command, 1 file, instant switching via buttons!
```
- ✅ Best for: Quick issue identification
- ✅ Filters out healthy dependencies automatically
- ✅ **IMPROVED:** Card-based layout organized by severity
- ✅ **IMPROVED:** Collapsible sections
- ✅ **IMPROVED:** Summary cards with counts
- ✅ **IMPROVED:** Beautiful 'No Conflicts' success state
- ✅ Color-coded by severity level
---
### 🔄 Dynamic Issue Detection (NEW in v3.1.2!) 🆕
## 🎨 Unified Graph Visualization (v3.1.4)
DevCompass now detects issues in **real-time** for ANY package!
DevCompass now features a **revolutionary unified interactive graph** - all layouts, filters, and controls in one beautiful interface!
### 🎯 Single Command, Infinite Possibilities
```bash
# Replaces hardcoded issues-db.json
devcompass analyze
# Generate unified interactive graph
devcompass graph
# Open in browser
devcompass graph --open
```
**Sources:**
- 🔐 **npm audit** - Real security vulnerabilities
- 📦 **npm registry** - Deprecation status
- 📅 **npm registry** - Maintenance status (unmaintained if 2+ years)
**What you get in ONE file:**
- ✅ All 4 layouts (Tree, Force, Radial, Conflict) - switchable via buttons
- ✅ All 5 filters (All, Vulnerable, Outdated, Deprecated, Unused) - instant filtering
- ✅ Depth control slider (1-10, ∞)
- ✅ Live search functionality
- ✅ Advanced zoom controls (In/Out/Reset/Fit/Center)
- ✅ Export options (PNG/JSON)
- ✅ Fullscreen mode
- ✅ Real-time statistics
- ✅ Professional UI/UX
- ✅ **No page reload needed!**
**Before v3.1.2:** Only detected issues for 5 hardcoded packages (axios, lodash, moment, request, express)
### 🎮 Interactive Controls
**After v3.1.2:** Detects issues for ALL packages dynamically!
#### **Layout Switcher**
Click buttons to instantly switch between layouts:
### 📤 Export Formats
1. **🌳 Tree Layout** - Hierarchical view (root left, children right)
- Best for: Understanding dependency hierarchy
- Fixed: Proper horizontal spreading
- Fixed: Curved links with smooth connections
- Fixed: Intelligent label positioning
#### HTML Export (Default)
2. **🌀 Force Layout** - Physics-based network
- Best for: Exploring complex relationships
- Drag nodes to reposition
- Natural clustering
- Real-time simulation
Interactive D3.js visualization with all features.
3. **⭕ Radial Layout** - Circular/concentric view
- Best for: Understanding dependency levels
- Root at center
- Dependencies radiating outward
- Fixed: Labels outside nodes
```bash
devcompass graph --output my-graph.html --open
```
4. **⚠️ Conflict Layout** - Issues-only view
- Best for: Quick issue identification
- Card-based UI by severity
- Collapsible sections
- Beautiful "No Conflicts" state
- ✅ Full interactivity
- ✅ Zoom, pan, and explore
- ✅ Hover tooltips
- ✅ Search and filter
- ✅ Keyboard shortcuts
- ✅ All layout options
#### **Filter Controls**
Click buttons to filter packages in real-time:
#### JSON Export
- **All** - Show everything
- **Vulnerable** - Security issues only
- **Outdated** - Packages with updates available
- **Deprecated** - Officially deprecated packages
- **Unused** - Unused dependencies
Complete graph data structure for programmatic access.
#### **Depth Slider**
Drag slider to control dependency depth:
- **1-9** - Show specific depth level
- **∞** (at position 10) - Show unlimited depth
```bash
devcompass graph --format json --output graph.json
```
#### **Search Box**
Type package name for instant filtering:
- Real-time updates as you type
- Highlights matching nodes
- Shows filtered count
- ✅ Full graph data (nodes + links)
- ✅ Programmatic access
- ✅ Integration with custom tools
- ✅ Lightweight and fast
#### **Zoom Controls**
Professional zoom management:
- **🔍+ Zoom In** - Magnify by 1.3x
- **🔍− Zoom Out** - Reduce by 0.7x
- **⟲ Reset** - Return to 1:1 scale
- **⛶ Fit** - Auto-scale to show entire graph
- **⊙ Center** - Smart centering within container
### 🔍 Search & Filter
#### **Export Controls**
Save your current view:
- **📸 PNG** - Download as image (current zoom/filter)
- **💾 JSON** - Export filtered data structure
**Real-time Search:**
- 🔍 Instant package name search (Press F to focus)
- 📦 Search by version
- ⚡ Live results as you type
- 🎯 Highlight matching nodes
- 📋 Quick navigation to packages
#### **Fullscreen Mode**
- **🖵 Fullscreen** - Toggle immersive view
- Press ESC to exit
**Advanced Filters:**
- ⚠️ Show only vulnerable packages
- 📅 Show only outdated packages
- 🚫 Show only deprecated packages
- 💚 Filter by health score (Critical/Warning/Caution/Healthy)
- 📊 Filter by dependency depth level
- 📦 Filter by package type
- 🔄 Combine multiple filters
- ⚡ Real-time graph updates with live stats
### 📊 Live Statistics Panel
**Filter UI Features:**
- 📊 Live statistics (visible/hidden nodes and links)
- 🔄 Filter reset button
- 🎨 Highlighted filtered nodes
- 🔗 Updated link connections
- 💡 Interactive dropdowns
Real-time metrics updated on every action:
### 🎨 Interactive Features
Statistics
───────────────
Total: 142
Vulnerable: 14
Deprecated: 0
Outdated: 4
Unused: 5
Healthy: 119
**All Layouts Include:**
- 🔍 Zoom controls (in/out/reset/fit-to-screen)
- 👆 Pan and drag support
- 💡 Hover tooltips with package details
- 🎯 Node highlighting on search
- ⚡ Smooth transitions and animations
- 📱 Responsive design
- ⌨️ Keyboard shortcuts
### 🎨 Color-Coded Health
**Force Layout Specific:**
- 🎮 Drag individual nodes
- 🌀 Real-time physics simulation
- 🔄 Reset layout button
- ⚙️ Toggle labels/links
- 🎯 Click to highlight connections
- 📊 Stats panel with selection count
- 🖥️ Fullscreen mode (cross-browser compatible)
Visual health indicators:
- 🟢 **Excellent (9-10)** - Green - Well-maintained
- 🟡 **Good (7-8)** - Light green - Healthy
- 🟠 **Fair (5-6)** - Yellow - Monitor
- 🔴 **Poor (3-4)** - Orange - Needs attention
- ⛔ **Critical (0-2)** - Red - Immediate action
**Keyboard Shortcuts:**
- **F** - Focus search box
- **R** - Reset simulation (force layout)
- **C** - Center view
- **+** or **=** - Zoom in
- **-** or **_** - Zoom out
- **ESC** - Clear selection
### 🚀 Example Workflow
---
```bash
# 1. Analyze project
devcompass analyze
## 🎉 Recent Updates
# 2. Generate unified graph
devcompass graph --open
### v3.1.2 (2026-04-17) - Graph Layout Fixes & Dynamic Issues
# 3. Explore interactively:
# - Click "Vulnerable" filter → See security issues
# - Click "Force" layout → Explore relationships
# - Click "Fit to Screen" → See entire graph
# - Search "axios" → Find specific package
# - Click "Export PNG" → Save screenshot
**Major layout fixes and dynamic issue detection!** This release fixes critical graph layout issues and adds real-time vulnerability detection.
# 4. Fix issues
devcompass fix --batch-mode high
**Layout Fixes:**
- ✅ **Tree layout horizontal spreading** - Proper D3.js d3.tree() implementation
- ✅ **Tree layout curved links** - Using d3.linkHorizontal() for beautiful connections
- ✅ **Tree layout sibling separation** - Nodes properly spaced vertically
- ✅ **Radial layout labels** - Positioned outside nodes, no overlap
- ✅ **Radial layout text truncation** - 15 character max with ellipsis
- ✅ **Panel separation** - Controls/Statistics in right-sidebar with flexbox
# 5. Re-visualize
devcompass graph --open
# - Click "Conflict" layout → Verify fixes
```
**New Features:**
- ✅ **Dynamic Issues Analyzer** - Real-time npm audit integration
- ✅ **Live deprecation detection** - Checks npm registry for deprecated packages
- ✅ **Maintenance status** - Identifies unmaintained packages (2+ years)
- ✅ **Works for ANY package** - No more hardcoded issues-db.json
- ✅ **Unified visualizer** - Single entry point for all layout types
- ✅ **Async graph generation** - Proper await handling
### 📈 Usage Examples
**Files Updated (6):**
- `src/graph/layouts/tree.js` - Complete rewrite with proper D3.js tree
- `src/graph/layouts/radial.js` - Fixed label positioning
- `src/graph/generator.js` - Async + dynamic issues + boolean flags
- `src/alerts/index.js` - Dynamic alerts for any package
- `src/commands/graph.js` - Await fix + enrichWithIssues option
- `src/analyzers/issues.js` - NEW dynamic issues analyzer
#### **Quick Security Audit**
```bash
devcompass graph --open
# In browser: Click "Vulnerable" filter → See all security issues
```
### v3.1.1 (2026-04-14) - Production Safety & Stability
#### **Dependency Exploration**
```bash
devcompass graph --open
# In browser:
# 1. Click "Force" layout
# 2. Drag nodes around
# 3. Click nodes to highlight connections
# 4. Use search to find packages
```
**Comprehensive bug fixes and production hardening!** This patch release fixes critical bugs and adds 100+ validation checks for production stability.
#### **Professional Documentation**
```bash
devcompass graph --open
# In browser:
# 1. Click "Tree" layout
# 2. Click "Fit to Screen"
# 3. Click "Export PNG"
# → Add screenshot to docs
```
**Critical Fixes:**
- ✅ **Fixed JSON mode crash** - `supplyChainData.warnings` undefined error resolved
- ✅ **Fixed division by zero** - Safe calculations in trend analysis
- ✅ **Fixed unsafe array operations** - Array.isArray() checks everywhere
- ✅ **Fixed cross-browser fullscreen** - Firefox/Safari/Chrome support
- ✅ **Added input validation** - All public functions validate inputs
- ✅ **Added null-safe operations** - Optional chaining throughout
- ✅ **Enhanced error handling** - Try-catch wrappers for external calls
- ✅ **Added graceful fallbacks** - Safe degradation for all operations
#### **Deep Dependency Analysis**
```bash
devcompass graph --open
# In browser:
# 1. Drag depth slider to "2"
# 2. Click "Radial" layout
# 3. See direct + transitive dependencies
```
### v3.1.0 (2026-04-08) - Advanced Graph Visualization
---
**Multiple layouts, search/filter, and enhanced UI!** This release dramatically enhances graph visualization capabilities.
## 🔄 Dynamic Issue Detection (v3.1.2+)
**What's New:**
- ✅ Force-directed network layout (interactive physics)
- ✅ Radial/circular layout (dependency levels)
- ✅ Conflict-only view (issues at a glance)
- ✅ Real-time search functionality
- ✅ Advanced filtering options
- ✅ Zoom controls (buttons + keyboard + mouse)
- ✅ Fullscreen mode
- ✅ Click to highlight connections
- ✅ Modern dark theme UI
- ✅ Improved performance
DevCompass detects issues in **real-time** for ANY package!
### v3.0.2 (2026-04-08) - Modern Dependency Analysis
```bash
devcompass analyze
devcompass graph --open
# Click "Vulnerable" filter to see live security issues
```
**Modern tooling for better accuracy!** This release replaces the stale `depcheck` package with `knip`, providing faster and more accurate unused dependency detection.
**Sources:**
- 🔐 **npm audit** - Real security vulnerabilities
- 📦 **npm registry** - Deprecation status
- 📅 **npm registry** - Maintenance status (2+ years = unmaintained)
**Coverage:** Works for ALL packages, not just a hardcoded list!
---
## ✨ Features
## ✨ All Features at a Glance
- 🎨 **Unified Interactive Graph** (v3.1.4) - 40+ files → 1 file with dynamic controls
- 🧹 **Cleanup & Code Improvements** (v3.1.3) - Removed unused dependencies
- 🎯 **Graph Layout Fixes** (v3.1.2) - Tree/Radial layouts properly fixed
- 🔄 **Dynamic Issue Detection** (v3.1.2) - Real-time npm audit integration
- 🛡️ **Production Safety & Stability** (v3.1.1) - Comprehensive bug fixes and hardening
- 🎨 **Advanced Graph Visualization** (v3.1.0) - Multiple layouts, search & filter, interactive UI
- ✨ **Modern Dependency Analysis** (v3.0.2) - Replaced depcheck with knip for better accuracy
- 📊 **Dependency Graph Visualization** (v3.0.0) - Interactive D3.js graphs with health-based color coding
- 📦 **Batch Fix Modes** (v2.8.5) - Granular control over which categories to fix
- 💾 **Backup & Rollback** (v2.8.4) - Complete backup management for safe dependency fixes
- 📦 **Package Quality Auto-Fix** (v2.8.3) - Automatic replacement of abandoned/deprecated packages
- ⚖️ **License Conflict Auto-Fix** (v2.8.2) - Automatic GPL/AGPL replacement with MIT alternatives
- 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Automatic malicious package removal & typosquatting fixes
- 🔧 **Enhanced Fix Command** (v2.8.0) - Dry-run, progress tracking, backups & reports
- 🛡️ **Supply Chain Security** (v2.7) - Malicious package & typosquatting detection
- ⚖️ **License Risk Analysis** (v2.7) - Enhanced license compliance checking
- 📊 **Package Quality Metrics** (v2.7) - Health scoring for dependencies
- 💡 **Security Recommendations** (v2.7) - Prioritized, actionable fixes
- ⚡ **Parallel Processing** (v2.6) - 80% faster GitHub issue tracking
- 🎯 **500+ Package Coverage** (v2.5) - Comprehensive ecosystem monitoring
- 🔮 **GitHub Issue Tracking** (v2.4) - Real-time monitoring of package health
- 📈 **Predictive Warnings** (v2.4) - Detect issues before they're announced
- 🔐 **Security Scanning** (v2.3) - npm audit integration with severity breakdown
- 📦 **Bundle Size Analysis** (v2.3) - Identify heavy packages (> 1MB)
- ⚖️ **License Checker** (v2.3) - Detect restrictive licenses (GPL, AGPL)
- 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes, and silent mode
- ⚡ **Smart Caching** (v2.2) - 70% faster on repeated runs
- 🛡️ **Production Safety** (v3.1.1) - Comprehensive bug fixes and hardening
- 🎨 **Advanced Graph Visualization** (v3.1.0) - Multiple layouts, search & filter
- ✨ **Modern Dependency Analysis** (v3.0.2) - Replaced depcheck with knip
- 📊 **Interactive Graphs** (v3.0.0) - D3.js visualizations
- 📦 **Batch Fix Modes** (v2.8.5) - Granular control
- 💾 **Backup & Rollback** (v2.8.4) - Safe dependency management
- 📦 **Package Quality Auto-Fix** (v2.8.3) - Replace abandoned packages
- ⚖️ **License Conflict Auto-Fix** (v2.8.2) - GPL/AGPL replacement
- 🛡️ **Supply Chain Auto-Fix** (v2.8.1) - Malicious package removal
- 🔧 **Enhanced Fix Command** (v2.8.0) - Dry-run, progress tracking, backups
- 🛡️ **Supply Chain Security** (v2.7) - Malicious package detection
- ⚖️ **License Risk Analysis** (v2.7) - License compliance
- 📊 **Package Quality Metrics** (v2.7) - Health scoring
- 💡 **Security Recommendations** (v2.7) - Prioritized fixes
- ⚡ **Parallel Processing** (v2.6) - 80% faster GitHub tracking
- 🎯 **500+ Package Coverage** (v2.5) - Comprehensive monitoring
- 🔮 **GitHub Issue Tracking** (v2.4) - Real-time package health
- 🔐 **Security Scanning** (v2.3) - npm audit integration
- 📦 **Bundle Size Analysis** (v2.3) - Identify heavy packages
- ⚖️ **License Checker** (v2.3) - Detect restrictive licenses
- 🚀 **CI/CD Integration** (v2.2) - JSON output, exit codes
- ⚡ **Smart Caching** (v2.2) - 70% faster repeated runs

@@ -341,3 +352,3 @@ ## 🚀 Installation

```bash
npm install -g devcompass@3.1.2
npm install -g devcompass@3.1.4
```

@@ -348,3 +359,3 @@

```bash
npm install --save-dev devcompass@3.1.2
npm install --save-dev devcompass@3.1.4
```

@@ -355,3 +366,3 @@

```bash
npx devcompass@3.1.2 analyze
npx devcompass@3.1.4 analyze
```

@@ -362,3 +373,3 @@

```bash
npm install -g devcompass@3.1.2
npm install -g devcompass@3.1.4
```

@@ -376,40 +387,23 @@

# Generate dependency graph with different layouts (v3.1.0+)
devcompass graph # Tree layout (default) - FIXED in v3.1.2!
devcompass graph --layout force --open # Interactive force-directed
devcompass graph --layout radial # Circular/radial view - FIXED in v3.1.2!
devcompass graph --layout conflict # Show only issues - IMPROVED in v3.1.2!
# Generate unified interactive graph (v3.1.4 - NEW!)
devcompass graph # Single file with ALL features
devcompass graph --open # Open in browser
# Export to different formats
devcompass graph --format json # JSON data
devcompass graph --format html --open # Interactive HTML
# Export formats
devcompass graph --format json # JSON data export
# Search and filter
devcompass graph --filter vulnerable # Vulnerable packages only
devcompass graph --filter outdated # Outdated packages only
devcompass graph --filter conflict # Packages with issues
# Auto-fix issues
devcompass fix # All fixes
devcompass fix --batch # Interactive selection
devcompass fix --dry-run # Preview only
# Combined options
devcompass graph --layout force --filter conflict --open
devcompass graph --layout radial --depth 2 --output radial-depth2.html
# Auto-fix issues (includes supply chain + license + quality fixes!)
devcompass fix
# Batch fix modes
devcompass fix --batch # Interactive selection
devcompass fix --batch-mode critical # Preset: critical only
devcompass fix --batch-mode high # Preset: high priority
devcompass fix --batch-mode all # Preset: all safe fixes
devcompass fix --only quality # Fix only quality issues
devcompass fix --skip updates # Skip updates category
devcompass fix --batch-mode critical # Critical only
devcompass fix --batch-mode high # High priority
devcompass fix --batch-mode all # All safe fixes
# Preview fixes without making changes
devcompass fix --dry-run
devcompass fix --batch --dry-run
# Category-specific fixes
devcompass fix --only quality # Quality only
devcompass fix --skip updates # Skip updates
# Auto-fix without confirmation (CI/CD)
devcompass fix --yes
devcompass fix --batch-mode high --yes
# Manage backups

@@ -421,134 +415,145 @@ devcompass backup list

# JSON output (for CI/CD)
devcompass analyze --json
# CI mode (exit code 1 if score < threshold)
devcompass analyze --ci
# Silent mode (no output)
devcompass analyze --silent
# CI/CD integration
devcompass analyze --json # JSON output
devcompass analyze --ci # Exit code based on score
devcompass analyze --silent # No output
```
## 📊 Graph Visualization Commands (v3.1.2)
## 🎨 Unified Graph Commands (v3.1.4)
### Layout Options
### Generate Unified Interactive Graph
#### Tree Layout (Default) - FIXED! 🆕
```bash
# Basic tree layout - now with proper horizontal spreading!
# Basic usage - generates single interactive HTML
devcompass graph
# With depth limit
devcompass graph --depth 2
# Open in browser
# Open in browser automatically
devcompass graph --open
```
#### Force-Directed Layout
# Custom output filename
devcompass graph --output my-dependencies.html --open
```bash
# Interactive physics simulation with ultimate UI
devcompass graph --layout force --open
# With custom dimensions
devcompass graph --layout force --width 1920 --height 1080
# With filters
devcompass graph --layout force --filter conflict --open
devcompass graph --width 1920 --height 1080 --open
```
#### Radial Layout - FIXED! 🆕
### What You Get (All in ONE File!)
**Single Command:**
```bash
# Circular visualization - labels now outside nodes!
devcompass graph --layout radial --open
# With depth limit
devcompass graph --layout radial --depth 3
devcompass graph --open
```
#### Conflict View - IMPROVED! 🆕
**Includes ALL of these features:**
```bash
# Card-based UI organized by severity
devcompass graph --layout conflict --open
#### **4 Layout Types** (Switchable via Buttons)
- 🌳 Tree - Hierarchical view
- 🌀 Force - Physics simulation
- ⭕ Radial - Circular view
- ⚠️ Conflict - Issues only
# Combined with filter
devcompass graph --layout conflict --filter vulnerable
```
#### **5 Filter Options** (Instant Filtering)
- All packages
- Vulnerable packages
- Outdated packages
- Deprecated packages
- Unused packages
#### **Interactive Controls**
- Depth slider (1-10, ∞)
- Live search box
- Zoom controls (In/Out/Reset/Fit/Center)
- Export (PNG/JSON)
- Fullscreen mode
- Live statistics
### Export Options
#### HTML Export (Interactive)
#### HTML Export (Default - Interactive)
```bash
# Default - interactive HTML
# Default format - single interactive HTML
devcompass graph --output my-graph.html
# Force layout
devcompass graph --layout force --output force.html --open
# With filters
devcompass graph --filter conflict --output conflicts.html
# Open immediately
devcompass graph --open
```
#### JSON Export
**File contains:**
- All 4 layouts (switchable)
- All 5 filters (instant)
- All interactive controls
- Export capabilities
- **Size: ~107 KB** (97% smaller than v3.1.3!)
#### JSON Export (Data Only)
```bash
# Complete graph data
# Export as JSON data
devcompass graph --format json --output graph.json
# With filters
devcompass graph --filter vulnerable --format json --output vulnerable.json
# JSON includes complete graph structure
# nodes, links, metadata, analysis results
```
### Filter Options
### Advanced Usage Examples
#### **Quick Security Check**
```bash
# Show only vulnerable packages
devcompass graph --filter vulnerable
# 1. Generate graph
devcompass graph --open
# Show only outdated packages
devcompass graph --filter outdated
# 2. In browser:
# - Click "Vulnerable" filter
# - See all security issues
# - Click "Export PNG" to save screenshot
```
# Show only unused packages
devcompass graph --filter unused
#### **Dependency Exploration**
```bash
# 1. Generate graph
devcompass graph --open
# Show only packages with conflicts
devcompass graph --filter conflict
# All packages (default)
devcompass graph --filter all
# 2. In browser:
# - Click "Force" layout
# - Drag nodes to explore relationships
# - Use search to find specific packages
# - Click nodes to highlight connections
# - Click "Fit to Screen" to see overview
```
### Advanced Examples
#### **Professional Documentation**
```bash
# Force layout with vulnerable packages only
devcompass graph --layout force --filter vulnerable --open
# 1. Generate graph
devcompass graph --open
# Radial layout, depth 2 - with fixed label positioning!
devcompass graph --layout radial --depth 2 --open
# 2. In browser:
# - Click "Tree" layout
# - Set depth to 2 (slider)
# - Click "Fit to Screen"
# - Click "Export PNG"
# → Add to project documentation
```
# Quick conflict check - card-based UI!
devcompass graph --layout conflict --open
#### **Complete Workflow**
```bash
# 1. Analyze project health
devcompass analyze
# Complete workflow
devcompass analyze # Analyze health (with dynamic issues!)
devcompass graph --layout conflict --open # Visualize issues
devcompass fix --batch-mode high # Fix critical issues
devcompass graph --layout force --open # Verify improvements
# 2. Visualize dependencies
devcompass graph --open
# - Explore with Force layout
# - Filter to Vulnerable packages
# - Export PNG for review
# Search and filter interactively
devcompass graph --layout force --open
# Then: Press F to search, use filter dropdowns, click nodes
# 3. Fix critical issues
devcompass fix --batch-mode high
# Fullscreen immersive experience
devcompass graph --layout force --open
# Then: Click Fullscreen button or press F11
# 4. Verify improvements
devcompass graph --open
# - Click "Conflict" layout
# - Should see fewer issues!
```
### Command Options (v3.1.2)
### Command Options (v3.1.4)

@@ -558,11 +563,10 @@ ```bash

-o, --output <file> # Output file (default: dependency-graph.html)
-f, --format <format> # Output format: html, json
-l, --layout <type> # Layout: tree, force, radial, conflict (default: tree)
-d, --depth <number> # Maximum depth to traverse (default: unlimited)
--filter <filter> # Filter: all, vulnerable, outdated, unused, conflict
-f, --format <format> # Output format: html, json (default: html)
-w, --width <number> # Graph width in pixels (default: 1200)
-h, --height <number> # Graph height in pixels (default: 800)
--open # Open in browser (HTML only)
--open # Open in browser after generation
```
**Note:** Layout and filter options are now **interactive buttons** in the HTML file, not CLI flags!
---

@@ -703,35 +707,2 @@

### Graph Layout Issues (Fixed in v3.1.2!)
All graph layout issues have been resolved:
```bash
# Verify you're on v3.1.2+
devcompass --version
# Should show: 3.1.2 or higher
# Test tree layout - should spread horizontally!
devcompass graph --layout tree --open
# Test radial layout - labels should be outside nodes!
devcompass graph --layout radial --open
# Test conflict layout - card-based UI!
devcompass graph --layout conflict --open
```
### JSON Mode (Fixed in v3.1.1!)
All JSON mode issues have been resolved:
```bash
# Verify you're on v3.1.1+
devcompass --version
# Should show: 3.1.2 or higher
# Test JSON mode
devcompass analyze --json
# Should work perfectly with no errors!
```
### Common Issues:

@@ -742,3 +713,3 @@

# Solution: Install globally
npm install -g devcompass@3.1.2
npm install -g devcompass@3.1.4
```

@@ -752,27 +723,23 @@

**Issue:** Tree layout shows vertical line (OLD BUG - FIXED!)
**Issue:** Graph controls not working
```bash
# Solution: Upgrade to v3.1.2
npm install -g devcompass@3.1.2
# Tree layout now spreads horizontally with proper D3.js tree
```
# Solution: Ensure you're on v3.1.4
devcompass --version # Should show 3.1.4
**Issue:** Radial labels overlapping nodes (OLD BUG - FIXED!)
```bash
# Solution: Upgrade to v3.1.2
npm install -g devcompass@3.1.2
# Radial labels now positioned outside nodes with smart anchoring
# Clear browser cache
# Hard refresh (Ctrl+F5 or Cmd+Shift+R)
```
**Issue:** Controls/Statistics panels overlapping (OLD BUG - FIXED!)
**Issue:** Center view centers entire page (OLD BUG - FIXED in v3.1.4!)
```bash
# Solution: Upgrade to v3.1.2
npm install -g devcompass@3.1.2
# Panels now in right-sidebar with flexbox layout
# Solution: Upgrade to v3.1.4
npm install -g devcompass@3.1.4
# Center now uses smart bounding box calculation
```
**Issue:** Force layout nodes clustered
**Issue:** Labels overlapping in tree layout (OLD BUG - FIXED in v3.1.4!)
```bash
# Solution: Use fit-to-screen button or wait for simulation to settle
# The layout spreads nodes automatically after a few seconds
# Solution: Upgrade to v3.1.4
npm install -g devcompass@3.1.4
# Labels now positioned intelligently
```

@@ -788,2 +755,9 @@

**Issue:** Graph not loading in browser
```bash
# Check browser console for errors
# Ensure D3.js CDN is accessible
# Try hard refresh (Ctrl+F5)
```
---

@@ -846,3 +820,10 @@

- [x] ~~Panel layout separation~~ ✅ **Fixed in v3.1.2!**
- [ ] Graph filters with analysis results (v3.1.3)
- [x] ~~Code cleanup and dependency optimization~~ ✅ **Added in v3.1.3!**
- [x] ~~Unified interactive graph system~~ ✅ **Added in v3.1.4!**
- [x] ~~Dynamic layout switching~~ ✅ **Added in v3.1.4!**
- [x] ~~Real-time filtering~~ ✅ **Added in v3.1.4!**
- [x] ~~Advanced zoom controls~~ ✅ **Added in v3.1.4!**
- [x] ~~Single file graph~~ ✅ **Added in v3.1.4!**
- [ ] Minimap for large graphs (v3.2.0)
- [ ] Node grouping/clustering (v3.2.0)
- [ ] Web dashboard for team health monitoring (v3.2.0)

@@ -852,2 +833,3 @@ - [ ] Monorepo support with knip (v3.2.0)

- [ ] Compare graphs before/after fixes (v3.2.0)
- [ ] Timeline view (dependency changes over time) (v3.3.0)

@@ -854,0 +836,0 @@ Want to contribute? Pick an item and open an issue! 🚀

// src/analyzers/license-risk.js
const fs = require('fs');
const path = require('path');
// v3.1.4 - Dynamic license risk analysis using npm registry
/**
* License risk levels and compatibility
*/
const LICENSE_RISKS = {
// High Risk - Restrictive/Copyleft
'GPL-1.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
'GPL-2.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
'GPL-3.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
'AGPL-1.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
'AGPL-3.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
'LGPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
'LGPL-2.1': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
'LGPL-3.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
// Medium Risk
'MPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
'MPL-1.1': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
'MPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
'EPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
'EPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
'CDDL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
// Low Risk - Permissive
'MIT': { risk: 'low', type: 'permissive', business: 'Very permissive' },
'Apache-2.0': { risk: 'low', type: 'permissive', business: 'Permissive with patent grant' },
'BSD-2-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
'BSD-3-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
'ISC': { risk: 'low', type: 'permissive', business: 'Very permissive' },
'CC0-1.0': { risk: 'low', type: 'public-domain', business: 'Public domain' },
'Unlicense': { risk: 'low', type: 'public-domain', business: 'Public domain' },
'0BSD': { risk: 'low', type: 'permissive', business: 'Very permissive' },
// Unknown/Special
'UNLICENSED': { risk: 'critical', type: 'unknown', business: 'No license - all rights reserved' },
'SEE LICENSE IN': { risk: 'high', type: 'unknown', business: 'Custom license - review required' },
'CUSTOM': { risk: 'high', type: 'unknown', business: 'Custom license - review required' }
};
const { analyzer } = require('../services');
/**
* License compatibility matrix
* Can license A be combined with license B?
*/
const LICENSE_COMPATIBILITY = {
'MIT': ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0', 'GPL-3.0', 'LGPL-2.1', 'LGPL-3.0'],
'Apache-2.0': ['Apache-2.0', 'GPL-3.0', 'LGPL-3.0'],
'GPL-2.0': ['GPL-2.0', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
'GPL-3.0': ['GPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
'LGPL-2.1': ['LGPL-2.1', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0'],
'LGPL-3.0': ['LGPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-3.0']
};
/**
* Normalize license name
*/
function normalizeLicense(license) {
if (!license) return 'UNLICENSED';
const normalized = license
.replace(/\s+/g, '-')
.replace(/[()]/g, '')
.toUpperCase();
// Handle common variations
if (normalized.includes('MIT')) return 'MIT';
if (normalized.includes('APACHE-2')) return 'Apache-2.0';
if (normalized.includes('BSD-2')) return 'BSD-2-Clause';
if (normalized.includes('BSD-3')) return 'BSD-3-Clause';
if (normalized.includes('ISC')) return 'ISC';
if (normalized.includes('GPL-2')) return 'GPL-2.0';
if (normalized.includes('GPL-3')) return 'GPL-3.0';
if (normalized.includes('LGPL-2')) return 'LGPL-2.1';
if (normalized.includes('LGPL-3')) return 'LGPL-3.0';
if (normalized.includes('AGPL')) return 'AGPL-3.0';
if (normalized.includes('MPL')) return 'MPL-2.0';
if (normalized.includes('SEE LICENSE')) return 'SEE LICENSE IN';
if (normalized === 'UNLICENSED') return 'UNLICENSED';
return license;
}
/**
* Get license risk information
*/
function getLicenseRisk(license) {
const normalized = normalizeLicense(license);
return LICENSE_RISKS[normalized] || {
risk: 'high',
type: 'unknown',
business: 'Unknown license - review required'
};
}
/**
* Load package alternatives database
*/
function loadAlternatives() {
async function analyzeLicenseRisks(projectPath, licenses = []) {
try {
const alternativesPath = path.join(__dirname, '../../data/package-alternatives.json');
if (fs.existsSync(alternativesPath)) {
return JSON.parse(fs.readFileSync(alternativesPath, 'utf8'));
// Extract package names from licenses
const packageNames = licenses.map(l => l.package);
if (packageNames.length === 0) {
return {
warnings: [],
stats: {
total: 0,
critical: 0,
high: 0,
medium: 0,
low: 0,
clean: 0
},
projectLicense: getProjectLicense(projectPath)
};
}
return null;
} catch (error) {
return null;
}
}
/**
* Find alternative package for license conflict
*/
function findAlternative(packageName, license) {
const alternatives = loadAlternatives();
if (!alternatives) return null;
const pkgName = packageName.split('@')[0];
// Check GPL alternatives
if (license.includes('GPL') && !license.includes('LGPL')) {
const gplAlts = alternatives.gpl_alternatives[pkgName];
if (gplAlts && gplAlts.alternatives.length > 0) {
return gplAlts.alternatives[0];
// Get project license
const projectLicense = getProjectLicense(projectPath);
// Analyze license conflicts dynamically
const conflicts = await analyzer.license.getLicenseConflicts(
packageNames,
projectLicense
);
const warnings = [];
// Process critical conflicts (AGPL, etc.)
for (const conflict of conflicts.critical) {
warnings.push({
package: conflict.package,
license: conflict.license,
severity: 'critical',
message: conflict.message,
risk: 'Viral copyleft - requires entire codebase to be open source',
recommendation: conflict.alternative
? `Replace with ${conflict.alternative}`
: 'Find permissive alternative',
suggestedAlternative: conflict.alternative ? {
name: conflict.alternative,
license: 'MIT' // Most alternatives are MIT
} : null,
autoFixable: conflict.alternative ? true : false
});
}
}
// Check AGPL alternatives
if (license.includes('AGPL')) {
const agplAlts = alternatives.agpl_alternatives[pkgName];
if (agplAlts && agplAlts.alternatives.length > 0) {
return agplAlts.alternatives[0];
// Process high-risk conflicts (GPL, etc.)
for (const conflict of conflicts.high) {
warnings.push({
package: conflict.package,
license: conflict.license,
severity: 'high',
message: conflict.message,
risk: 'Strong copyleft - derivative works must be GPL licensed',
recommendation: conflict.alternative
? `Replace with ${conflict.alternative}`
: 'Consider permissive alternative',
suggestedAlternative: conflict.alternative ? {
name: conflict.alternative,
license: 'MIT'
} : null,
autoFixable: conflict.alternative ? true : false
});
}
}
// Check LGPL alternatives
if (license.includes('LGPL')) {
const lgplAlts = alternatives.lgpl_alternatives[pkgName];
if (lgplAlts && lgplAlts.alternatives.length > 0) {
return lgplAlts.alternatives[0];
// Process medium-risk conflicts (LGPL, MPL, etc.)
for (const conflict of conflicts.medium) {
warnings.push({
package: conflict.package,
license: conflict.license,
severity: 'medium',
message: conflict.message,
risk: 'Weak copyleft - modifications must be shared',
recommendation: conflict.alternative
? `Consider replacing with ${conflict.alternative}`
: 'Review license compatibility',
suggestedAlternative: conflict.alternative ? {
name: conflict.alternative,
license: 'MIT'
} : null,
autoFixable: false // Medium risk - manual review needed
});
}
}
return null;
}
/**
* Check license compatibility
*/
function checkLicenseCompatibility(projectLicense, dependencyLicenses) {
const conflicts = [];
const normalized = normalizeLicense(projectLicense);
const compatible = LICENSE_COMPATIBILITY[normalized] || [];
for (const [pkg, license] of Object.entries(dependencyLicenses)) {
const depNormalized = normalizeLicense(license);
const depRisk = getLicenseRisk(license);
// Check if copyleft license conflicts with permissive project
if (depRisk.type === 'copyleft' && !compatible.includes(depNormalized)) {
const alternative = findAlternative(pkg, license);
conflicts.push({
package: pkg,
license: license,
projectLicense: projectLicense,
severity: 'high',
issue: 'License incompatibility',
message: `${license} dependency may conflict with ${projectLicense} project license`,
recommendation: alternative
? `Replace with ${alternative.name} (${alternative.license})`
: 'Review license compatibility with legal team',
autoFixable: alternative ? true : false,
autoFixAction: alternative ? 'replace' : 'review',
suggestedAlternative: alternative,
requiresConfirmation: true
// Process unknown licenses
for (const conflict of conflicts.unknown) {
warnings.push({
package: conflict.package,
license: conflict.license,
severity: 'medium',
message: conflict.message,
risk: 'Unknown license - cannot determine compatibility',
recommendation: 'Review license manually',
suggestedAlternative: null,
autoFixable: false
});
}
return {
warnings,
stats: {
total: packageNames.length,
critical: conflicts.critical.length,
high: conflicts.high.length,
medium: conflicts.medium.length,
low: 0,
clean: conflicts.clean
},
projectLicense,
conflicts: {
critical: conflicts.critical,
high: conflicts.high,
medium: conflicts.medium,
unknown: conflicts.unknown
}
};
} catch (error) {
console.error('[license-risk] Analysis failed:', error.message);
return {
warnings: [],
stats: {
total: 0,
critical: 0,
high: 0,
medium: 0,
low: 0,
clean: 0
},
projectLicense: 'MIT'
};
}
return conflicts;
}
/**
* Analyze license risks for all dependencies
*/
async function analyzeLicenseRisks(projectPath, licenses) {
const warnings = [];
const stats = {
total: 0,
critical: 0,
high: 0,
medium: 0,
low: 0,
copyleft: 0,
permissive: 0,
unknown: 0
};
// Get project license
let projectLicense = 'MIT'; // Default
function getProjectLicense(projectPath) {
try {
const projectPkgPath = path.join(projectPath, 'package.json');
if (fs.existsSync(projectPkgPath)) {
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
projectLicense = projectPkg.license || 'MIT';
const fs = require('fs');
const path = require('path');
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.license || 'MIT';
}
} catch (error) {
// Use default
// Ignore errors
}
const dependencyLicenses = {};
// Analyze each license
for (const pkg of licenses) {
stats.total++;
const risk = getLicenseRisk(pkg.license);
dependencyLicenses[pkg.package] = pkg.license;
// Count by type
if (risk.type === 'copyleft' || risk.type === 'weak-copyleft') {
stats.copyleft++;
} else if (risk.type === 'permissive' || risk.type === 'public-domain') {
stats.permissive++;
} else {
stats.unknown++;
}
// Add warnings for high-risk licenses
if (risk.risk === 'critical' || risk.risk === 'high') {
stats[risk.risk]++;
const alternative = findAlternative(pkg.package, pkg.license);
warnings.push({
package: pkg.package,
license: pkg.license,
severity: risk.risk,
type: risk.type,
issue: 'High-risk license',
message: `${pkg.license}: ${risk.business}`,
recommendation: alternative
? `Replace with ${alternative.name} (${alternative.license})`
: risk.risk === 'critical'
? 'Replace with permissive alternative immediately'
: 'Consider replacing with MIT/Apache alternative',
autoFixable: alternative ? true : false,
autoFixAction: alternative ? 'replace' : 'review',
suggestedAlternative: alternative,
requiresConfirmation: true
});
} else if (risk.risk === 'medium') {
stats.medium++;
} else {
stats.low++;
}
return 'MIT'; // Default assumption
}
function getLicenseRiskScore(licenseRiskData) {
if (!licenseRiskData || !licenseRiskData.stats) {
return 0;
}
// Check license compatibility
const conflicts = checkLicenseCompatibility(projectLicense, dependencyLicenses);
warnings.push(...conflicts);
const { critical, high, medium } = licenseRiskData.stats;
return {
warnings,
stats,
projectLicense
};
}
/**
* Get license risk score (0-10)
*/
function getLicenseRiskScore(stats) {
let score = 10;
// Penalty calculation
let penalty = 0;
penalty += critical * 3; // Critical = -3 points each
penalty += high * 2; // High = -2 points each
penalty += medium * 1; // Medium = -1 point each
score -= stats.critical * 3;
score -= stats.high * 2;
score -= stats.medium * 0.5;
return Math.max(0, score);
return Math.min(10, penalty); // Cap at 10 points penalty
}

@@ -287,8 +179,3 @@

analyzeLicenseRisks,
getLicenseRisk,
checkLicenseCompatibility,
normalizeLicense,
getLicenseRiskScore,
findAlternative,
LICENSE_RISKS
getLicenseRiskScore
};
// src/analyzers/package-quality.js
const fs = require('fs');
const path = require('path');
const https = require('https');
// v3.1.4 - Dynamic quality analysis using npm registry
/**
* Fetch package metadata from npm registry
*/
function fetchNpmPackageInfo(packageName) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'registry.npmjs.org',
path: `/${packageName}`,
method: 'GET',
headers: {
'User-Agent': 'DevCompass',
'Accept': 'application/json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(new Error('Failed to parse npm response'));
}
} else if (res.statusCode === 404) {
reject(new Error('Package not found'));
} else if (res.statusCode === 429) {
reject(new Error('Rate limit exceeded'));
} else {
reject(new Error(`npm registry returned ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
const { analyzer } = require('../services');
/**
* Calculate days since last publish
*/
function daysSincePublish(dateString) {
if (!dateString) return 0;
async function analyzePackageQuality(dependencies, githubData = []) {
const packages = Object.keys(dependencies);
try {
const publishDate = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - publishDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
} catch (error) {
return 0;
}
}
/**
* Calculate package health score (0-10)
*/
function calculateHealthScore(packageData, githubData = null) {
let score = 10;
// ✅ FIXED: Validate packageData structure
if (!packageData || typeof packageData !== 'object') {
return 5; // Default score for invalid data
}
// Get latest version info with safety checks
const latestVersion = packageData['dist-tags']?.latest;
const versionData = latestVersion ? packageData.versions?.[latestVersion] : null;
const time = latestVersion ? packageData.time?.[latestVersion] : null;
if (!versionData || !time) {
return 5; // Default score if data missing
}
// 1. Age factor (max -2 points)
const daysSince = daysSincePublish(time);
if (daysSince > 365 * 3) {
score -= 2; // 3+ years old
} else if (daysSince > 365 * 2) {
score -= 1.5; // 2-3 years old
} else if (daysSince > 365) {
score -= 1; // 1-2 years old
} else if (daysSince > 180) {
score -= 0.5; // 6-12 months old
}
// 2. Maintenance frequency (max -2 points)
const versions = Object.keys(packageData.versions || {});
const recentVersions = versions.filter(v => {
const vTime = packageData.time?.[v];
if (!vTime) return false;
return daysSincePublish(vTime) <= 365;
});
if (recentVersions.length === 0) {
score -= 2; // No updates in a year
} else if (recentVersions.length < 3) {
score -= 1; // Less than 3 updates in a year
}
// 3. GitHub activity (if available) (max -2 points)
if (githubData && typeof githubData === 'object') {
const { totalIssues = 0, last7Days = 0, last30Days = 0 } = githubData;
// High issue count with low activity is bad
if (totalIssues > 100 && last30Days < 5) {
score -= 1.5; // Many issues, low maintenance
} else if (totalIssues > 50 && last30Days < 3) {
score -= 1; // Medium issues, low maintenance
}
// Very high recent activity might indicate problems
if (last7Days > 30) {
score -= 0.5; // Unusually high activity
}
}
// 4. Dependencies count (max -1 point)
const deps = versionData.dependencies || {};
const depCount = Object.keys(deps).length;
if (depCount > 50) {
score -= 1; // Too many dependencies
} else if (depCount > 30) {
score -= 0.5;
}
// 5. Has description and repository (max -1 point)
if (!packageData.description) {
score -= 0.5;
}
if (!packageData.repository) {
score -= 0.5;
}
// 6. Deprecated packages (automatic 0)
if (versionData.deprecated) {
return 0;
}
return Math.max(0, Math.min(10, score));
}
/**
* Determine package status based on health score
*/
function getPackageStatus(score, daysSince) {
// ✅ FIXED: Add input validation
const validScore = typeof score === 'number' && !isNaN(score) ? score : 5;
const validDaysSince = typeof daysSince === 'number' && !isNaN(daysSince) ? daysSince : 0;
if (validScore === 0) {
if (packages.length === 0) {
return {
status: 'DEPRECATED',
color: 'red',
severity: 'critical',
label: 'DEPRECATED'
results: [],
stats: {
total: 0,
healthy: 0,
needsAttention: 0,
stale: 0,
abandoned: 0,
deprecated: 0
}
};
} else if (validScore < 3 || validDaysSince > 365 * 3) {
return {
status: 'ABANDONED',
color: 'red',
severity: 'critical',
label: 'ABANDONED'
};
} else if (validScore < 5 || validDaysSince > 365 * 2) {
return {
status: 'STALE',
color: 'yellow',
severity: 'high',
label: 'STALE'
};
} else if (validScore < 7) {
return {
status: 'NEEDS_ATTENTION',
color: 'yellow',
severity: 'medium',
label: 'NEEDS ATTENTION'
};
} else {
return {
status: 'HEALTHY',
color: 'green',
severity: 'low',
label: 'HEALTHY'
};
}
}
/**
* Get maintainer activity status
*/
function getMaintainerStatus(packageData) {
// ✅ FIXED: Add null checks
if (!packageData || typeof packageData !== 'object') {
return 'unknown';
}
const maintainers = packageData.maintainers || [];
const latestVersion = packageData['dist-tags']?.latest;
const time = latestVersion ? packageData.time?.[latestVersion] : null;
if (!time) {
return 'unknown';
}
const daysSince = daysSincePublish(time);
if (daysSince > 365 * 2) {
return 'inactive';
} else if (daysSince > 365) {
return 'low_activity';
} else if (daysSince > 180) {
return 'moderate_activity';
} else {
return 'active';
}
}
/**
* Format last update time in human-readable format
*/
function formatLastUpdate(daysSince) {
// ✅ FIXED: Add input validation
const validDays = typeof daysSince === 'number' && !isNaN(daysSince) ? daysSince : 0;
if (validDays < 30) {
return `${validDays} days ago`;
} else if (validDays < 365) {
const months = Math.floor(validDays / 30);
return `${months} month${months > 1 ? 's' : ''} ago`;
} else {
const years = Math.floor(validDays / 365);
return `${years} year${years > 1 ? 's' : ''} ago`;
}
}
/**
* Analyze package quality for all dependencies
*/
async function analyzePackageQuality(dependencies, githubData = []) {
// ✅ FIXED: Validate inputs
if (!dependencies || typeof dependencies !== 'object') {
return {
total: 0,
try {
// Fetch quality data for all packages dynamically
const qualityResults = await analyzer.quality.analyzeBatch(packages);
const results = [];
const stats = {
total: packages.length,
healthy: 0,

@@ -266,240 +34,106 @@ needsAttention: 0,

abandoned: 0,
deprecated: 0,
packages: []
deprecated: 0
};
}
// ✅ FIXED: Ensure githubData is always an array
const safeGithubData = Array.isArray(githubData) ? githubData : [];
// Load quality fixer for alternative suggestions
let qualityFixer = null;
try {
const QualityFixer = require('../utils/quality-fixer');
qualityFixer = new QualityFixer();
} catch (error) {
// Quality fixer not available, continue without alternatives
}
const results = [];
const stats = {
total: 0,
healthy: 0,
needsAttention: 0,
stale: 0,
abandoned: 0,
deprecated: 0
};
// Create GitHub data lookup
const githubLookup = {};
for (const data of safeGithubData) {
if (data && data.package) {
githubLookup[data.package] = data;
}
}
// Analyze each package (limit to prevent rate limiting)
const packages = Object.keys(dependencies).slice(0, 20); // Analyze first 20
for (const packageName of packages) {
// ✅ FIXED: Skip invalid package names
if (!packageName || typeof packageName !== 'string') {
continue;
}
try {
stats.total++;
for (const [name, data] of qualityResults) {
// Find matching GitHub data if available
const githubMetrics = githubData.find(g => g.package === name);
// Fetch package info from npm
const packageData = await fetchNpmPackageInfo(packageName);
// ✅ FIXED: Validate packageData before proceeding
if (!packageData || typeof packageData !== 'object') {
console.error(`Invalid package data for ${packageName}`);
continue;
}
// Calculate health score
const github = githubLookup[packageName];
const healthScore = calculateHealthScore(packageData, github);
const healthScore = calculateHealthScore(data, githubMetrics);
// Get latest version info
const latestVersion = packageData['dist-tags']?.latest;
const time = latestVersion ? packageData.time?.[latestVersion] : null;
const daysSince = time ? daysSincePublish(time) : 0;
// Determine status
const statusInfo = getPackageStatus(healthScore, daysSince);
const maintainerStatus = getMaintainerStatus(packageData);
// Count by status
if (statusInfo.status === 'HEALTHY') {
let status = 'healthy';
if (data.deprecated) {
status = 'deprecated';
stats.deprecated++;
} else if (data.abandoned) {
status = 'abandoned';
stats.abandoned++;
} else if (data.stale) {
status = 'stale';
stats.stale++;
} else if (healthScore < 7) {
status = 'needs_attention';
stats.needsAttention++;
} else {
stats.healthy++;
} else if (statusInfo.status === 'NEEDS_ATTENTION') {
stats.needsAttention++;
} else if (statusInfo.status === 'STALE') {
stats.stale++;
} else if (statusInfo.status === 'ABANDONED') {
stats.abandoned++;
} else if (statusInfo.status === 'DEPRECATED') {
stats.deprecated++;
}
// Get repository info
const repository = packageData.repository?.url || '';
const hasGithub = repository.includes('github.com');
// Check for alternative packages (only if qualityFixer is available)
let alternative = null;
if (qualityFixer) {
try {
alternative = qualityFixer.findAlternative(packageName);
} catch (error) {
// Alternative lookup failed, continue without it
}
}
// Determine if package is auto-fixable
const isAutoFixable = (
(statusInfo.status === 'ABANDONED' || statusInfo.status === 'DEPRECATED' || statusInfo.status === 'STALE') &&
alternative !== null
);
// Build result
const result = {
name: packageName,
package: packageName,
version: dependencies[packageName],
healthScore: Number(healthScore.toFixed(1)),
status: statusInfo.status,
severity: statusInfo.severity,
label: statusInfo.label,
lastPublish: time ? new Date(time).toISOString().split('T')[0] : 'unknown',
lastUpdate: formatLastUpdate(daysSince),
daysSincePublish: daysSince,
maintainerStatus: maintainerStatus,
hasRepository: !!packageData.repository,
hasGithub: hasGithub,
totalVersions: Object.keys(packageData.versions || {}).length,
description: packageData.description || '',
deprecated: latestVersion && packageData.versions?.[latestVersion]?.deprecated ? true : false,
// Auto-fix metadata
autoFixable: isAutoFixable,
autoFixAction: isAutoFixable ? 'replace' : null,
suggestedAlternative: alternative ? alternative.recommended : null,
allAlternatives: alternative ? alternative.alternatives : null,
migrationGuide: alternative ? alternative.migration_guide : null,
requiresConfirmation: true,
reason: alternative
? alternative.reason
: getQualityRecommendation({
status: statusInfo.status,
healthScore,
daysSincePublish: daysSince
}).recommendation
};
// Add GitHub metrics if available
if (github && typeof github === 'object') {
result.githubMetrics = {
totalIssues: github.totalIssues || 0,
recentIssues: github.last30Days || 0,
trend: github.trend || 'stable'
};
}
results.push(result);
// Small delay to respect npm registry rate limits
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// ✅ FIXED: Better error handling with specific error types
if (error.message === 'Rate limit exceeded') {
console.error(`Rate limit hit while analyzing ${packageName}, skipping remaining packages`);
break; // Stop analyzing more packages
} else if (error.message === 'Package not found') {
// Skip packages that don't exist
continue;
} else {
console.error(`Error analyzing ${packageName}:`, error.message);
// Continue with next package
}
// Add to results
results.push({
package: name,
name: name,
status: status,
deprecated: data.deprecated,
abandoned: data.abandoned,
stale: data.stale,
lastPublish: data.lastPublish,
daysSincePublish: data.monthsSinceUpdate ? data.monthsSinceUpdate * 30 : null,
monthsSinceUpdate: data.monthsSinceUpdate,
deprecationMessage: data.deprecationMessage,
alternative: data.alternative,
healthScore: healthScore,
githubMetrics: githubMetrics || null,
autoFixable: data.alternative ? true : false,
source: data.source
});
}
}
// ✅ FIXED: Return consistent structure with results instead of packages
return {
total: results.length,
healthy: stats.healthy,
needsAttention: stats.needsAttention,
stale: stats.stale,
abandoned: stats.abandoned,
deprecated: stats.deprecated,
results: results, // Return as 'results'
packages: results, // Also keep as 'packages' for backward compatibility
stats: stats // Also include stats object
};
}
/**
* Get quality recommendations for a package
*/
function getQualityRecommendation(packageResult) {
// ✅ FIXED: Add input validation
if (!packageResult || typeof packageResult !== 'object') {
return {
action: 'none',
message: 'No data available',
recommendation: 'Unable to provide recommendation'
results,
stats,
packages: results.filter(r => r.autoFixable) // For fix command
};
}
const {
status = 'UNKNOWN',
healthScore = 5,
daysSincePublish = 0,
maintainerStatus = 'unknown'
} = packageResult;
if (status === 'DEPRECATED') {
} catch (error) {
console.error('[package-quality] Analysis failed:', error.message);
return {
action: 'critical',
message: 'Package is deprecated',
recommendation: 'Find an actively maintained alternative immediately'
results: [],
stats: {
total: packages.length,
healthy: 0,
needsAttention: 0,
stale: 0,
abandoned: 0,
deprecated: 0
}
};
}
}
function calculateHealthScore(qualityData, githubMetrics) {
let score = 10;
if (status === 'ABANDONED') {
const years = Math.floor(daysSincePublish / 365);
return {
action: 'high',
message: `Last updated ${years} year${years > 1 ? 's' : ''} ago`,
recommendation: 'Migrate to an actively maintained alternative'
};
// Penalize based on maintenance status
if (qualityData.deprecated) {
score = 0; // Deprecated = 0
} else if (qualityData.abandoned) {
score = 2; // Abandoned = 2
} else if (qualityData.stale) {
score = 5; // Stale = 5
} else if (qualityData.monthsSinceUpdate) {
// Gradual penalty for age
const months = qualityData.monthsSinceUpdate;
if (months > 12) score -= 2;
else if (months > 6) score -= 1;
}
if (status === 'STALE') {
const months = Math.floor(daysSincePublish / 30);
return {
action: 'medium',
message: `Not updated in ${months} month${months > 1 ? 's' : ''} ago`,
recommendation: 'Consider finding a more actively maintained package'
};
// Adjust based on GitHub metrics if available
if (githubMetrics) {
const { openIssues, totalIssues, starsCount } = githubMetrics;
// Penalize high issue ratio
if (totalIssues > 0) {
const issueRatio = openIssues / totalIssues;
if (issueRatio > 0.8) score -= 1;
if (issueRatio > 0.9) score -= 1;
}
// Bonus for popular packages
if (starsCount > 10000) score += 0.5;
if (starsCount > 50000) score += 0.5;
}
if (status === 'NEEDS_ATTENTION') {
return {
action: 'low',
message: `Health score: ${healthScore}/10`,
recommendation: 'Monitor for updates and potential alternatives'
};
}
return {
action: 'none',
message: 'Package is healthy',
recommendation: 'No action needed'
};
return Math.max(0, Math.min(10, Math.round(score * 10) / 10));
}

@@ -509,7 +143,3 @@

analyzePackageQuality,
calculateHealthScore,
getPackageStatus,
getMaintainerStatus,
getQualityRecommendation,
fetchNpmPackageInfo
calculateHealthScore
};
// src/analyzers/supply-chain.js
const fs = require('fs');
const path = require('path');
// v3.1.4 - Dynamic supply chain security analysis
// Known malicious packages database
const MALICIOUS_PACKAGES = [
'epress', 'expres', 'expresss', 'requst', 'requist', 'lodas', 'loadash',
'axois', 'axioss', 'webpak', 'webback', 'reacts', 'mongose', 'mongosse'
];
const { analyzer } = require('../services');
// Popular packages to check for typosquatting
const POPULAR_PACKAGES = [
'express', 'request', 'lodash', 'axios', 'webpack', 'react', 'vue', 'angular',
'next', 'typescript', 'eslint', 'prettier', 'jest', 'mocha', 'chai'
];
// Legitimate packages that should NOT be flagged (expanded whitelist)
const WHITELIST = [
'chalk', 'ora', 'yargs', 'commander', 'semver', 'dotenv', 'debug', 'uuid',
'mime', 'qs', 'joi', 'bcrypt', 'passport', 'multer', 'cors', 'helmet',
'morgan', 'winston', 'pino', 'bunyan', 'nodemon', 'pm2', 'async', 'bluebird',
'ramda', 'underscore', 'moment', 'dayjs', 'date-fns', 'luxon', 'validator',
'sanitize-html', 'dompurify', 'cheerio', 'jsdom', 'puppeteer', 'playwright'
];
// Suspicious install script patterns
const SUSPICIOUS_PATTERNS = [
'curl', 'wget', 'http://', 'https://', 'eval', 'exec', 'child_process',
'/bin/sh', '/bin/bash', 'powershell', 'bitcoin', 'mining', 'keylogger', 'backdoor'
];
async function analyzeSupplyChain(projectPath, dependencies) {
const warnings = [];
/**
* Analyze supply chain security for project dependencies
*/
async function analyzeSupplyChain(projectPath, dependencies = {}) {
const packages = Object.keys(dependencies);
if (packages.length === 0) {
return {
warnings: [],
total: 0,
summary: {
typosquatting: 0,
suspiciousScripts: 0,
vulnerabilities: 0
}
};
}
try {
const packageJsonPath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return { warnings: [], total: 0, summary: { malicious: 0, typosquatting: 0, suspiciousScripts: 0 } };
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
for (const [pkgName, version] of Object.entries(allDeps)) {
// Check for malicious packages
if (MALICIOUS_PACKAGES.includes(pkgName)) {
const warnings = [];
// 1. Check for typosquatting (dynamic)
for (const pkg of packages) {
const typosquat = analyzer.security.checkTyposquatting(pkg);
if (typosquat) {
warnings.push({
package: pkgName,
type: 'malicious',
severity: 'critical',
description: 'Known malicious package detected',
reason: 'This package is known to be malicious',
action: 'Remove immediately',
autoFixable: true,
autoFixAction: 'remove',
requiresConfirmation: false
});
continue;
}
// Check for typosquatting
const typosquatCheck = checkTyposquatting(pkgName);
if (typosquatCheck) {
warnings.push({
package: pkgName,
package: pkg,
type: 'typosquatting',
severity: 'high',
description: `Similar to: ${typosquatCheck.original} (official package)`,
reason: `Potential malicious package - typo of ${typosquatCheck.original}`,
action: `Remove ${pkgName} and install ${typosquatCheck.original}`,
replacement: typosquatCheck.original,
autoFixable: true,
autoFixAction: 'replace',
requiresConfirmation: false
description: typosquat.warning,
correctPackage: typosquat.similarTo,
distance: typosquat.distance,
reason: `Package name is ${typosquat.distance} character(s) different from popular package "${typosquat.similarTo}"`,
risk: 'Possible typosquatting attack - malicious package mimicking popular library',
action: 'remove',
autoFixable: false // Too risky to auto-remove
});
continue;
}
// Check for suspicious install scripts
const scriptCheck = await checkInstallScripts(projectPath, pkgName);
if (scriptCheck) {
}
// 2. Run npm audit for vulnerabilities (dynamic)
let auditResults = { vulnerabilities: [], summary: { total: 0 } };
try {
auditResults = analyzer.security.runNpmAudit(projectPath);
} catch (error) {
// npm audit may fail in some environments, continue anyway
}
// Add high/critical vulnerabilities to warnings
for (const vuln of auditResults.vulnerabilities) {
const severity = (vuln.severity || 'moderate').toLowerCase();
if (severity === 'critical' || severity === 'high') {
warnings.push({
package: pkgName,
type: 'suspicious_script',
severity: 'medium',
description: `Install script contains suspicious patterns`,
reason: `Suspicious install script detected`,
action: 'Review the install script before deployment',
script: scriptCheck.script,
patterns: scriptCheck.patterns,
autoFixable: true,
autoFixAction: 'review',
requiresConfirmation: true
package: vuln.package,
type: 'vulnerability',
severity: severity,
description: vuln.title,
reason: vuln.title,
risk: `${severity.toUpperCase()} security vulnerability`,
action: 'update',
url: vuln.url,
range: vuln.range,
autoFixable: true // npm audit fix can handle this
});
}
}
// Calculate summary
// Summary statistics
const summary = {
malicious: warnings.filter(w => w.type === 'malicious').length,
typosquatting: warnings.filter(w => w.type === 'typosquatting').length,
suspiciousScripts: warnings.filter(w => w.type === 'suspicious_script').length
suspiciousScripts: warnings.filter(w => w.type === 'install_script').length,
vulnerabilities: auditResults.summary.total,
critical: auditResults.summary.critical || 0,
high: auditResults.summary.high || 0
};
return {
warnings,
total: warnings.length,
summary
summary,
audit: auditResults
};
} catch (error) {
console.error('Supply chain analysis error:', error.message);
return { warnings: [], total: 0, summary: { malicious: 0, typosquatting: 0, suspiciousScripts: 0 } };
console.error('[supply-chain] Analysis failed:', error.message);
return {
warnings: [],
total: 0,
summary: {
typosquatting: 0,
suspiciousScripts: 0,
vulnerabilities: 0
}
};
}
}
/**
* Check for typosquatting using Levenshtein distance
*/
function checkTyposquatting(packageName) {
// Skip if package is in whitelist
if (WHITELIST.includes(packageName)) {
return null;
}
for (const popular of POPULAR_PACKAGES) {
// Skip if both are in whitelist
if (WHITELIST.includes(popular)) {
continue;
}
const distance = levenshteinDistance(packageName, popular);
// Flag if 1-2 character difference
if (distance > 0 && distance <= 2 && packageName !== popular) {
return { original: popular, distance };
}
}
return null;
function addToWhitelist(packageName) {
analyzer.security.addToWhitelist(packageName);
}
/**
* Calculate Levenshtein distance between two strings
*/
function levenshteinDistance(str1, str2) {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[str2.length][str1.length];
function removeFromWhitelist(packageName) {
analyzer.security.removeFromWhitelist(packageName);
}
/**
* Check package for suspicious install scripts
*/
async function checkInstallScripts(projectPath, packageName) {
try {
const pkgPath = path.join(projectPath, 'node_modules', packageName, 'package.json');
if (!fs.existsSync(pkgPath)) {
return null;
}
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const scripts = pkgJson.scripts || {};
const suspiciousScripts = ['preinstall', 'install', 'postinstall'];
for (const scriptName of suspiciousScripts) {
if (scripts[scriptName]) {
const scriptContent = scripts[scriptName];
const foundPatterns = SUSPICIOUS_PATTERNS.filter(pattern =>
scriptContent.toLowerCase().includes(pattern.toLowerCase())
);
if (foundPatterns.length > 0) {
return {
script: scriptName,
content: scriptContent,
patterns: foundPatterns
};
}
}
}
return null;
} catch (error) {
return null;
}
function isWhitelisted(packageName) {
return analyzer.security.isWhitelisted(packageName);
}
module.exports = { analyzeSupplyChain };
module.exports = {
analyzeSupplyChain,
addToWhitelist,
removeFromWhitelist,
isWhitelisted
};
// src/commands/graph.js
// v3.1.3 - Fixed analyzeProject import and graph filter enrichment
// v3.1.4 - Unified graph with dynamic layout/filter controls
const fs = require('fs');

@@ -11,3 +12,3 @@ const path = require('path');

/**
* Graph command
* Graph command - Generate unified interactive dependency visualization
*/

@@ -29,18 +30,12 @@ async function graphCommand(options) {

// Validate layout option
const validLayouts = ['tree', 'force', 'radial', 'conflict'];
if (!validLayouts.includes(layout)) {
console.error(chalk.red(`✗ Invalid layout: ${layout}`));
console.log(chalk.gray(` Valid options: ${validLayouts.join(', ')}`));
return;
// For JSON export, use traditional single-layout approach
const isJSONExport = format === 'json' || output.endsWith('.json');
if (!isJSONExport) {
console.log(chalk.cyan('💡 Generating unified interactive graph with:'));
console.log(chalk.gray(' • All layouts (Tree, Force, Radial, Conflict)'));
console.log(chalk.gray(' • All filters (Vulnerable, Outdated, Unused, Deprecated)'));
console.log(chalk.gray(' • Dynamic controls (no page reload needed)\n'));
}
// Validate filter option
const validFilters = ['all', 'vulnerable', 'outdated', 'unused', 'conflict', 'deprecated'];
if (!validFilters.includes(filter)) {
console.error(chalk.red(`✗ Invalid filter: ${filter}`));
console.log(chalk.gray(` Valid options: ${validFilters.join(', ')}`));
return;
}
const spinner = ora('Generating dependency graph...').start();

@@ -56,7 +51,5 @@

// v3.1.3 - FIXED: Import analyzeProject correctly
try {
const analyzeModule = require('./analyze');
// Check if analyzeProject exists (v3.1.3+)
if (typeof analyzeModule.analyzeProject === 'function') {

@@ -69,23 +62,6 @@ spinner.text = 'Running analysis for graph enrichment...';

analysisLoaded = true;
// Debug: log what we got
if (process.env.DEBUG) {
console.log('\n[graph] Analysis results loaded:');
console.log(' - Security vulnerabilities:', analysisResults.security?.vulnerabilities?.length || 0);
console.log(' - Outdated packages:', analysisResults.outdatedPackages?.length || 0);
console.log(' - Unused dependencies:', analysisResults.unusedDependencies?.length || 0);
console.log(' - Ecosystem alerts:', analysisResults.ecosystemAlerts?.length || 0);
}
}
} else {
// Fallback for older versions without analyzeProject
if (process.env.DEBUG) {
console.log('[graph] analyzeProject not available, skipping enrichment');
}
}
} catch (error) {
// Analysis not available, continue without enrichment
if (process.env.DEBUG) {
console.log('[graph] Analysis failed:', error.message);
}
}

@@ -95,7 +71,6 @@

// v3.1.3 - generator.generate() is async, must use await
const graphData = await generator.generate({
maxDepth: depth !== Infinity ? parseInt(depth) : Infinity,
filter,
enrichWithIssues: false // Dynamic npm fetching disabled for speed
enrichWithIssues: false
});

@@ -108,11 +83,16 @@

// v3.1.3 - Show enrichment status in spinner
if (analysisLoaded) {
const issueCount = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length;
spinner.succeed(`Generated graph with ${graphData.nodes.length} nodes (${issueCount} with issues)`);
} else {
spinner.succeed(`Generated graph with ${graphData.nodes.length} nodes`);
}
// Add metadata for unified HTML
graphData.metadata = graphData.metadata || {};
graphData.metadata.availableLayouts = ['tree', 'force', 'radial', 'conflict'];
graphData.metadata.availableFilters = ['all', 'vulnerable', 'outdated', 'unused', 'deprecated', 'conflict'];
graphData.metadata.defaultLayout = layout;
graphData.metadata.defaultFilter = filter;
graphData.metadata.defaultDepth = depth !== Infinity ? depth : 10;
graphData.metadata.width = width;
graphData.metadata.height = height;
// Detect format from output filename if not specified
const issueCount = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length;
spinner.succeed(`Generated graph with ${chalk.cyan(graphData.nodes.length)} nodes${issueCount > 0 ? ` (${chalk.yellow(issueCount)} with issues)` : ''}`);
// Detect format
let detectedFormat = format;

@@ -130,3 +110,4 @@ if (!detectedFormat) {

height: parseInt(height),
filter
filter,
unified: detectedFormat === 'html' // Enable unified mode for HTML
});

@@ -147,65 +128,6 @@

// Display summary
console.log('\n' + chalk.gray('─'.repeat(70)));
console.log(chalk.bold('\n📈 GRAPH SUMMARY\n'));
console.log(` ${chalk.gray('Format:')} ${result.format.toUpperCase()}`);
console.log(` ${chalk.gray('Layout:')} ${layout}`);
console.log(` ${chalk.gray('Total Nodes:')} ${graphData.nodes.length}`);
console.log(` ${chalk.gray('Total Links:')} ${graphData.links.length}`);
console.log(` ${chalk.gray('Max Depth:')} ${graphData.metadata.maxDepth}`);
console.log(` ${chalk.gray('File Size:')} ${result.fileSize || getFileSize(result.path)}`);
if (filter !== 'all') {
console.log(` ${chalk.gray('Filter:')} ${filter}`);
console.log(` ${chalk.gray('Filtered:')} ${graphData.metadata.visibleDependencies} / ${graphData.metadata.totalDependencies}`);
}
// v3.1.3 - Show enrichment status with detailed breakdown
if (analysisLoaded) {
const issueNodes = graphData.nodes.filter(n => n.issues && n.issues.length > 0).length;
const vulnNodes = graphData.nodes.filter(n => n.isVulnerable).length;
const outdatedNodes = graphData.nodes.filter(n => n.isOutdated).length;
const unusedNodes = graphData.nodes.filter(n => n.isUnused).length;
const deprecatedNodes = graphData.nodes.filter(n => n.isDeprecated).length;
console.log(` ${chalk.gray('Enriched:')} ${chalk.green('✓')} Analysis data applied`);
if (issueNodes > 0) {
console.log(` ${chalk.gray('With Issues:')} ${issueNodes} packages`);
if (vulnNodes > 0) console.log(` ${chalk.gray('Vulnerable:')} ${chalk.red(vulnNodes)}`);
if (outdatedNodes > 0) console.log(` ${chalk.gray('Outdated:')} ${chalk.yellow(outdatedNodes)}`);
if (unusedNodes > 0) console.log(` ${chalk.gray('Unused:')} ${chalk.blue(unusedNodes)}`);
if (deprecatedNodes > 0) console.log(` ${chalk.gray('Deprecated:')} ${chalk.magenta(deprecatedNodes)}`);
} else {
console.log(` ${chalk.gray('With Issues:')} ${chalk.green('0 (healthy project)')}`);
}
} else {
console.log(` ${chalk.gray('Enriched:')} ${chalk.yellow('✗')} Run 'devcompass analyze' first for full data`);
}
if (result.method) {
console.log(` ${chalk.gray('Export Method:')} ${result.method}`);
}
console.log('\n' + chalk.gray('─'.repeat(70)));
displaySummary(graphData, result, analysisLoaded, options);
// Show format-specific tips
if (result.format === 'html') {
console.log(chalk.cyan('\n💡 TIPS:'));
console.log(` • ${chalk.gray('Zoom:')} Mouse wheel or pinch`);
console.log(` • ${chalk.gray('Pan:')} Click and drag`);
console.log(` • ${chalk.gray('Details:')} Hover over nodes`);
if (layout === 'force') {
console.log(` • ${chalk.gray('Move nodes:')} Drag individual nodes`);
console.log(` • ${chalk.gray('Reset:')} Use "Reset Layout" button`);
}
if (options.includeSearch !== false) {
console.log(` • ${chalk.gray('Search:')} Use search panel on left`);
console.log(` • ${chalk.gray('Filter:')} Apply filters to focus on issues`);
}
}
// Open in browser if requested
if (result.format === 'html' && shouldOpen) {
if (result.format === 'HTML' && shouldOpen) {
try {

@@ -221,38 +143,2 @@ console.log(chalk.cyan('\n🌐 Opening in browser...'));

// Show next steps
console.log(chalk.bold('\n📋 NEXT STEPS:\n'));
if (result.format === 'html') {
console.log(` 1. Open in browser: ${chalk.cyan(`file://${path.resolve(result.path)}`)}`);
console.log(` 2. Explore dependencies interactively`);
console.log(` 3. Use filters to identify issues`);
}
// v3.1.3 - Show filter suggestions based on analysis
if (filter === 'all' && analysisLoaded) {
const vulnCount = graphData.nodes.filter(n => n.isVulnerable).length;
const outdatedCount = graphData.nodes.filter(n => n.isOutdated).length;
const unusedCount = graphData.nodes.filter(n => n.isUnused).length;
if (vulnCount > 0) {
console.log(` • Try: ${chalk.cyan(`devcompass graph --filter vulnerable`)} to see ${chalk.red(vulnCount)} vulnerable packages`);
}
if (outdatedCount > 0) {
console.log(` • Try: ${chalk.cyan(`devcompass graph --filter outdated`)} to see ${chalk.yellow(outdatedCount)} outdated packages`);
}
if (unusedCount > 0) {
console.log(` • Try: ${chalk.cyan(`devcompass graph --filter unused`)} to see ${chalk.blue(unusedCount)} unused packages`);
}
if (vulnCount === 0 && outdatedCount === 0 && unusedCount === 0) {
console.log(` • ${chalk.green('✓')} Your project looks healthy! No issues detected.`);
}
} else if (filter === 'all') {
console.log(` • Try: ${chalk.cyan(`devcompass graph --filter conflict`)} to see only problematic packages`);
}
if (layout === 'tree') {
console.log(` • Try: ${chalk.cyan(`devcompass graph --layout force`)} for interactive physics layout`);
console.log(` • Try: ${chalk.cyan(`devcompass graph --layout radial`)} for circular visualization`);
}
console.log(chalk.green('\n✓ Graph generation complete!\n'));

@@ -262,10 +148,2 @@

exportSpinner.fail(`Export failed: ${result.error}`);
// Show helpful error messages
if (result.error.includes('puppeteer') || result.error.includes('canvas')) {
console.log(chalk.yellow('\n💡 TIP: For PNG export, install one of:'));
console.log(chalk.gray(' npm install -g puppeteer (recommended, ~300MB)'));
console.log(chalk.gray(' npm install -g canvas (lighter, ~50MB)'));
console.log(chalk.gray('\nOr use HTML/SVG formats which require no additional dependencies.'));
}
}

@@ -280,12 +158,102 @@

}
}
}
/**
* Display comprehensive summary
*/
function displaySummary(graphData, result, analysisLoaded, options) {
const stats = {
totalNodes: graphData.nodes.length,
totalLinks: graphData.links.length,
maxDepth: graphData.metadata.maxDepth,
withIssues: graphData.nodes.filter(n => n.issues && n.issues.length > 0).length,
vulnerable: graphData.nodes.filter(n => n.isVulnerable).length,
deprecated: graphData.nodes.filter(n => n.isDeprecated).length,
outdated: graphData.nodes.filter(n => n.isOutdated).length,
unused: graphData.nodes.filter(n => n.isUnused).length
};
console.log('\n' + chalk.gray('─'.repeat(70)));
console.log(chalk.bold('\n📈 GRAPH SUMMARY\n'));
console.log(` ${chalk.gray('Format:')} ${result.format}`);
if (result.format === 'HTML') {
console.log(` ${chalk.gray('Mode:')} ${chalk.green('✓ Unified Interactive')}`);
console.log(` ${chalk.gray('Layouts:')} Tree, Force, Radial, Conflict ${chalk.gray('(switchable)')}`);
console.log(` ${chalk.gray('Filters:')} All, Vulnerable, Outdated, Unused, Deprecated ${chalk.gray('(switchable)')}`);
} else {
console.log(` ${chalk.gray('Layout:')} ${options.layout || 'tree'}`);
}
console.log(` ${chalk.gray('Total Nodes:')} ${stats.totalNodes}`);
console.log(` ${chalk.gray('Total Links:')} ${stats.totalLinks}`);
console.log(` ${chalk.gray('Max Depth:')} ${stats.maxDepth}`);
console.log(` ${chalk.gray('File Size:')} ${result.fileSize || getFileSize(result.path)}`);
if (analysisLoaded) {
console.log(` ${chalk.gray('Enriched:')} ${chalk.green('✓ Analysis data applied')}`);
// Show troubleshooting tips
console.log(chalk.yellow('\n💡 TROUBLESHOOTING:'));
console.log(chalk.gray(' • Ensure package.json exists in the project directory'));
console.log(chalk.gray(' • Run npm install to generate package-lock.json'));
console.log(chalk.gray(' • Check file permissions for output directory'));
console.log(chalk.gray(` • Try: ${chalk.cyan('devcompass graph --format json')} for simpler output`));
if (stats.withIssues > 0) {
console.log(` ${chalk.gray('With Issues:')} ${stats.withIssues} packages`);
if (stats.vulnerable > 0) console.log(` ${chalk.gray('Vulnerable:')} ${chalk.red(stats.vulnerable)}`);
if (stats.deprecated > 0) console.log(` ${chalk.gray('Deprecated:')} ${chalk.magenta(stats.deprecated)}`);
if (stats.outdated > 0) console.log(` ${chalk.gray('Outdated:')} ${chalk.yellow(stats.outdated)}`);
if (stats.unused > 0) console.log(` ${chalk.gray('Unused:')} ${chalk.blue(stats.unused)}`);
}
}
console.log('\n' + chalk.gray('─'.repeat(70)));
if (result.format === 'HTML') {
console.log(chalk.bold('\n📋 INTERACTIVE CONTROLS\n'));
console.log(' Open the HTML file to access:');
console.log(` ${chalk.cyan('•')} Layout switcher (Tree/Force/Radial/Conflict)`);
console.log(` ${chalk.cyan('•')} Filter controls (Vulnerable/Outdated/Unused/Deprecated)`);
console.log(` ${chalk.cyan('•')} Depth slider (1-10)`);
console.log(` ${chalk.cyan('•')} Search functionality`);
console.log(` ${chalk.cyan('•')} Zoom & pan controls`);
console.log(` ${chalk.cyan('•')} Real-time updates (no page reload)`);
console.log(chalk.bold('\n💡 USAGE TIPS\n'));
console.log(` ${chalk.gray('Zoom:')} Mouse wheel or pinch`);
console.log(` ${chalk.gray('Pan:')} Click and drag background`);
console.log(` ${chalk.gray('Move nodes:')} Drag nodes (Force layout)`);
console.log(` ${chalk.gray('Node details:')} Hover over nodes`);
console.log(` ${chalk.gray('Search:')} Type package name in search box`);
console.log(chalk.cyan('\n──────────────────────────────────────────────────────────────────────\n'));
}
// Suggestions
if (stats.vulnerable > 0 || stats.deprecated > 0 || stats.outdated > 0) {
console.log(chalk.bold('📋 SUGGESTIONS\n'));
if (stats.vulnerable > 0) {
console.log(chalk.yellow(` ⚠️ ${stats.vulnerable} vulnerable package(s) detected`));
console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Vulnerable filter')} in the graph UI`);
console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('devcompass fix')} to resolve\n`);
}
if (stats.deprecated > 0) {
console.log(chalk.yellow(` ⚠️ ${stats.deprecated} deprecated package(s) found`));
console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Deprecated filter')} in the graph UI`);
console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('devcompass fix --only quality')}\n`);
}
if (stats.outdated > 0) {
console.log(chalk.yellow(` ⚠️ ${stats.outdated} outdated package(s) found`));
console.log(` ${chalk.gray('→')} Use ${chalk.cyan('Outdated filter')} in the graph UI`);
console.log(` ${chalk.gray('→')} Run: ${chalk.cyan('npm update')}\n`);
}
} else if (analysisLoaded) {
console.log(chalk.bold('📋 STATUS\n'));
console.log(` ${chalk.green('✓')} Your project looks healthy! No critical issues detected.\n`);
}
}
/**
* Get file size helper
*/
function getFileSize(filePath) {

@@ -292,0 +260,0 @@ try {

// src/graph/exporter.js
// Graph exporter - generates HTML/JSON output from graph data
// v3.1.2 - Fixed: module.exports = GraphExporter (not object)
// v3.1.4 - Unified graph exporter with dynamic controls

@@ -8,3 +7,3 @@ const fs = require('fs');

// Import layout generators with safe fallbacks
// Import layout generators
let generateTreeLayoutHTML, generateRadialLayoutHTML, generateForceLayoutHTML, generateConflictLayoutHTML;

@@ -41,236 +40,3 @@

/**
* Fallback HTML generator if layout file fails to load
*/
function generateFallbackHTML(graphData, options, layoutType) {
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
const projectName = options.projectName || 'Project';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--border-color: #475569;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
}
.header {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 41, 59, 0.95);
padding: 16px 32px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
z-index: 100;
text-align: center;
}
.header h1 { font-size: 20px; margin-bottom: 4px; }
.header p { font-size: 12px; color: var(--text-secondary); }
svg { width: 100vw; height: 100vh; cursor: grab; }
svg:active { cursor: grabbing; }
.node circle {
fill: var(--accent-blue);
stroke: var(--bg-primary);
stroke-width: 2px;
cursor: pointer;
transition: all 0.2s;
}
.node circle:hover {
stroke: var(--text-primary);
filter: drop-shadow(0 0 8px var(--accent-cyan));
}
.node.root circle { fill: #60a5fa; r: 18; }
.node text {
fill: var(--text-secondary);
font-size: 10px;
pointer-events: none;
}
.link {
stroke: var(--border-color);
stroke-width: 1.5px;
stroke-opacity: 0.5;
}
.controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 100;
}
.controls button {
width: 44px;
height: 44px;
background: rgba(30, 41, 59, 0.95);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-primary);
font-size: 20px;
cursor: pointer;
backdrop-filter: blur(12px);
}
.controls button:hover {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.legend {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(30, 41, 59, 0.95);
padding: 16px;
border-radius: 16px;
border: 1px solid var(--border-color);
backdrop-filter: blur(12px);
z-index: 100;
}
.legend-title { font-size: 13px; font-weight: 700; margin-bottom: 10px; }
.legend-item { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 11px; color: var(--text-secondary); }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; }
.tooltip {
position: absolute;
padding: 12px 16px;
background: rgba(15, 23, 42, 0.98);
border: 1px solid var(--border-color);
border-radius: 10px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 1000;
backdrop-filter: blur(12px);
}
.tooltip.visible { opacity: 1; }
.tooltip-title { font-weight: 700; color: var(--accent-cyan); margin-bottom: 6px; }
</style>
</head>
<body>
<div class="header">
<h1>🧭 DevCompass - ${layoutType} Layout</h1>
<p>${nodes.length} packages • ${links.length} dependencies</p>
</div>
<svg id="graph"></svg>
<div class="controls">
<button onclick="zoomIn()" title="Zoom In">+</button>
<button onclick="zoomOut()" title="Zoom Out">−</button>
<button onclick="resetZoom()" title="Reset">⟲</button>
</div>
<div class="legend">
<div class="legend-title">🎨 Health Status</div>
<div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div>Healthy (7-10)</div>
<div class="legend-item"><div class="legend-dot" style="background: #eab308;"></div>Caution (5-7)</div>
<div class="legend-item"><div class="legend-dot" style="background: #f97316;"></div>Warning (3-5)</div>
<div class="legend-item"><div class="legend-dot" style="background: #ef4444;"></div>Critical (<3)</div>
<div class="legend-item"><div class="legend-dot" style="background: #60a5fa;"></div>Root Package</div>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
const graphData = ${JSON.stringify({ nodes, links })};
const width = window.innerWidth;
const height = window.innerHeight;
function getColor(node) {
if (node.type === 'root') return '#60a5fa';
const score = node.healthScore || 8;
if (score >= 7) return '#10b981';
if (score >= 5) return '#eab308';
if (score >= 3) return '#f97316';
return '#ef4444';
}
function getRadius(node) {
if (node.type === 'root') return 18;
if (node.depth === 1) return 10;
return 6;
}
const svg = d3.select("#graph").attr("width", width).attr("height", height);
const g = svg.append("g");
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on("zoom", (e) => g.attr("transform", e.transform));
svg.call(zoom);
const simulation = d3.forceSimulation(graphData.nodes)
.force("link", d3.forceLink(graphData.links).id(d => d.id).distance(60))
.force("charge", d3.forceManyBody().strength(-150))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => getRadius(d) + 5));
const link = g.append("g").selectAll("line")
.data(graphData.links).join("line").attr("class", "link");
const node = g.append("g").selectAll("g")
.data(graphData.nodes).join("g")
.attr("class", d => "node" + (d.type === "root" ? " root" : ""))
.call(d3.drag()
.on("start", (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
.on("drag", (e, d) => { d.fx = e.x; d.fy = e.y; })
.on("end", (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }))
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip);
node.append("circle").attr("r", d => getRadius(d)).attr("fill", d => getColor(d));
node.append("text").attr("dy", d => getRadius(d) + 12).attr("text-anchor", "middle")
.text(d => (d.name || d.id).substring(0, 20));
simulation.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
node.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
});
function showTooltip(event, d) {
const tooltip = document.getElementById('tooltip');
tooltip.innerHTML = '<div class="tooltip-title">' + (d.name || d.id) + '</div>' +
'Version: ' + (d.version || 'N/A') + '<br>' +
'Health: ' + (d.healthScore || 8) + '/10<br>' +
'Depth: ' + (d.depth || 0);
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
tooltip.classList.add('visible');
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
window.zoomIn = () => svg.transition().call(zoom.scaleBy, 1.3);
window.zoomOut = () => svg.transition().call(zoom.scaleBy, 0.7);
window.resetZoom = () => svg.transition().call(zoom.transform, d3.zoomIdentity);
</script>
</body>
</html>`;
}
/**
* GraphExporter - Exports graph data to various formats
* This class IS the module.exports (not wrapped in object)
*/

@@ -287,2 +53,3 @@ class GraphExporter {

filter: options.filter || 'all',
unified: options.unified !== false, // v3.1.4 - Enable unified mode by default
...options

@@ -324,2 +91,3 @@ };

n.type === 'root' ||
n.isVulnerable === true ||
(Array.isArray(n.issues) && n.issues.some(i =>

@@ -347,2 +115,10 @@ i.type === 'security' || i.type === 'vulnerability'

case 'deprecated':
filteredNodes = nodes.filter(n =>
n.type === 'root' ||
n.isDeprecated === true ||
(Array.isArray(n.issues) && n.issues.some(i => i.type === 'deprecated'))
);
break;
case 'conflict':

@@ -375,5 +151,26 @@ filteredNodes = nodes.filter(n =>

/**
* Generate HTML content based on layout type
* Generate unified HTML with dynamic controls (v3.1.4)
*/
generateHTML() {
generateUnifiedHTML() {
const templatePath = path.join(__dirname, 'template.html');
try {
let template = fs.readFileSync(templatePath, 'utf8');
// Inject graph data - replace the placeholder
template = template.replace('{{GRAPH_DATA}}', JSON.stringify(this.graphData, null, 2));
return template;
} catch (error) {
console.error('Failed to load unified template:', error.message);
console.error('Template path:', templatePath);
// Fallback to traditional layout
return this.generateTraditionalHTML();
}
}
/**
* Generate traditional HTML (single layout)
*/
generateTraditionalHTML() {
const filteredData = this.applyFilter();

@@ -413,7 +210,66 @@ const layout = (this.options.layout || 'tree').toLowerCase();

// Fallback to built-in generator
return generateFallbackHTML(filteredData, this.options, layout);
// Final fallback
return this.generateFallbackHTML(filteredData, layout);
}
/**
* Generate HTML content
*/
generateHTML() {
// v3.1.4 - Use unified template by default
if (this.options.unified) {
return this.generateUnifiedHTML();
}
// Fallback to traditional single-layout mode
return this.generateTraditionalHTML();
}
/**
* Fallback HTML generator
*/
generateFallbackHTML(graphData, layoutType) {
const nodes = Array.isArray(graphData.nodes) ? graphData.nodes : [];
const links = Array.isArray(graphData.links) ? graphData.links : [];
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevCompass - Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; margin: 0; }
svg { width: 100vw; height: 100vh; }
.node circle { fill: #3b82f6; stroke: #fff; stroke-width: 2px; }
.node text { fill: #94a3b8; font-size: 10px; }
.link { stroke: #475569; stroke-width: 1.5px; stroke-opacity: 0.6; }
</style>
</head>
<body>
<svg id="graph"></svg>
<script>
const data = ${JSON.stringify({ nodes, links })};
const svg = d3.select("#graph");
const width = window.innerWidth;
const height = window.innerHeight;
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width/2, height/2));
const link = svg.append("g").selectAll("line").data(data.links).join("line").attr("class", "link");
const node = svg.append("g").selectAll("g").data(data.nodes).join("g").attr("class", "node");
node.append("circle").attr("r", 8);
node.append("text").attr("dy", -12).text(d => d.name);
simulation.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y);
node.attr("transform", d => \`translate(\${d.x},\${d.y})\`);
});
</script>
</body>
</html>`;
}
/**
* Generate JSON output

@@ -442,3 +298,3 @@ */

/**
* Export to file (main method called by graph.js)
* Export to file (main method)
*/

@@ -450,3 +306,3 @@ export(outputPath) {

/**
* Export to file
* Export to file implementation
*/

@@ -510,6 +366,2 @@ exportToFile(outputPath) {

// ============================================================================
// CRITICAL: Export the CLASS DIRECTLY, not wrapped in an object
// This matches: const GraphExporter = require('../graph/exporter');
// ============================================================================
module.exports = GraphExporter;

@@ -6,3 +6,3 @@ <!DOCTYPE html>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - DevCompass</title>
<title>DevCompass - Dependency Graph</title>
<script src="https://d3js.org/d3.v7.min.js"></script>

@@ -15,262 +15,349 @@ <style>

}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #0f172a;
color: #e2e8f0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0a0e1a;
color: #e0e6ed;
overflow: hidden;
}
/* ========== HEADER ========== */
.header {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
border: 1px solid #334155;
background: linear-gradient(135deg, #1a1f35 0%, #0f1219 100%);
border-bottom: 1px solid #2a3142;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
position: relative;
z-index: 1000;
}
h1 {
font-size: 28px;
color: #f1f5f9;
margin-bottom: 12px;
font-weight: 700;
.header h1 {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
display: flex;
align-items: center;
gap: 12px;
gap: 0.5rem;
}
h1::before {
content: "📊";
font-size: 32px;
.header h1 .icon {
font-size: 1.5rem;
}
.metadata {
color: #94a3b8;
font-size: 14px;
/* ========== CONTROLS PANEL ========== */
.controls {
display: flex;
gap: 1.5rem;
align-items: center;
flex-wrap: wrap;
gap: 24px;
}
.metadata-item {
.control-group {
display: flex;
gap: 0.5rem;
align-items: center;
gap: 6px;
}
.metadata-label {
font-weight: 600;
color: #cbd5e1;
.control-label {
font-size: 0.85rem;
color: #8b92a7;
font-weight: 500;
}
.controls {
background: #1e293b;
padding: 16px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
.btn-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
border: 1px solid #334155;
gap: 0.25rem;
background: #1a1f35;
border-radius: 6px;
padding: 3px;
}
.controls button {
padding: 10px 18px;
border: 1px solid #475569;
border-radius: 8px;
background: linear-gradient(135deg, #334155 0%, #475569 100%);
color: #f1f5f9;
font-size: 14px;
.btn {
padding: 0.4rem 0.9rem;
border: none;
background: transparent;
color: #8b92a7;
font-size: 0.85rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn:hover {
background: #252b42;
color: #fff;
}
.btn.active {
background: #3b82f6;
color: #fff;
}
.slider-container {
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
}
.controls button:hover {
background: linear-gradient(135deg, #475569 0%, #64748b 100%);
border-color: #64748b;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
input[type="range"] {
width: 100px;
height: 4px;
background: #1a1f35;
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.controls button:active {
transform: translateY(0);
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
}
.controls button::before {
font-size: 16px;
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
border: none;
}
.controls button.zoom-in::before { content: "🔍"; }
.controls button.zoom-out::before { content: "🔎"; }
.controls button.reset::before { content: "↻"; }
.controls button.export::before { content: "💾"; }
#graph-container {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
overflow: hidden;
border: 1px solid #334155;
.depth-value {
min-width: 30px;
text-align: center;
font-size: 0.85rem;
color: #3b82f6;
font-weight: 600;
}
/* ========== SEARCH ========== */
.search-container {
position: relative;
}
.search-input {
padding: 0.5rem 1rem;
background: #1a1f35;
border: 1px solid #2a3142;
border-radius: 6px;
color: #e0e6ed;
font-size: 0.85rem;
width: 200px;
outline: none;
transition: all 0.2s;
}
.search-input:focus {
border-color: #3b82f6;
background: #252b42;
}
/* ========== MAIN CONTAINER ========== */
.container {
display: flex;
height: calc(100vh - 80px);
}
/* ========== GRAPH AREA ========== */
.graph-container {
flex: 1;
position: relative;
overflow: hidden;
}
#graph {
width: 100%;
height: 800px;
height: 100%;
}
.legend {
background: #1e293b;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
margin-top: 20px;
border: 1px solid #334155;
/* ========== SIDEBAR ========== */
.sidebar {
width: 300px;
background: #0f1219;
border-left: 1px solid #2a3142;
overflow-y: auto;
padding: 1.5rem;
}
.legend h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #f1f5f9;
.sidebar h2 {
font-size: 1rem;
color: #fff;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #2a3142;
}
.stat-grid {
display: grid;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.stat-item {
background: #1a1f35;
padding: 0.75rem;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.legend h3::before {
content: "📌";
font-size: 18px;
.stat-label {
font-size: 0.85rem;
color: #8b92a7;
}
.legend-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
.stat-value {
font-size: 1.25rem;
font-weight: 600;
color: #fff;
}
.stat-value.danger { color: #ef4444; }
.stat-value.warning { color: #f59e0b; }
.stat-value.success { color: #10b981; }
/* ========== LEGEND ========== */
.legend {
margin-top: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #cbd5e1;
padding: 8px;
gap: 0.5rem;
padding: 0.5rem 0;
font-size: 0.85rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
}
/* ========== CONTROL BUTTONS ========== */
.control-btn {
width: 100%;
padding: 0.6rem 1rem;
background: #1a1f35;
border: 1px solid #2a3142;
border-radius: 6px;
transition: background 0.2s;
color: #e0e6ed;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
text-align: left;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 0.5rem;
}
.legend-item:hover {
background: rgba(51, 65, 85, 0.5);
.control-btn:hover {
background: #252b42;
border-color: #3b82f6;
color: #fff;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #0f172a;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
flex-shrink: 0;
.control-btn:active {
transform: scale(0.98);
background: #3b82f6;
}
.footer {
margin-top: 20px;
padding: 16px;
text-align: center;
color: #64748b;
font-size: 13px;
background: #1e293b;
border-radius: 12px;
border: 1px solid #334155;
/* ========== TOOLTIP ========== */
.tooltip {
position: absolute;
background: rgba(15, 18, 25, 0.95);
border: 1px solid #3b82f6;
border-radius: 8px;
padding: 1rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
max-width: 350px;
z-index: 10000;
backdrop-filter: blur(10px);
}
.footer a {
color: #60a5fa;
text-decoration: none;
.tooltip.visible {
opacity: 1;
}
.tooltip-title {
font-size: 1rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.5rem;
}
.tooltip-content {
font-size: 0.85rem;
color: #8b92a7;
line-height: 1.5;
}
.tooltip-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
background: #ef4444;
color: #fff;
font-size: 0.75rem;
border-radius: 4px;
margin-right: 0.25rem;
font-weight: 500;
}
.footer a:hover {
color: #93c5fd;
text-decoration: underline;
}
/* Node and link styles */
.tooltip-badge.warning { background: #f59e0b; }
.tooltip-badge.info { background: #3b82f6; }
/* ========== GRAPH STYLES ========== */
.node {
cursor: pointer;
stroke: #1e293b;
stroke-width: 2px;
transition: all 0.3s ease;
transition: all 0.2s;
}
.node:hover {
stroke: #f1f5f9;
stroke-width: 3px;
filter: brightness(1.2);
filter: brightness(1.3);
}
.link {
stroke: #475569;
stroke-opacity: 0.4;
stroke-width: 1.5px;
fill: none;
.node-circle {
stroke-width: 2px;
transition: all 0.2s;
}
.node-label {
font-size: 11px;
fill: #cbd5e1;
fill: #e0e6ed;
pointer-events: none;
text-anchor: middle;
pointer-events: none;
user-select: none;
font-weight: 500;
}
/* Tooltip */
.tooltip {
position: absolute;
padding: 12px 16px;
background: rgba(15, 23, 42, 0.95);
border: 1px solid #475569;
border-radius: 8px;
pointer-events: none;
font-size: 13px;
max-width: 320px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
z-index: 1000;
backdrop-filter: blur(8px);
.link {
stroke: #2a3142;
stroke-opacity: 0.6;
fill: none;
stroke-width: 1.5px;
}
.tooltip-title {
font-weight: 600;
margin-bottom: 10px;
color: #60a5fa;
font-size: 15px;
border-bottom: 1px solid #334155;
padding-bottom: 8px;
.link.circular {
stroke: #ef4444;
stroke-dasharray: 5, 5;
stroke-opacity: 0.8;
}
.tooltip-row {
margin: 6px 0;
display: flex;
justify-content: space-between;
gap: 16px;
}
.tooltip-label {
color: #94a3b8;
font-weight: 500;
}
.tooltip-value {
color: #e2e8f0;
font-weight: 600;
}
/* Loading spinner */
/* ========== LOADING ========== */
.loading {

@@ -281,194 +368,717 @@ position: absolute;

transform: translate(-50%, -50%);
color: #60a5fa;
font-size: 18px;
font-weight: 600;
text-align: center;
display: none;
}
.loading::after {
content: "...";
animation: dots 1.5s steps(4, end) infinite;
.loading.visible {
display: block;
}
@keyframes dots {
0%, 20% { content: "."; }
40% { content: ".."; }
60%, 100% { content: "..."; }
.spinner {
width: 50px;
height: 50px;
border: 3px solid #2a3142;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
/* Responsive */
@media (max-width: 768px) {
body {
padding: 12px;
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== RESPONSIVE ========== */
@media (max-width: 1024px) {
.sidebar {
width: 250px;
}
h1 {
font-size: 22px;
.controls {
gap: 1rem;
}
.metadata {
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 8px;
height: auto;
gap: 1rem;
}
.controls {
justify-content: center;
width: 100%;
justify-content: flex-start;
}
.legend-items {
grid-template-columns: 1fr;
.sidebar {
display: none;
}
#graph {
height: 600px;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f172a;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<h1>{{TITLE}}</h1>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Project:</span>
<span>{{PROJECT_NAME}}</span>
<h1>
<span class="icon">📊</span>
DevCompass - Dependency Graph
</h1>
<!-- Controls -->
<div class="controls">
<!-- Layout Switcher -->
<div class="control-group">
<span class="control-label">Layout:</span>
<div class="btn-group">
<button class="btn active" data-layout="tree">Tree</button>
<button class="btn" data-layout="force">Force</button>
<button class="btn" data-layout="radial">Radial</button>
<button class="btn" data-layout="conflict">Conflict</button>
</div>
</div>
<div class="metadata-item">
<span class="metadata-label">Version:</span>
<span>{{PROJECT_VERSION}}</span>
<!-- Filter Switcher -->
<div class="control-group">
<span class="control-label">Filter:</span>
<div class="btn-group">
<button class="btn active" data-filter="all">All</button>
<button class="btn" data-filter="vulnerable">Vulnerable</button>
<button class="btn" data-filter="outdated">Outdated</button>
<button class="btn" data-filter="deprecated">Deprecated</button>
<button class="btn" data-filter="unused">Unused</button>
</div>
</div>
<div class="metadata-item">
<span class="metadata-label">Dependencies:</span>
<span>{{TOTAL_DEPS}}</span>
<!-- Depth Slider -->
<div class="control-group slider-container">
<span class="control-label">Depth:</span>
<input type="range" id="depthSlider" min="1" max="10" value="10">
<span class="depth-value" id="depthValue">∞</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Max Depth:</span>
<span>{{MAX_DEPTH}}</span>
<!-- Search -->
<div class="search-container">
<input type="text" class="search-input" placeholder="Search packages..." id="searchInput">
</div>
<div class="metadata-item">
<span class="metadata-label">Generated:</span>
<span>{{GENERATED_AT}}</span>
</div>
</div>
</div>
<div class="controls">
<button id="zoom-in" class="zoom-in">Zoom In</button>
<button id="zoom-out" class="zoom-out">Zoom Out</button>
<button id="reset-zoom" class="reset">Reset View</button>
<button id="export-svg" class="export">Export SVG</button>
</div>
{{SEARCH_FILTER_HTML}}
<div id="graph-container">
<div id="graph">
<div class="loading">Loading graph</div>
<!-- Main Container -->
<div class="container">
<!-- Graph -->
<div class="graph-container">
<svg id="graph"></svg>
<div class="loading" id="loading">
<div class="spinner"></div>
<div>Rendering graph...</div>
</div>
</div>
</div>
<div class="legend">
<h3>Health Score Legend</h3>
<div class="legend-items">
<div class="legend-item">
<div class="legend-color" style="background: #10b981;"></div>
<span>Excellent (9-10)</span>
<!-- Sidebar -->
<div class="sidebar">
<h2>Statistics</h2>
<div class="stat-grid" id="stats">
<!-- Populated by JS -->
</div>
<div class="legend-item">
<div class="legend-color" style="background: #84cc16;"></div>
<span>Good (7-8)</span>
<h2>Legend</h2>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #10b981;"></div>
<span>Healthy</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f59e0b;"></div>
<span>Outdated</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ef4444;"></div>
<span>Vulnerable</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #8b5cf6;"></div>
<span>Deprecated</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #6b7280;"></div>
<span>Unused</span>
</div>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #eab308;"></div>
<span>Fair (5-6)</span>
<h2>Controls</h2>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<button class="control-btn" onclick="fitToScreen()">⛶ Fit to Screen</button>
<button class="control-btn" onclick="zoomIn()">🔍+ Zoom In</button>
<button class="control-btn" onclick="zoomOut()">🔍− Zoom Out</button>
<button class="control-btn" onclick="resetZoom()">⟲ Reset Zoom</button>
<button class="control-btn" onclick="centerGraph()">⊙ Center View</button>
<hr style="border: none; border-top: 1px solid #2a3142; margin: 0.5rem 0;">
<button class="control-btn" onclick="exportPNG()">📸 Save as PNG</button>
<button class="control-btn" onclick="exportJSON()">💾 Save as JSON</button>
<button class="control-btn" onclick="toggleFullscreen()">🖵 Fullscreen</button>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f97316;"></div>
<span>Poor (3-4)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ef4444;"></div>
<span>Critical (0-2)</span>
</div>
</div>
</div>
<div class="footer">
Generated by <a href="https://github.com/AjayBThorat-20/devcompass" target="_blank">DevCompass</a> v3.1.0
· <a href="https://www.npmjs.com/package/devcompass" target="_blank">npm</a>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<script>
// Remove loading indicator
setTimeout(() => {
const loading = document.querySelector('.loading');
if (loading) loading.remove();
}, 100);
{{GRAPH_SCRIPT}}
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', function() {
if (typeof zoom !== 'undefined') {
d3.select('#graph svg').transition().duration(300).call(zoom.scaleBy, 1.3);
}
// Graph data will be injected here
// @ts-nocheck
const graphData = {{GRAPH_DATA}};
const metadata = graphData.metadata || {};
// State
let currentLayout = metadata.defaultLayout || 'tree';
let currentFilter = metadata.defaultFilter || 'all';
let currentDepth = metadata.defaultDepth || 10;
let searchTerm = '';
let currentZoom = null; // Store zoom behavior
let currentSvg = null; // Store current SVG
let currentG = null; // Store current group
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeControls();
renderGraph();
updateStats();
});
document.getElementById('zoom-out').addEventListener('click', function() {
if (typeof zoom !== 'undefined') {
d3.select('#graph svg').transition().duration(300).call(zoom.scaleBy, 0.7);
// Control handlers
function initializeControls() {
// Layout buttons
document.querySelectorAll('[data-layout]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-layout]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentLayout = btn.dataset.layout;
renderGraph();
});
});
// Filter buttons
document.querySelectorAll('[data-filter]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
renderGraph();
});
});
// Depth slider
const slider = document.getElementById('depthSlider');
const value = document.getElementById('depthValue');
slider.addEventListener('input', (e) => {
currentDepth = parseInt(e.target.value);
value.textContent = currentDepth === 10 ? '∞' : currentDepth;
renderGraph();
});
// Search
document.getElementById('searchInput').addEventListener('input', (e) => {
searchTerm = e.target.value.toLowerCase();
renderGraph();
});
}
// Filter nodes
function filterNodes(nodes) {
let filtered = nodes;
// Apply depth filter
const maxDepth = currentDepth === 10 ? Infinity : currentDepth;
filtered = filtered.filter(n => (n.depth || 0) <= maxDepth);
// Apply category filter
if (currentFilter !== 'all') {
filtered = filtered.filter(n => {
if (n.type === 'root') return true;
switch(currentFilter) {
case 'vulnerable': return n.isVulnerable;
case 'outdated': return n.isOutdated;
case 'deprecated': return n.isDeprecated;
case 'unused': return n.isUnused;
case 'conflict': return n.isVulnerable || n.isDeprecated || n.isOutdated;
default: return true;
}
});
}
});
// Apply search filter
if (searchTerm) {
filtered = filtered.filter(n =>
n.name.toLowerCase().includes(searchTerm)
);
}
return filtered;
}
// Render graph
function renderGraph() {
const loading = document.getElementById('loading');
loading.classList.add('visible');
setTimeout(() => {
const svg = d3.select('#graph');
svg.selectAll('*').remove();
const filteredNodes = filterNodes(graphData.nodes);
const nodeIds = new Set(filteredNodes.map(n => n.id));
const filteredLinks = graphData.links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
// Render based on layout
switch(currentLayout) {
case 'tree': renderTreeLayout(svg, filteredNodes, filteredLinks); break;
case 'force': renderForceLayout(svg, filteredNodes, filteredLinks); break;
case 'radial': renderRadialLayout(svg, filteredNodes, filteredLinks); break;
case 'conflict': renderConflictLayout(svg, filteredNodes, filteredLinks); break;
}
loading.classList.remove('visible');
updateStats();
}, 100);
}
// Layout implementations
function renderTreeLayout(svg, nodes, links) {
const width = window.innerWidth - 300;
const height = window.innerHeight - 80;
svg.attr('width', width).attr('height', height);
const g = svg.append('g');
const root = buildHierarchy(nodes, links);
if (!root) return;
// Increase spacing between nodes
const tree = d3.tree()
.size([width - 200, height - 200])
.separation((a, b) => (a.parent === b.parent ? 1.5 : 2));
tree(root);
// Links
g.selectAll('.link')
.data(root.links())
.enter().append('path')
.attr('class', 'link')
.attr('d', d3.linkVertical()
.x(d => d.x + 100)
.y(d => d.y + 100));
// Nodes
const node = g.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x + 100},${d.y + 100})`);
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 6)
.attr('fill', d => getNodeColor(d.data))
.attr('stroke', d => getNodeStroke(d.data))
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
// Improved label positioning to prevent overlap
node.append('text')
.attr('class', 'node-label')
.attr('dy', d => d.children ? -12 : 15) // Above if has children, below if leaf
.attr('text-anchor', 'middle')
.text(d => {
// Truncate long names
const name = d.data.name;
return name.length > 15 ? name.substring(0, 15) + '...' : name;
})
.style('font-size', '10px')
.style('pointer-events', 'none');
addZoom(svg, g);
// Auto-fit after rendering
setTimeout(() => centerGraph(), 100);
}
function renderForceLayout(svg, nodes, links) {
const width = window.innerWidth - 300;
const height = window.innerHeight - 80;
svg.attr('width', width).attr('height', height);
const g = svg.append('g');
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2));
const link = g.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
const node = g.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd));
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 8)
.attr('fill', getNodeColor)
.attr('stroke', getNodeStroke)
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
node.append('text')
.attr('class', 'node-label')
.attr('dy', -12)
.text(d => d.name);
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
addZoom(svg, g);
function dragStart(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragging(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragEnd(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
}
function renderRadialLayout(svg, nodes, links) {
const width = window.innerWidth - 300;
const height = window.innerHeight - 80;
const radius = Math.min(width, height) / 2 - 100;
svg.attr('width', width).attr('height', height);
const g = svg.append('g').attr('transform', `translate(${width/2},${height/2})`);
const root = buildHierarchy(nodes, links);
if (!root) return;
const tree = d3.cluster().size([2 * Math.PI, radius]);
tree(root);
// Links
g.selectAll('.link')
.data(root.links())
.enter().append('path')
.attr('class', 'link')
.attr('d', d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y));
// Nodes
const node = g.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
node.append('circle')
.attr('class', 'node-circle')
.attr('r', 6)
.attr('fill', d => getNodeColor(d.data))
.attr('stroke', d => getNodeStroke(d.data))
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
node.append('text')
.attr('class', 'node-label')
.attr('dy', -10)
.attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
.text(d => d.data.name);
addZoom(svg, g);
}
function renderConflictLayout(svg, nodes, links) {
const conflictNodes = nodes.filter(n =>
n.type === 'root' || n.isVulnerable || n.isDeprecated || n.isOutdated
);
const nodeIds = new Set(conflictNodes.map(n => n.id));
const conflictLinks = links.filter(l =>
nodeIds.has(typeof l.source === 'object' ? l.source.id : l.source) &&
nodeIds.has(typeof l.target === 'object' ? l.target.id : l.target)
);
renderForceLayout(svg, conflictNodes, conflictLinks);
}
// Utilities
function buildHierarchy(nodes, links) {
const root = nodes.find(n => n.type === 'root');
if (!root) return null;
try {
return d3.stratify()
.id(d => d.id)
.parentId(d => {
const link = links.find(l => {
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return targetId === d.id;
});
return link ? (typeof link.source === 'object' ? link.source.id : link.source) : null;
})(nodes);
} catch (e) {
console.error('Hierarchy build failed:', e);
return null;
}
}
function getNodeColor(node) {
if (node.type === 'root') return '#3b82f6';
if (node.isVulnerable) return '#ef4444';
if (node.isDeprecated) return '#8b5cf6';
if (node.isOutdated) return '#f59e0b';
if (node.isUnused) return '#6b7280';
return '#10b981';
}
function getNodeStroke(node) {
if (node.type === 'root') return '#2563eb';
return '#1a1f35';
}
function showTooltip(event, d) {
const data = d.data || d;
const tooltip = document.getElementById('tooltip');
let content = `<div class="tooltip-title">${data.name}@${data.version || 'unknown'}</div>`;
content += `<div class="tooltip-content">`;
if (data.isVulnerable) content += `<span class="tooltip-badge">Vulnerable</span>`;
if (data.isDeprecated) content += `<span class="tooltip-badge">Deprecated</span>`;
if (data.isOutdated) content += `<span class="tooltip-badge warning">Outdated</span>`;
if (data.isUnused) content += `<span class="tooltip-badge info">Unused</span>`;
content += `<br>Depth: ${data.depth || 0}`;
if (data.healthScore) content += `<br>Health: ${data.healthScore}/10`;
content += `</div>`;
tooltip.innerHTML = content;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY + 10) + 'px';
tooltip.classList.add('visible');
}
function hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
function addZoom(svg, g) {
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => g.attr('transform', event.transform));
svg.call(zoom);
// Store references for external controls
currentZoom = zoom;
currentSvg = svg;
currentG = g;
}
// ========== CONTROL FUNCTIONS ==========
document.getElementById('reset-zoom').addEventListener('click', function() {
if (typeof zoom !== 'undefined') {
d3.select('#graph svg').transition().duration(500).call(zoom.transform, d3.zoomIdentity);
function zoomIn() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 1.3);
}
});
// SVG export
document.getElementById('export-svg').addEventListener('click', function() {
}
function zoomOut() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(300).call(currentZoom.scaleBy, 0.7);
}
}
function resetZoom() {
if (currentSvg && currentZoom) {
currentSvg.transition().duration(500).call(currentZoom.transform, d3.zoomIdentity);
}
}
function centerGraph() {
if (!currentSvg || !currentZoom || !currentG) return;
try {
const svgElement = document.querySelector('#graph svg');
if (!svgElement) {
alert('No graph to export. Please wait for the graph to load.');
return;
}
// Get the bounding box of all graph elements
const bbox = currentG.node().getBBox();
const svgData = svgElement.outerHTML;
const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'dependency-graph-' + new Date().getTime() + '.svg';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
// Get container dimensions
const containerWidth = currentSvg.node().clientWidth;
const containerHeight = currentSvg.node().clientHeight;
// Calculate scale to fit content with padding
const padding = 50;
const scaleX = (containerWidth - padding * 2) / bbox.width;
const scaleY = (containerHeight - padding * 2) / bbox.height;
const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 1x
// Calculate translation to center the content
const tx = (containerWidth - bbox.width * scale) / 2 - bbox.x * scale;
const ty = (containerHeight - bbox.height * scale) / 2 - bbox.y * scale;
// Apply transform
currentSvg.transition().duration(750).call(
currentZoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale)
);
} catch (error) {
console.error('Export failed:', error);
alert('Failed to export SVG. See console for details.');
// Fallback to simple center
const width = currentSvg.node().clientWidth;
const height = currentSvg.node().clientHeight;
currentSvg.transition().duration(500).call(
currentZoom.transform,
d3.zoomIdentity.translate(width / 2, height / 2).scale(1)
);
}
});
}
function fitToScreen() {
centerGraph(); // Uses the same logic
}
function exportPNG() {
try {
const svgElement = document.getElementById('graph');
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
canvas.width = svgElement.clientWidth;
canvas.height = svgElement.clientHeight;
img.onload = function() {
ctx.fillStyle = '#0a0e1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'dependency-graph-' + currentLayout + '-' + currentFilter + '.png';
a.click();
URL.revokeObjectURL(url);
});
};
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
} catch (error) {
alert('PNG export requires the graph to be fully loaded. Please try again.');
}
}
function exportJSON() {
const filtered = filterNodes(graphData.nodes);
const nodeIds = new Set(filtered.map(n => n.id));
const filteredLinks = graphData.links.filter(l => {
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
const exportData = {
layout: currentLayout,
filter: currentFilter,
depth: currentDepth,
nodes: filtered,
links: filteredLinks,
metadata: graphData.metadata
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'dependency-graph-' + currentLayout + '-' + currentFilter + '.json';
a.click();
URL.revokeObjectURL(url);
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
alert('Fullscreen not supported on this browser');
});
} else {
document.exitFullscreen();
}
}
function updateStats() {
const filtered = filterNodes(graphData.nodes);
const stats = {
total: filtered.length,
vulnerable: filtered.filter(n => n.isVulnerable).length,
deprecated: filtered.filter(n => n.isDeprecated).length,
outdated: filtered.filter(n => n.isOutdated).length,
unused: filtered.filter(n => n.isUnused).length,
healthy: filtered.filter(n => !n.isVulnerable && !n.isDeprecated && !n.isOutdated && !n.isUnused).length
};
document.getElementById('stats').innerHTML = `
<div class="stat-item">
<span class="stat-label">Total</span>
<span class="stat-value">${stats.total}</span>
</div>
<div class="stat-item">
<span class="stat-label">Vulnerable</span>
<span class="stat-value danger">${stats.vulnerable}</span>
</div>
<div class="stat-item">
<span class="stat-label">Deprecated</span>
<span class="stat-value warning">${stats.deprecated}</span>
</div>
<div class="stat-item">
<span class="stat-label">Outdated</span>
<span class="stat-value warning">${stats.outdated}</span>
</div>
<div class="stat-item">
<span class="stat-label">Unused</span>
<span class="stat-value">${stats.unused}</span>
</div>
<div class="stat-item">
<span class="stat-label">Healthy</span>
<span class="stat-value success">${stats.healthy}</span>
</div>
`;
}
</script>
</body>
</html>
// src/utils/license-conflict-fixer.js
// v3.1.4 - License conflict fixer with dynamic alternatives
const { execSync } = require('child_process');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const { analyzer } = require('../services');
class LicenseConflictFixer {
constructor() {
this.fixesApplied = [];
this.fixesSkipped = [];
this.fixes = [];
this.skipped = [];
this.errors = [];
this.alternatives = this.loadAlternatives();
}
/**
* Load package alternatives database
* Fix a license conflict warning
*/
loadAlternatives() {
async fixWarning(warning, dryRun = false) {
const packageName = warning.package;
try {
const alternativesPath = path.join(__dirname, '../../data/package-alternatives.json');
if (fs.existsSync(alternativesPath)) {
return JSON.parse(fs.readFileSync(alternativesPath, 'utf8'));
}
return null;
} catch (error) {
console.error('Warning: Could not load package alternatives database');
return null;
}
}
async fixWarning(warning, projectPath, report, progress, skipConfirmation = false) {
try {
switch (warning.autoFixAction) {
case 'replace':
return await this.replacePackage(warning, projectPath, report, progress, skipConfirmation);
case 'review':
// Requires manual review
this.fixesSkipped.push({
package: warning.package,
reason: 'Requires manual legal review',
warning: warning
// Get dynamic alternative from license analyzer
const result = await analyzer.license.analyzePackage(packageName);
if (result.alternative) {
if (!dryRun) {
// Uninstall problematic package
try {
execSync(`npm uninstall ${packageName}`, {
stdio: 'pipe',
cwd: process.cwd()
});
} catch (error) {
// Ignore uninstall errors
}
// Install alternative
execSync(`npm install ${result.alternative}`, {
stdio: 'pipe',
cwd: process.cwd()
});
report.addSkipped(
warning.package,
'License conflict - requires manual legal review'
);
return false;
}
default:
this.fixesSkipped.push({
package: warning.package,
reason: 'No auto-fix available',
warning: warning
});
return false;
this.fixes.push({
package: packageName,
action: 'replaced',
replacement: result.alternative,
oldLicense: warning.license,
newLicense: 'MIT', // Most alternatives are MIT
severity: warning.severity
});
return {
success: true,
action: 'replaced',
metadata: {
from: packageName,
to: result.alternative,
oldLicense: warning.license,
newLicense: 'MIT'
}
};
} else {
// No alternative found - requires manual review
this.skipped.push({
package: packageName,
license: warning.license,
severity: warning.severity,
reason: 'No permissive alternative available - manual review required'
});
return {
success: false,
action: 'review',
reason: 'No alternative found'
};
}
} catch (error) {
this.errors.push({
package: warning.package,
package: packageName,
error: error.message
});
report.addError(warning.package, error);
return false;
return {
success: false,
action: 'error',
reason: error.message
};
}
}
/**
* Replace package with license-compatible alternative
* Display summary of license fixes
*/
async replacePackage(warning, projectPath, report, progress, skipConfirmation) {
const pkgName = warning.package.split('@')[0];
const alternative = warning.suggestedAlternative;
if (!alternative) {
this.fixesSkipped.push({
package: warning.package,
reason: 'No alternative available',
warning: warning
displaySummary() {
console.log(chalk.bold.cyan('\n⚖️ LICENSE FIXES SUMMARY\n'));
if (this.fixes.length > 0) {
console.log(chalk.green(`✓ ${this.fixes.length} license conflict(s) resolved:\n`));
this.fixes.forEach(fix => {
console.log(` ${chalk.cyan(fix.package)} → ${chalk.green(fix.replacement)}`);
console.log(` ${chalk.gray('License:')} ${chalk.red(fix.oldLicense)} → ${chalk.green(fix.newLicense)}`);
});
return false;
console.log('');
}
// Requires confirmation unless skipConfirmation is true
if (!skipConfirmation && warning.requiresConfirmation) {
this.fixesSkipped.push({
package: warning.package,
reason: 'Requires confirmation (use --yes to auto-apply)',
warning: warning
if (this.skipped.length > 0) {
console.log(chalk.yellow(`⚠️ ${this.skipped.length} conflict(s) require manual review:\n`));
this.skipped.forEach(skip => {
console.log(` ${chalk.yellow(skip.package)}`);
console.log(` ${chalk.gray('License:')} ${skip.license}`);
console.log(` ${chalk.gray('Severity:')} ${skip.severity}`);
console.log(` ${chalk.gray('Reason:')} ${skip.reason}`);
});
report.addSkipped(
warning.package,
'License conflict - requires confirmation'
);
return false;
console.log('');
}
progress.update(`Replacing ${pkgName} with ${alternative.name}...`);
try {
// Remove conflicting package
execSync(`npm uninstall ${pkgName}`, {
cwd: projectPath,
stdio: 'pipe'
if (this.errors.length > 0) {
console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`));
this.errors.forEach(err => {
console.log(` ${chalk.red(err.package)}`);
console.log(` ${chalk.gray('Error:')} ${err.error}`);
});
// Install alternative
execSync(`npm install ${alternative.name}`, {
cwd: projectPath,
stdio: 'pipe'
});
this.fixesApplied.push({
type: 'license_replaced',
package: warning.package,
alternative: alternative.name,
action: `Replaced with license-compatible alternative: ${alternative.name}`,
oldLicense: warning.license,
newLicense: alternative.license
});
report.addFix(
'license-conflict',
warning.package,
`Replaced with ${alternative.name}`,
{
from: warning.package,
to: alternative.name,
oldLicense: warning.license,
newLicense: alternative.license,
reason: warning.reason
}
);
return true;
} catch (error) {
throw new Error(`Failed to replace ${pkgName}: ${error.message}`);
console.log('');
}
}
/**
* Find alternative for a package
* Get summary statistics
* @returns {Object}
*/
findAlternative(packageName, license) {
if (!this.alternatives) return null;
// Check GPL alternatives
if (license.includes('GPL') && !license.includes('LGPL')) {
const gplAlts = this.alternatives.gpl_alternatives[packageName];
if (gplAlts && gplAlts.alternatives.length > 0) {
return gplAlts.alternatives[0];
}
}
// Check AGPL alternatives
if (license.includes('AGPL')) {
const agplAlts = this.alternatives.agpl_alternatives[packageName];
if (agplAlts && agplAlts.alternatives.length > 0) {
return agplAlts.alternatives[0];
}
}
// Check LGPL alternatives
if (license.includes('LGPL')) {
const lgplAlts = this.alternatives.lgpl_alternatives[packageName];
if (lgplAlts && lgplAlts.alternatives.length > 0) {
return lgplAlts.alternatives[0];
}
}
return null;
}
/**
* Get summary of fixes
*/
getSummary() {
return {
applied: this.fixesApplied.length,
skipped: this.fixesSkipped.length,
errors: this.errors.length,
details: {
gplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('GPL') && !f.oldLicense.includes('LGPL')).length,
agplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('AGPL')).length,
lgplReplaced: this.fixesApplied.filter(f => f.oldLicense && f.oldLicense.includes('LGPL')).length
}
totalFixes: this.fixes.length,
totalSkipped: this.skipped.length,
totalErrors: this.errors.length,
fixes: this.fixes,
skipped: this.skipped,
errors: this.errors
};
}
/**
* Display summary
* Reset fixer state
*/
displaySummary() {
const summary = this.getSummary();
if (summary.applied > 0) {
console.log(chalk.green.bold(`\n✓ License Conflict Fixes Applied: ${summary.applied}`));
if (summary.details.gplReplaced > 0) {
console.log(chalk.yellow(` • GPL packages replaced: ${summary.details.gplReplaced}`));
}
if (summary.details.agplReplaced > 0) {
console.log(chalk.red(` • AGPL packages replaced: ${summary.details.agplReplaced}`));
}
if (summary.details.lgplReplaced > 0) {
console.log(chalk.yellow(` • LGPL packages replaced: ${summary.details.lgplReplaced}`));
}
}
if (summary.skipped > 0) {
console.log(chalk.yellow(`\n⊘ License Conflict Fixes Skipped: ${summary.skipped}`));
this.fixesSkipped.forEach(skip => {
console.log(chalk.gray(` • ${skip.package}: ${skip.reason}`));
});
}
if (summary.errors > 0) {
console.log(chalk.red(`\n✗ License Conflict Fix Errors: ${summary.errors}`));
this.errors.forEach(err => {
console.log(chalk.red(` • ${err.package}: ${err.error}`));
});
}
reset() {
this.fixes = [];
this.skipped = [];
this.errors = [];
}

@@ -224,0 +158,0 @@ }

// src/utils/quality-fixer.js
const fs = require('fs');
const path = require('path');
// v3.1.4 - Quality issue fixer with dynamic alternatives
const { execSync } = require('child_process');
const chalk = require('chalk');
const { analyzer } = require('../services');
class QualityFixer {
constructor() {
this.fixesApplied = [];
this.fixesSkipped = [];
this.alternatives = null;
this.fixes = [];
this.skipped = [];
this.errors = [];
}
/**
* Load quality alternatives database
* Find alternative for a package (dynamic lookup)
*/
loadAlternatives() {
if (this.alternatives) {
return this.alternatives;
}
async findAlternative(packageName) {
try {
const alternativesPath = path.join(__dirname, '../../data/quality-alternatives.json');
this.alternatives = JSON.parse(fs.readFileSync(alternativesPath, 'utf8'));
return this.alternatives;
const result = await analyzer.quality.analyzePackage(packageName);
if (result.alternative) {
return {
recommended: result.alternative,
reason: result.deprecationMessage || 'Package has quality issues',
migration_guide: null // Could be enhanced with migration guides
};
}
return null;
} catch (error) {
console.error(chalk.yellow('⚠️ Could not load quality alternatives database'));
this.alternatives = {
abandoned_alternatives: {},
stale_alternatives: {},
migration_guides: {}
};
return this.alternatives;
return null;
}
}
async fixWarning(warning, dryRun = false) {
this.loadAlternatives();
const { package: pkgName, status, healthScore } = warning;
// Determine fix action based on status
if (status === 'ABANDONED' || status === 'DEPRECATED') {
return this.replacePackage(pkgName, warning, dryRun);
} else if (status === 'STALE') {
return this.replacePackage(pkgName, warning, dryRun);
} else if (status === 'NEEDS_ATTENTION') {
// For packages that need attention but aren't abandoned, just review
return this.reviewPackage(pkgName, warning, dryRun);
}
return {
success: false,
action: 'skipped',
reason: 'No fix available for this status'
};
}
/**
* Replace package with modern alternative
* Fix a quality warning
*/
async replacePackage(pkgName, warning, dryRun = false) {
const alternative = this.findAlternative(pkgName);
if (!alternative) {
this.fixesSkipped.push({
package: pkgName,
reason: 'No alternative found in database',
status: warning.status
async fixWarning(pkg, dryRun = false) {
const packageName = pkg.name || pkg.package;
try {
// Get dynamic alternative
const alternative = await this.findAlternative(packageName);
if (alternative) {
if (!dryRun) {
// Uninstall old package
try {
execSync(`npm uninstall ${packageName}`, {
stdio: 'pipe',
cwd: process.cwd()
});
} catch (error) {
// Ignore uninstall errors
}
// Install alternative
execSync(`npm install ${alternative.recommended}`, {
stdio: 'pipe',
cwd: process.cwd()
});
}
this.fixes.push({
package: packageName,
action: 'replaced',
replacement: alternative.recommended,
reason: alternative.reason,
status: pkg.status
});
return {
success: true,
action: 'replaced',
metadata: {
from: packageName,
to: alternative.recommended,
reason: alternative.reason
}
};
} else {
// No alternative available - requires manual review
this.skipped.push({
package: packageName,
reason: 'No alternative available - requires manual review',
status: pkg.status
});
return {
success: false,
action: 'review',
reason: 'No alternative available'
};
}
} catch (error) {
this.errors.push({
package: packageName,
error: error.message
});
return {
success: false,
action: 'skipped',
reason: 'No alternative available'
action: 'error',
reason: error.message
};
}
const fix = {
package: pkgName,
action: 'Replace with modern alternative',
alternative: alternative.recommended,
allAlternatives: alternative.alternatives,
reason: alternative.reason,
migrationGuide: alternative.migration_guide,
status: warning.status
};
if (dryRun) {
return {
success: true,
action: 'planned',
fix
};
}
// Record the fix
this.fixesApplied.push(fix);
return {
success: true,
action: 'replaced',
fix,
command: `npm uninstall ${pkgName} && npm install ${alternative.recommended}`
};
}
/**
* Review package (no automatic fix)
* Display summary of quality fixes
*/
async reviewPackage(pkgName, warning, dryRun = false) {
const fix = {
package: pkgName,
action: 'Review and monitor',
healthScore: warning.healthScore,
lastUpdate: warning.lastUpdate,
reason: 'Package needs attention but is not abandoned',
status: warning.status
};
if (dryRun) {
return {
success: true,
action: 'review',
fix
};
displaySummary() {
console.log(chalk.bold.cyan('\n📦 QUALITY FIXES SUMMARY\n'));
if (this.fixes.length > 0) {
console.log(chalk.green(`✓ ${this.fixes.length} package(s) replaced:\n`));
this.fixes.forEach(fix => {
console.log(` ${chalk.cyan(fix.package)} → ${chalk.green(fix.replacement)}`);
console.log(` ${chalk.gray('Reason:')} ${fix.reason}`);
});
console.log('');
}
this.fixesSkipped.push(fix);
return {
success: false,
action: 'review',
reason: 'Manual review required',
fix
};
}
/**
* Remove package without replacement
*/
async removePackage(pkgName, warning, dryRun = false) {
const fix = {
package: pkgName,
action: 'Remove package',
reason: warning.reason || 'Package is obsolete',
status: warning.status
};
if (dryRun) {
return {
success: true,
action: 'planned',
fix
};
if (this.skipped.length > 0) {
console.log(chalk.yellow(`⚠️ ${this.skipped.length} package(s) require manual review:\n`));
this.skipped.forEach(skip => {
console.log(` ${chalk.yellow(skip.package)}`);
console.log(` ${chalk.gray('Reason:')} ${skip.reason}`);
});
console.log('');
}
this.fixesApplied.push(fix);
return {
success: true,
action: 'removed',
fix,
command: `npm uninstall ${pkgName}`
};
}
/**
* Find alternative for a package
*/
findAlternative(pkgName) {
this.loadAlternatives();
// Check abandoned packages first
if (this.alternatives.abandoned_alternatives[pkgName]) {
return this.alternatives.abandoned_alternatives[pkgName];
if (this.errors.length > 0) {
console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`));
this.errors.forEach(err => {
console.log(` ${chalk.red(err.package)}`);
console.log(` ${chalk.gray('Error:')} ${err.error}`);
});
console.log('');
}
// Check stale packages
if (this.alternatives.stale_alternatives[pkgName]) {
return this.alternatives.stale_alternatives[pkgName];
}
return null;
}
/**
* Get migration guide URL
* Get summary statistics
*/
getMigrationGuide(from, to) {
this.loadAlternatives();
const key = `${from}_to_${to}`;
return this.alternatives.migration_guides[key] || null;
}
/**
* Get summary of fixes
*/
getSummary() {
const abandonedFixed = this.fixesApplied.filter(f =>
f.status === 'ABANDONED' || f.status === 'DEPRECATED'
).length;
const staleFixed = this.fixesApplied.filter(f =>
f.status === 'STALE'
).length;
return {
total: this.fixesApplied.length,
abandoned: abandonedFixed,
stale: staleFixed,
skipped: this.fixesSkipped.length,
fixes: this.fixesApplied,
skippedItems: this.fixesSkipped
totalFixes: this.fixes.length,
totalSkipped: this.skipped.length,
totalErrors: this.errors.length,
fixes: this.fixes,
skipped: this.skipped,
errors: this.errors
};
}
/**
* Display summary in terminal
* Reset fixer state
*/
displaySummary() {
const summary = this.getSummary();
if (summary.total === 0) {
return;
}
console.log('\n' + chalk.green('✓ Package Quality Fixes Applied: ') + chalk.bold(summary.total));
if (summary.abandoned > 0) {
console.log(chalk.gray(' • Abandoned/deprecated packages replaced: ') + chalk.bold(summary.abandoned));
}
if (summary.stale > 0) {
console.log(chalk.gray(' • Stale packages replaced: ') + chalk.bold(summary.stale));
}
if (summary.skipped > 0) {
console.log(chalk.gray(' • Packages skipped (no alternative): ') + chalk.bold(summary.skipped));
}
}
/**
* Reset fixes tracking
*/
reset() {
this.fixesApplied = [];
this.fixesSkipped = [];
this.fixes = [];
this.skipped = [];
this.errors = [];
}

@@ -249,0 +173,0 @@ }

// src/utils/supply-chain-fixer.js
// v3.1.4 - Supply chain security fixer with dynamic detection
const { execSync } = require('child_process');
const chalk = require('chalk');
const { analyzer } = require('../services');
class SupplyChainFixer {
constructor() {
this.fixesApplied = [];
this.fixesSkipped = [];
this.fixes = [];
this.skipped = [];
this.errors = [];
}
async fixWarning(warning, projectPath, report, progress, skipConfirmation = false) {
/**
* Fix a supply chain warning
*/
async fixWarning(warning, dryRun = false) {
const packageName = warning.package;
try {
switch (warning.autoFixAction) {
case 'remove':
return await this.removeMaliciousPackage(warning, projectPath, report, progress);
if (warning.type === 'typosquatting') {
// Verify it's still suspicious with live check
const check = analyzer.security.checkTyposquatting(packageName);
case 'replace':
return await this.replaceTyposquatPackage(warning, projectPath, report, progress);
case 'review':
// Requires manual confirmation due to suspicious scripts
if (skipConfirmation || warning.requiresConfirmation === false) {
return await this.removeSuspiciousPackage(warning, projectPath, report, progress);
} else {
this.fixesSkipped.push({
package: warning.package,
reason: 'Requires manual review (suspicious install script)',
warning: warning
if (check) {
if (!dryRun) {
// Remove suspicious package
execSync(`npm uninstall ${packageName}`, {
stdio: 'pipe',
cwd: process.cwd()
});
report.addSkipped(
warning.package,
'Suspicious install script - requires manual review'
);
return false;
// Install correct package if suggested
if (warning.correctPackage) {
try {
execSync(`npm install ${warning.correctPackage}`, {
stdio: 'pipe',
cwd: process.cwd()
});
} catch (error) {
// If correct package install fails, just remove suspicious one
}
}
}
this.fixes.push({
package: packageName,
action: 'removed',
correctPackage: warning.correctPackage || null,
reason: 'Typosquatting detected',
distance: warning.distance
});
return {
success: true,
action: 'removed',
metadata: {
package: packageName,
correctPackage: warning.correctPackage,
reason: 'Typosquatting'
}
};
} else {
// No longer flagged as suspicious
this.skipped.push({
package: packageName,
reason: 'No longer flagged as suspicious'
});
return {
success: false,
action: 'skip',
reason: 'Not suspicious'
};
}
} else if (warning.type === 'vulnerability') {
// Security vulnerabilities are handled by npm audit fix
if (!dryRun) {
execSync('npm audit fix', {
stdio: 'pipe',
cwd: process.cwd()
});
}
default:
this.fixesSkipped.push({
package: warning.package,
reason: 'No auto-fix available',
warning: warning
});
return false;
this.fixes.push({
package: packageName,
action: 'updated',
reason: 'Security vulnerability fixed',
severity: warning.severity
});
return {
success: true,
action: 'updated',
metadata: {
package: packageName,
severity: warning.severity
}
};
} else {
// Unknown type - skip
this.skipped.push({
package: packageName,
reason: `Unknown warning type: ${warning.type}`
});
return {
success: false,
action: 'skip',
reason: 'Unknown type'
};
}
} catch (error) {
this.errors.push({
package: warning.package,
package: packageName,
error: error.message
});
report.addError(warning.package, error);
return false;
return {
success: false,
action: 'error',
reason: error.message
};
}
}
/**
* Remove malicious package
* Display summary of supply chain fixes
*/
async removeMaliciousPackage(warning, projectPath, report, progress) {
progress.update(`Removing malicious package: ${warning.package}...`);
try {
execSync(`npm uninstall ${warning.package}`, {
cwd: projectPath,
stdio: 'pipe'
displaySummary() {
console.log(chalk.bold.cyan('\n🛡️ SUPPLY CHAIN FIXES SUMMARY\n'));
if (this.fixes.length > 0) {
console.log(chalk.green(`✓ ${this.fixes.length} issue(s) resolved:\n`));
this.fixes.forEach(fix => {
if (fix.action === 'removed') {
console.log(` ${chalk.red('✗')} Removed: ${chalk.cyan(fix.package)}`);
if (fix.correctPackage) {
console.log(` ${chalk.green('✓')} Installed: ${chalk.green(fix.correctPackage)}`);
}
console.log(` ${chalk.gray('Reason:')} ${fix.reason}`);
} else if (fix.action === 'updated') {
console.log(` ${chalk.green('↑')} Updated: ${chalk.cyan(fix.package)}`);
console.log(` ${chalk.gray('Reason:')} ${fix.reason}`);
}
});
this.fixesApplied.push({
type: 'malicious_removed',
package: warning.package,
action: 'Removed malicious package'
});
report.addFix(
'supply-chain',
warning.package,
'Removed malicious package',
{ reason: warning.reason }
);
return true;
} catch (error) {
throw new Error(`Failed to remove ${warning.package}: ${error.message}`);
console.log('');
}
}
/**
* Replace typosquatting package with correct one
*/
async replaceTyposquatPackage(warning, projectPath, report, progress) {
progress.update(`Fixing typosquatting: ${warning.package} → ${warning.replacement}...`);
try {
// Remove typosquat
execSync(`npm uninstall ${warning.package}`, {
cwd: projectPath,
stdio: 'pipe'
if (this.skipped.length > 0) {
console.log(chalk.yellow(`⚠️ ${this.skipped.length} warning(s) skipped:\n`));
this.skipped.forEach(skip => {
console.log(` ${chalk.yellow(skip.package)}`);
console.log(` ${chalk.gray('Reason:')} ${skip.reason}`);
});
// Install correct package
execSync(`npm install ${warning.replacement}`, {
cwd: projectPath,
stdio: 'pipe'
});
this.fixesApplied.push({
type: 'typosquat_fixed',
package: warning.package,
replacement: warning.replacement,
action: `Replaced with correct package: ${warning.replacement}`
});
report.addFix(
'supply-chain',
warning.package,
`Replaced typosquat with ${warning.replacement}`,
{
from: warning.package,
to: warning.replacement,
reason: warning.reason
}
);
return true;
} catch (error) {
throw new Error(`Failed to replace ${warning.package}: ${error.message}`);
console.log('');
}
}
/**
* Remove package with suspicious install scripts
*/
async removeSuspiciousPackage(warning, projectPath, report, progress) {
progress.update(`Removing suspicious package: ${warning.package}...`);
try {
execSync(`npm uninstall ${warning.package}`, {
cwd: projectPath,
stdio: 'pipe'
if (this.errors.length > 0) {
console.log(chalk.red(`✗ ${this.errors.length} error(s) occurred:\n`));
this.errors.forEach(err => {
console.log(` ${chalk.red(err.package)}`);
console.log(` ${chalk.gray('Error:')} ${err.error}`);
});
this.fixesApplied.push({
type: 'suspicious_removed',
package: warning.package,
action: 'Removed package with suspicious install script'
});
report.addFix(
'supply-chain',
warning.package,
'Removed package with suspicious install script',
{
script: warning.script,
patterns: warning.patterns,
reason: warning.reason
}
);
return true;
} catch (error) {
throw new Error(`Failed to remove ${warning.package}: ${error.message}`);
console.log('');
}
}
/**
* Get summary of fixes
* Get summary statistics
*/
getSummary() {
return {
applied: this.fixesApplied.length,
skipped: this.fixesSkipped.length,
errors: this.errors.length,
details: {
maliciousRemoved: this.fixesApplied.filter(f => f.type === 'malicious_removed').length,
typosquatsFixed: this.fixesApplied.filter(f => f.type === 'typosquat_fixed').length,
suspiciousRemoved: this.fixesApplied.filter(f => f.type === 'suspicious_removed').length
}
totalFixes: this.fixes.length,
totalSkipped: this.skipped.length,
totalErrors: this.errors.length,
fixes: this.fixes,
skipped: this.skipped,
errors: this.errors
};
}
/**
* Display summary
* Reset fixer state
*/
displaySummary() {
const summary = this.getSummary();
if (summary.applied > 0) {
console.log(chalk.green.bold(`\n✓ Supply Chain Fixes Applied: ${summary.applied}`));
if (summary.details.maliciousRemoved > 0) {
console.log(chalk.red(` • Malicious packages removed: ${summary.details.maliciousRemoved}`));
}
if (summary.details.typosquatsFixed > 0) {
console.log(chalk.yellow(` • Typosquats fixed: ${summary.details.typosquatsFixed}`));
}
if (summary.details.suspiciousRemoved > 0) {
console.log(chalk.yellow(` • Suspicious packages removed: ${summary.details.suspiciousRemoved}`));
}
}
if (summary.skipped > 0) {
console.log(chalk.yellow(`\n⊘ Supply Chain Fixes Skipped: ${summary.skipped}`));
this.fixesSkipped.forEach(skip => {
console.log(chalk.gray(` • ${skip.package}: ${skip.reason}`));
});
}
if (summary.errors > 0) {
console.log(chalk.red(`\n✗ Supply Chain Fix Errors: ${summary.errors}`));
this.errors.forEach(err => {
console.log(chalk.red(` • ${err.package}: ${err.error}`));
});
}
reset() {
this.fixes = [];
this.skipped = [];
this.errors = [];
}

@@ -215,0 +199,0 @@ }

{
"malicious_packages": [
"epress",
"expres",
"expresss",
"reqest",
"requet",
"lodas",
"loadsh",
"axois",
"axioss",
"webpak",
"webpackk",
"reactt",
"vuee",
"angularr"
],
"typosquat_patterns": {
"express": ["epress", "expres", "expresss", "exprss"],
"request": ["reqest", "requet", "requets"],
"lodash": ["lodas", "loadsh", "lodahs", "lodsh"],
"axios": ["axois", "axioss", "axos", "axious"],
"webpack": ["webpak", "webpackk", "wepback"],
"react": ["reactt", "reakt", "raect"],
"vue": ["vuee", "veu", "vuw"],
"angular": ["angularr", "anguler", "angulr"],
"next": ["nextt", "nxt", "nex"],
"typescript": ["typscript", "typescrpt", "typescrip"],
"eslint": ["esslint", "elint", "eslnt"],
"prettier": ["pretier", "prettir", "pretter"],
"jest": ["jst", "jestt", "jест"],
"mocha": ["mocha", "mоcha", "mосha"],
"chai": ["chаi", "сhai", "chаi"]
},
"suspicious_patterns": {
"install_scripts": [
"curl",
"wget",
"powershell",
"eval",
"exec",
"child_process",
"/bin/sh",
"/bin/bash",
"http://",
"https://"
],
"suspicious_dependencies": [
"bitcoin",
"cryptocurrency",
"mining",
"miner",
"keylogger",
"backdoor"
]
}
}
{
"gpl_alternatives": {
"readline": {
"license": "GPL-3.0",
"alternatives": [
{
"name": "readline-sync",
"license": "MIT",
"description": "Synchronous readline for interactively running"
}
]
},
"node-gtk": {
"license": "GPL-3.0",
"alternatives": [
{
"name": "electron",
"license": "MIT",
"description": "Build cross-platform desktop apps"
}
]
}
},
"agpl_alternatives": {
"ghost": {
"license": "AGPL-3.0",
"alternatives": [
{
"name": "hexo",
"license": "MIT",
"description": "A fast, simple & powerful blog framework"
}
]
}
},
"lgpl_alternatives": {
"sharp": {
"license": "LGPL-3.0",
"alternatives": [
{
"name": "jimp",
"license": "MIT",
"description": "An image processing library written entirely in JavaScript"
},
{
"name": "gm",
"license": "MIT",
"description": "GraphicsMagick and ImageMagick for node"
}
]
}
},
"unlicensed_alternatives": {
"default": [
{
"suggestion": "Find well-maintained MIT/Apache-2.0 alternative",
"action": "Search npm for similar packages with permissive licenses"
}
]
},
"compatible_licenses": {
"MIT": ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "CC0-1.0", "Unlicense"],
"Apache-2.0": ["Apache-2.0", "MIT", "BSD-2-Clause", "BSD-3-Clause", "ISC"],
"GPL-2.0": ["GPL-2.0", "GPL-3.0"],
"GPL-3.0": ["GPL-3.0"],
"AGPL-3.0": ["AGPL-3.0"]
}
}
{
"abandoned_alternatives": {
"request": {
"alternatives": ["axios", "got", "ky"],
"recommended": "axios",
"reason": "request is deprecated since 2020",
"migration_guide": "https://github.com/request/request/issues/3142"
},
"moment": {
"alternatives": ["dayjs", "date-fns", "luxon"],
"recommended": "dayjs",
"reason": "moment is in maintenance mode",
"migration_guide": "https://momentjs.com/docs/#/-project-status/"
},
"tslint": {
"alternatives": ["eslint"],
"recommended": "eslint",
"reason": "tslint is deprecated",
"migration_guide": "https://github.com/palantir/tslint/issues/4534"
},
"node-sass": {
"alternatives": ["sass", "dart-sass"],
"recommended": "sass",
"reason": "node-sass is deprecated, use Dart Sass",
"migration_guide": "https://sass-lang.com/blog/libsass-is-deprecated"
},
"babel-core": {
"alternatives": ["@babel/core"],
"recommended": "@babel/core",
"reason": "babel-core is deprecated",
"migration_guide": "https://babeljs.io/docs/en/v7-migration"
},
"colors": {
"alternatives": ["chalk", "colorette", "picocolors"],
"recommended": "chalk",
"reason": "colors had a security incident in 2022",
"migration_guide": "https://github.com/Marak/colors.js/issues/285"
},
"mkdirp": {
"alternatives": ["native fs.mkdir with recursive option"],
"recommended": "native",
"reason": "mkdirp is obsolete, use native fs.mkdir({recursive: true})",
"migration_guide": "https://nodejs.org/api/fs.html#fspromisesmkdirpath-options"
},
"rimraf": {
"alternatives": ["native fs.rm with recursive option"],
"recommended": "native",
"reason": "rimraf is obsolete, use native fs.rm({recursive: true})",
"migration_guide": "https://nodejs.org/api/fs.html#fspromisesrmpath-options"
},
"node-uuid": {
"alternatives": ["uuid"],
"recommended": "uuid",
"reason": "node-uuid is deprecated",
"migration_guide": "https://github.com/kelektiv/node-uuid"
},
"validator": {
"alternatives": ["validator (different package)", "joi", "yup"],
"recommended": "joi",
"reason": "Old validator package is abandoned",
"migration_guide": "https://joi.dev/api/"
},
"babel-preset-es2015": {
"alternatives": ["@babel/preset-env"],
"recommended": "@babel/preset-env",
"reason": "babel-preset-es2015 is deprecated",
"migration_guide": "https://babeljs.io/docs/en/babel-preset-env"
},
"babel-preset-react": {
"alternatives": ["@babel/preset-react"],
"recommended": "@babel/preset-react",
"reason": "babel-preset-react is deprecated",
"migration_guide": "https://babeljs.io/docs/en/babel-preset-react"
},
"react-addons-test-utils": {
"alternatives": ["react-dom/test-utils", "@testing-library/react"],
"recommended": "@testing-library/react",
"reason": "react-addons-test-utils is deprecated",
"migration_guide": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme"
},
"prop-types": {
"alternatives": ["TypeScript", "Zod"],
"recommended": "typescript",
"reason": "Consider TypeScript for type safety",
"migration_guide": "https://react-typescript-cheatsheet.netlify.app/"
},
"react-router-redux": {
"alternatives": ["connected-react-router", "react-router v6"],
"recommended": "react-router",
"reason": "react-router-redux is deprecated",
"migration_guide": "https://reactrouter.com/en/main/upgrading/v5"
},
"redux-devtools-extension": {
"alternatives": ["@redux-devtools/extension"],
"recommended": "@redux-devtools/extension",
"reason": "redux-devtools-extension is deprecated",
"migration_guide": "https://github.com/reduxjs/redux-devtools"
},
"enzyme": {
"alternatives": ["@testing-library/react", "react-testing-library"],
"recommended": "@testing-library/react",
"reason": "enzyme is no longer maintained",
"migration_guide": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme"
},
"faker": {
"alternatives": ["@faker-js/faker"],
"recommended": "@faker-js/faker",
"reason": "faker was archived, use @faker-js/faker",
"migration_guide": "https://fakerjs.dev/guide/upgrading.html"
},
"gulp-util": {
"alternatives": ["individual gulp utilities"],
"recommended": "native",
"reason": "gulp-util is deprecated",
"migration_guide": "https://github.com/gulpjs/gulp-util"
},
"native-promise-only": {
"alternatives": ["native Promise"],
"recommended": "native",
"reason": "native-promise-only is obsolete",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"
},
"q": {
"alternatives": ["native Promise", "bluebird"],
"recommended": "native",
"reason": "Q promise library is obsolete",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"
},
"coffee-script": {
"alternatives": ["coffeescript"],
"recommended": "coffeescript",
"reason": "coffee-script is renamed to coffeescript",
"migration_guide": "https://coffeescript.org/"
},
"node-pre-gyp": {
"alternatives": ["@mapbox/node-pre-gyp"],
"recommended": "@mapbox/node-pre-gyp",
"reason": "node-pre-gyp moved to @mapbox scope",
"migration_guide": "https://github.com/mapbox/node-pre-gyp"
},
"bower": {
"alternatives": ["npm", "yarn"],
"recommended": "npm",
"reason": "bower is deprecated",
"migration_guide": "https://bower.io/blog/2017/how-to-migrate-away-from-bower/"
},
"grunt": {
"alternatives": ["gulp", "webpack", "vite"],
"recommended": "vite",
"reason": "grunt is largely unmaintained",
"migration_guide": "https://vitejs.dev/guide/migration.html"
},
"phantomjs": {
"alternatives": ["puppeteer", "playwright"],
"recommended": "playwright",
"reason": "phantomjs is abandoned",
"migration_guide": "https://playwright.dev/docs/intro"
},
"protractor": {
"alternatives": ["cypress", "playwright", "@playwright/test"],
"recommended": "playwright",
"reason": "protractor is deprecated",
"migration_guide": "https://playwright.dev/docs/protractor"
},
"karma": {
"alternatives": ["vitest", "jest", "playwright"],
"recommended": "vitest",
"reason": "karma is in maintenance mode",
"migration_guide": "https://vitest.dev/guide/migration.html"
},
"istanbul": {
"alternatives": ["nyc", "c8"],
"recommended": "c8",
"reason": "istanbul is deprecated, use nyc or c8",
"migration_guide": "https://github.com/bcoe/c8"
},
"grunt-contrib-uglify": {
"alternatives": ["terser", "esbuild"],
"recommended": "esbuild",
"reason": "grunt-contrib-uglify is deprecated",
"migration_guide": "https://esbuild.github.io/"
},
"uglify-js": {
"alternatives": ["terser", "esbuild"],
"recommended": "terser",
"reason": "uglify-js is no longer maintained",
"migration_guide": "https://terser.org/"
},
"node-fetch": {
"alternatives": ["native fetch", "undici"],
"recommended": "native",
"reason": "Node.js 18+ has native fetch",
"migration_guide": "https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#fetch"
},
"isomorphic-fetch": {
"alternatives": ["native fetch", "cross-fetch"],
"recommended": "native",
"reason": "isomorphic-fetch is obsolete with native fetch",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"
},
"cross-env": {
"alternatives": ["dotenv", "native support"],
"recommended": "dotenv",
"reason": "cross-env is less needed with modern Node.js",
"migration_guide": "https://github.com/motdotla/dotenv"
},
"nodemon": {
"alternatives": ["tsx", "node --watch"],
"recommended": "tsx",
"reason": "Node.js 18+ has native --watch flag",
"migration_guide": "https://nodejs.org/api/cli.html#--watch"
},
"body-parser": {
"alternatives": ["express.json() built-in"],
"recommended": "native",
"reason": "body-parser is built into Express 4.16+",
"migration_guide": "https://expressjs.com/en/api.html#express.json"
},
"morgan": {
"alternatives": ["pino-http", "winston"],
"recommended": "pino-http",
"reason": "morgan is basic, modern alternatives are better",
"migration_guide": "https://github.com/pinojs/pino-http"
},
"async": {
"alternatives": ["native async/await", "Promise"],
"recommended": "native",
"reason": "async library is obsolete with native async/await",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"
},
"bluebird": {
"alternatives": ["native Promise"],
"recommended": "native",
"reason": "bluebird is mostly obsolete with native Promises",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"
},
"jade": {
"alternatives": ["pug"],
"recommended": "pug",
"reason": "jade was renamed to pug",
"migration_guide": "https://pugjs.org/"
},
"ejs": {
"alternatives": ["handlebars", "pug", "react/vue"],
"recommended": "handlebars",
"reason": "ejs is functional but alternatives are more modern",
"migration_guide": "https://handlebarsjs.com/guide/"
},
"underscore": {
"alternatives": ["lodash", "native methods"],
"recommended": "lodash",
"reason": "underscore is less maintained than lodash",
"migration_guide": "https://lodash.com/docs/"
},
"harmony-reflect": {
"alternatives": ["native Reflect"],
"recommended": "native",
"reason": "harmony-reflect is obsolete with native Reflect",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect"
},
"core-js": {
"alternatives": ["native features", "modern browsers"],
"recommended": "native",
"reason": "core-js is often unnecessary with modern browsers",
"migration_guide": "https://github.com/zloirock/core-js"
},
"left-pad": {
"alternatives": ["String.prototype.padStart"],
"recommended": "native",
"reason": "left-pad is obsolete with native padStart",
"migration_guide": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart"
},
"cheerio": {
"alternatives": ["jsdom", "linkedom"],
"recommended": "linkedom",
"reason": "cheerio is slow, linkedom is faster and more modern",
"migration_guide": "https://github.com/WebReflection/linkedom"
},
"xml2js": {
"alternatives": ["fast-xml-parser", "xmldom"],
"recommended": "fast-xml-parser",
"reason": "xml2js is slower than modern alternatives",
"migration_guide": "https://github.com/NaturalIntelligence/fast-xml-parser"
},
"should": {
"alternatives": ["chai", "jest"],
"recommended": "jest",
"reason": "should is deprecated",
"migration_guide": "https://jestjs.io/docs/getting-started"
}
},
"stale_alternatives": {
"depcheck": {
"alternatives": ["npm-check", "knip"],
"recommended": "knip",
"reason": "depcheck hasn't been updated in 30+ months",
"migration_guide": "https://github.com/webpro/knip"
}
},
"migration_guides": {
"request_to_axios": "https://github.com/axios/axios#example",
"moment_to_dayjs": "https://day.js.org/docs/en/parse/string-format",
"tslint_to_eslint": "https://github.com/typescript-eslint/tslint-to-eslint-config",
"node-sass_to_sass": "https://sass-lang.com/install",
"babel_migration": "https://babeljs.io/docs/en/v7-migration",
"enzyme_to_rtl": "https://testing-library.com/docs/react-testing-library/migrate-from-enzyme"
}
}