UNPKG

dep-insight-cli

Version:

A powerful CLI tool for managing npm packages, checking dependencies, analyzing sizes, and ensuring license compliance.

1,780 lines (1,531 loc) 90.5 kB
#!/usr/bin/env node const { program } = require("commander"); const chalk = require("chalk"); const inquirer = require("inquirer"); const fs = require("fs"); const path = require("path"); const axios = require("axios"); const ora = require("ora"); const boxen = require("boxen"); const { table } = require("table"); const fuzzy = require("fuzzy"); const filesize = require("filesize"); const cliProgress = require("cli-progress"); const asciichart = require("asciichart"); const { format, subDays, parseISO } = require("date-fns"); const Table3 = require("cli-table3"); const pacote = require("pacote"); const treeify = require("tree-node-cli"); const madge = require("madge"); const { promisify } = require("util"); const execAsync = promisify(require("child_process").exec); const crossSpawn = require("cross-spawn"); const npmRunPath = require("npm-run-path"); const which = require("which"); const dependencyTree = require("dependency-tree"); const graphviz = require("graphviz"); const temp = require("temp"); const semver = require("semver"); const { execSync } = require("child_process"); // Read package.json from the current working directory function readPackageJson() { try { const packageJsonPath = path.join(process.cwd(), "package.json"); return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); } catch (error) { console.error(chalk.red("Error: Could not find or read package.json")); process.exit(1); } } // Format download count function formatDownloads(count) { if (count > 1000000) { return `${(count / 1000000).toFixed(1)}M`; } else if (count > 1000) { return `${(count / 1000).toFixed(1)}K`; } return count; } // Get package download stats async function getPackageStats(packageName) { try { const response = await axios.get( `https://api.npmjs.org/downloads/point/last-week/${packageName}`, ); return response.data.downloads; } catch (error) { return null; } } // Check for vulnerabilities async function checkVulnerabilities(packageName, version) { try { const response = await axios.get( `https://registry.npmjs.org/-/npm/v1/security/advisories/search?package=${packageName}`, ); const vulnerabilities = response.data.objects.filter((vuln) => semver.satisfies(version, vuln.vulnerable_versions || ""), ); return vulnerabilities; } catch (error) { return []; } } // Check outdated packages async function checkOutdated() { const packageJson = readPackageJson(); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, }; console.log(chalk.cyan("Checking package versions...")); console.log( chalk.dim( "Package".padEnd(20), "Current".padEnd(15), "Latest".padEnd(15), "Downloads/week", ), ); console.log(chalk.dim("─".repeat(65))); for (const [pkg, version] of Object.entries(dependencies)) { try { const [registryData, downloads] = await Promise.all([ axios.get(`https://registry.npmjs.org/${pkg}`), getPackageStats(pkg), ]); const latestVersion = registryData.data["dist-tags"].latest; const currentVersion = version.replace(/[\^~]/g, ""); const downloadCount = downloads ? formatDownloads(downloads) : "N/A"; const isOutdated = semver.lt(currentVersion, latestVersion); const vulnerabilities = await checkVulnerabilities(pkg, currentVersion); console.log( chalk.white(pkg.padEnd(20)), (isOutdated ? chalk.red : chalk.green)(currentVersion.padEnd(15)), chalk.green(latestVersion.padEnd(15)), chalk.blue(downloadCount), ); if (vulnerabilities.length > 0) { console.log( chalk.red(` ⚠️ ${vulnerabilities.length} known vulnerabilities`), ); } } catch (error) { console.error(chalk.red(`Error checking ${pkg}: ${error.message}`)); } } } // Check compatibility for a new package async function checkPackageCompatibility(packageNames) { const packages = Array.isArray(packageNames) ? packageNames : [packageNames]; for (const packageName of packages) { try { const [registryData, downloads] = await Promise.all([ axios.get(`https://registry.npmjs.org/${packageName}`), getPackageStats(packageName), ]); const latest = registryData.data["dist-tags"].latest; const versions = Object.keys(registryData.data.versions) .reverse() .slice(0, 5); const downloadCount = downloads ? formatDownloads(downloads) : "N/A"; console.log(chalk.cyan(`\nPackage: ${packageName}`)); console.log(chalk.blue(`Weekly Downloads: ${downloadCount}`)); console.log(chalk.green(`Latest version: ${latest}`)); console.log(chalk.yellow("Recent versions:")); versions.forEach((v) => console.log(chalk.dim(` - ${v}`))); // Check vulnerabilities const vulnerabilities = await checkVulnerabilities(packageName, latest); if (vulnerabilities.length > 0) { console.log( chalk.red( `\n⚠️ Warning: ${vulnerabilities.length} known vulnerabilities found!`, ), ); } const answers = await inquirer.prompt([ { type: "list", name: "version", message: "Select version to install:", choices: [...versions, "Cancel"], default: latest, }, { type: "confirm", name: "confirmInstall", message: (answers) => `Do you want to install ${packageName}@${answers.version}?`, default: true, when: (answers) => answers.version !== "Cancel", }, ]); if (answers.version !== "Cancel" && answers.confirmInstall) { const packageManager = fs.existsSync("yarn.lock") ? "yarn" : "npm"; const installCmd = packageManager === "yarn" ? `yarn add ${packageName}@${answers.version}` : `npm install ${packageName}@${answers.version}`; console.log(chalk.cyan(`\nRunning: ${installCmd}`)); try { execSync(installCmd, { stdio: "inherit" }); console.log(chalk.green("\nPackage installed successfully!")); } catch (error) { console.error( chalk.red(`\nError installing package: ${error.message}`), ); } } } catch (error) { console.error( chalk.red( `Error: Could not fetch package information for ${packageName}`, ), ); console.error(error.message); } } } // Package categories and their popular packages const PACKAGE_CATEGORIES = { "API Development": { description: "Build REST APIs and web services", packages: ["express", "fastify", "koa", "nest.js", "hapi"], }, Database: { description: "Database integration and ORMs", packages: ["mongoose", "sequelize", "prisma", "typeorm", "knex"], }, "Frontend Development": { description: "UI frameworks and libraries", packages: ["react", "vue", "angular", "svelte", "next"], }, Testing: { description: "Testing frameworks and tools", packages: ["jest", "mocha", "chai", "cypress", "playwright"], }, Authentication: { description: "Authentication and authorization", packages: ["passport", "jsonwebtoken", "oauth", "auth0", "keycloak"], }, Utilities: { description: "Common utility libraries", packages: ["lodash", "moment", "axios", "uuid", "winston"], }, }; // Suggest packages based on user needs async function suggestPackages() { console.log( chalk.cyan( boxen("Package Suggestion Assistant", { padding: 1, margin: 1, borderStyle: "round", }), ), ); // Ask for project type const { category } = await inquirer.prompt([ { type: "list", name: "category", message: "What type of packages are you looking for?", choices: Object.keys(PACKAGE_CATEGORIES).map((cat) => ({ name: `${cat} - ${PACKAGE_CATEGORIES[cat].description}`, value: cat, })), }, ]); // Ask for specific requirements const { requirements } = await inquirer.prompt([ { type: "input", name: "requirements", message: "Describe your specific requirements (optional):", default: "", }, ]); const spinner = ora("Analyzing packages...").start(); try { const categoryPackages = PACKAGE_CATEGORIES[category].packages; const packagesData = await Promise.all( categoryPackages.map(async (pkg) => { try { const [npmData, downloads] = await Promise.all([ axios.get(`https://registry.npmjs.org/${pkg}`), getPackageStats(pkg), ]); const latest = npmData.data["dist-tags"].latest; const description = npmData.data.description || ""; const maintainers = npmData.data.maintainers?.length || 0; const lastUpdate = new Date(npmData.data.time[latest]); const weeklyDownloads = downloads || 0; return { name: pkg, description, version: latest, downloads: weeklyDownloads, maintainers, lastUpdate, score: calculatePackageScore( weeklyDownloads, maintainers, lastUpdate, ), }; } catch (error) { return null; } }), ); spinner.succeed("Analysis complete!"); // Filter out failed requests and sort by score const validPackages = packagesData .filter((pkg) => pkg !== null) .sort((a, b) => b.score - a.score); // Display results in a nice table const tableData = [ [ "Package", "Description", "Weekly Downloads", "Version", "Last Updated", ].map((h) => chalk.cyan(h)), ]; validPackages.forEach((pkg) => { tableData.push([ chalk.green(pkg.name), truncate(pkg.description, 40), formatDownloads(pkg.downloads), pkg.version, formatDate(pkg.lastUpdate), ]); }); console.log( "\n" + table(tableData, { border: { topBody: chalk.dim("─"), topJoin: chalk.dim("┬"), topLeft: chalk.dim("┌"), topRight: chalk.dim("┐"), bottomBody: chalk.dim("─"), bottomJoin: chalk.dim("┴"), bottomLeft: chalk.dim("└"), bottomRight: chalk.dim("┘"), bodyLeft: chalk.dim("│"), bodyRight: chalk.dim("│"), bodyJoin: chalk.dim("│"), joinBody: chalk.dim("─"), joinLeft: chalk.dim("├"), joinRight: chalk.dim("┤"), joinJoin: chalk.dim("┼"), }, }), ); // Ask if user wants to install any package const { installPackage } = await inquirer.prompt([ { type: "list", name: "installPackage", message: "Would you like to install any of these packages?", choices: [ ...validPackages.map((pkg) => ({ name: `${pkg.name} (${pkg.version})`, value: pkg.name, })), { name: "None", value: null }, ], }, ]); if (installPackage) { await checkPackageCompatibility(installPackage); } } catch (error) { spinner.fail("Error analyzing packages"); console.error(chalk.red(`Error: ${error.message}`)); } } // Helper function to calculate package score function calculatePackageScore(downloads, maintainers, lastUpdate) { const downloadScore = Math.min(downloads / 100000, 10); // Max 10 points for downloads const maintainerScore = Math.min(maintainers, 5); // Max 5 points for maintainers const updateScore = Math.max( 0, 5 - (Date.now() - lastUpdate) / (1000 * 60 * 60 * 24 * 30), ); // Max 5 points for recency return downloadScore + maintainerScore + updateScore; } // Helper function to truncate text function truncate(text, length) { if (!text) return ""; return text.length > length ? text.substring(0, length - 3) + "..." : text; } // Helper function to format date function formatDate(date) { const now = new Date(); const diff = now - date; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) return "today"; if (days === 1) return "yesterday"; if (days < 30) return `${days} days ago`; if (days < 365) return `${Math.floor(days / 30)} months ago`; return `${Math.floor(days / 365)} years ago`; } // Health check constants const HEALTH_METRICS = { DOWNLOADS: { weight: 0.3, threshold: 10000 }, MAINTENANCE: { weight: 0.2, threshold: 30 }, QUALITY: { weight: 0.3, threshold: 0.8 }, SECURITY: { weight: 0.2, threshold: 0.9 }, }; // Analyze package health async function analyzePackageHealth(packageName) { console.log( chalk.cyan( boxen(`Package Health Check: ${packageName}`, { padding: 1, margin: 1, borderStyle: "round", }), ), ); const spinner = ora("Analyzing package health...").start(); try { // Fetch package data from multiple sources const [npmData, downloads] = await Promise.all([ axios.get(`https://registry.npmjs.org/${packageName}`), getPackageStats(packageName), ]); spinner.succeed("Analysis complete!"); const latest = npmData.data["dist-tags"].latest; const packageInfo = npmData.data.versions[latest]; // Calculate metrics const metrics = await calculateMetrics( packageName, npmData.data, downloads, ); // Display package overview displayPackageOverview(packageName, packageInfo, downloads); // Display detailed metrics await displayDetailedMetrics(metrics); // Display dependencies displayDependencies(packageInfo); // Display security info await displaySecurityInfo(packageName, latest); // Display maintenance stats displayMaintenanceStats(npmData.data); // Display size analysis await displaySizeAnalysis(packageName, latest); // Display final health score displayHealthScore(metrics); } catch (error) { spinner.fail("Error analyzing package health"); console.error(chalk.red(`Error: ${error.message}`)); } } // Calculate package metrics async function calculateMetrics(packageName, npmData, downloads) { const latest = npmData["dist-tags"].latest; const lastUpdate = new Date(npmData.time[latest]); const firstPublish = new Date(npmData.time.created); const ageInDays = (Date.now() - firstPublish) / (1000 * 60 * 60 * 24); const metrics = { downloads: { score: Math.min(downloads / HEALTH_METRICS.DOWNLOADS.threshold, 1), details: { weekly: downloads, trend: "stable", // You could implement trend analysis here }, }, maintenance: { score: calculateMaintenanceScore(npmData), details: { lastUpdate, releaseFrequency: (Object.keys(npmData.versions).length / ageInDays) * 30, // Releases per month issuesResolutionTime: "N/A", // Would require GitHub API integration }, }, quality: { score: calculateQualityScore(npmData), details: { hasTypes: !!npmData.versions[latest].types || !!npmData.versions[latest].typings, hasTests: hasTests(npmData.versions[latest]), hasDocs: true, // Assuming README exists dependencies: Object.keys(npmData.versions[latest].dependencies || {}) .length, }, }, security: { score: 1, // Default to 1, will be updated in displaySecurityInfo details: { vulnerabilities: [], hasLicense: !!npmData.versions[latest].license, }, }, }; return metrics; } // Display package overview function displayPackageOverview(packageName, packageInfo, downloads) { console.log("\n" + chalk.bold("📦 Package Overview")); console.log( table( [ ["Name", chalk.green(packageName)], ["Version", chalk.blue(packageInfo.version)], ["Description", packageInfo.description || "No description"], ["Weekly Downloads", chalk.blue(formatDownloads(downloads))], ["License", packageInfo.license || "Not specified"], ["Homepage", packageInfo.homepage || "Not specified"], ], { border: getBorderCharacters(), }, ), ); } // Display detailed metrics async function displayDetailedMetrics(metrics) { console.log("\n" + chalk.bold("📊 Detailed Metrics")); const bars = new cliProgress.MultiBar( { clearOnComplete: false, hideCursor: true, format: "{bar} {percentage}% | {metric}: {value}", }, cliProgress.Presets.shades_classic, ); const maxValue = 100; const downloadBar = bars.create(maxValue, 0, { metric: "Downloads ", value: "", }); const maintenanceBar = bars.create(maxValue, 0, { metric: "Maintenance ", value: "", }); const qualityBar = bars.create(maxValue, 0, { metric: "Quality ", value: "", }); const securityBar = bars.create(maxValue, 0, { metric: "Security ", value: "", }); downloadBar.update(metrics.downloads.score * 100, { value: formatDownloads(metrics.downloads.details.weekly), }); maintenanceBar.update(metrics.maintenance.score * 100, { value: `${metrics.maintenance.details.releaseFrequency.toFixed(1)} releases/month`, }); qualityBar.update(metrics.quality.score * 100, { value: getQualityLabel(metrics.quality.score), }); securityBar.update(metrics.security.score * 100, { value: getSecurityLabel(metrics.security.score), }); bars.stop(); } // Display dependencies function displayDependencies(packageInfo) { console.log("\n" + chalk.bold("🔗 Dependencies")); const deps = packageInfo.dependencies || {}; const devDeps = packageInfo.devDependencies || {}; const peerDeps = packageInfo.peerDependencies || {}; if ( Object.keys(deps).length === 0 && Object.keys(devDeps).length === 0 && Object.keys(peerDeps).length === 0 ) { console.log(chalk.dim("No dependencies")); return; } if (Object.keys(deps).length > 0) { console.log(chalk.yellow("\nProduction Dependencies:")); Object.entries(deps).forEach(([name, version]) => { console.log(`${chalk.green("├─")} ${name}: ${chalk.blue(version)}`); }); } if (Object.keys(devDeps).length > 0) { console.log(chalk.yellow("\nDevelopment Dependencies:")); Object.entries(devDeps).forEach(([name, version]) => { console.log(`${chalk.green("├─")} ${name}: ${chalk.blue(version)}`); }); } if (Object.keys(peerDeps).length > 0) { console.log(chalk.yellow("\nPeer Dependencies:")); Object.entries(peerDeps).forEach(([name, version]) => { console.log(`${chalk.green("├─")} ${name}: ${chalk.blue(version)}`); }); } } // Display security information async function displaySecurityInfo(packageName, version) { console.log("\n" + chalk.bold("🔒 Security Analysis")); const vulnerabilities = await checkVulnerabilities(packageName, version); if (vulnerabilities.length === 0) { console.log(chalk.green("✓ No known vulnerabilities")); } else { console.log( chalk.red(`⚠️ ${vulnerabilities.length} known vulnerabilities found:`), ); vulnerabilities.forEach((vuln) => { console.log(chalk.red(` ■ ${vuln.title}`)); console.log(chalk.dim(` Severity: ${vuln.severity}`)); console.log( chalk.dim(` Affected versions: ${vuln.vulnerable_versions}`), ); }); } } // Display maintenance statistics function displayMaintenanceStats(npmData) { console.log("\n" + chalk.bold("🔧 Maintenance")); const latest = npmData["dist-tags"].latest; const lastUpdate = new Date(npmData.time[latest]); const firstPublish = new Date(npmData.time.created); const maintainers = npmData.maintainers || []; console.log( table( [ ["Last Update", formatDate(lastUpdate)], ["Package Age", formatPackageAge(firstPublish)], ["Release Count", Object.keys(npmData.versions).length], ["Maintainers", maintainers.length], ], { border: getBorderCharacters(), }, ), ); } // Display size analysis async function displaySizeAnalysis(packageName, version) { console.log("\n" + chalk.bold("📏 Size Analysis")); try { const response = await axios.get( `https://bundlephobia.com/api/size?package=${packageName}@${version}`, ); const { size, gzip } = response.data; console.log( table( [ ["Unpacked Size", chalk.blue(filesize(size))], ["Gzipped Size", chalk.blue(filesize(gzip))], ["Download Time", chalk.blue(calculateDownloadTime(gzip))], ], { border: getBorderCharacters(), }, ), ); } catch (error) { console.log(chalk.dim("Size information not available")); } } // Display final health score function displayHealthScore(metrics) { const score = calculateOverallScore(metrics); const label = getHealthLabel(score); const color = getHealthColor(score); console.log("\n" + chalk.bold("🏆 Overall Health Score")); console.log( boxen(color(`${label}\nScore: ${(score * 100).toFixed(1)}%`), { padding: 1, margin: 1, borderStyle: "round", borderColor: color.name, }), ); } // Helper functions function calculateMaintenanceScore(npmData) { const latest = npmData["dist-tags"].latest; const lastUpdate = new Date(npmData.time[latest]); const daysSinceUpdate = (Date.now() - lastUpdate) / (1000 * 60 * 60 * 24); return Math.max( 0, 1 - daysSinceUpdate / HEALTH_METRICS.MAINTENANCE.threshold, ); } function calculateQualityScore(npmData) { const latest = npmData["dist-tags"].latest; const latestVersion = npmData.versions[latest]; let score = 0; score += latestVersion.readme ? 0.2 : 0; score += latestVersion.repository ? 0.2 : 0; score += latestVersion.homepage ? 0.2 : 0; score += latestVersion.bugs ? 0.2 : 0; score += latestVersion.license ? 0.2 : 0; return score; } function hasTests(packageInfo) { const hasTestScript = packageInfo.scripts && (packageInfo.scripts.test || packageInfo.scripts["test:unit"] || packageInfo.scripts["test:integration"]); const hasTestDeps = packageInfo.devDependencies && Object.keys(packageInfo.devDependencies).some( (dep) => dep.includes("test") || dep.includes("jest") || dep.includes("mocha") || dep.includes("chai"), ); return hasTestScript || hasTestDeps; } function calculateOverallScore(metrics) { return Object.entries(HEALTH_METRICS).reduce((score, [key, { weight }]) => { return score + metrics[key.toLowerCase()].score * weight; }, 0); } function getHealthLabel(score) { if (score >= 0.9) return "Excellent"; if (score >= 0.7) return "Good"; if (score >= 0.5) return "Fair"; return "Needs Improvement"; } function getHealthColor(score) { if (score >= 0.9) return chalk.green; if (score >= 0.7) return chalk.blue; if (score >= 0.5) return chalk.yellow; return chalk.red; } function getQualityLabel(score) { if (score >= 0.8) return "High"; if (score >= 0.6) return "Good"; if (score >= 0.4) return "Fair"; return "Low"; } function getSecurityLabel(score) { if (score >= 0.9) return "Secure"; if (score >= 0.7) return "Fair"; return "At Risk"; } function formatPackageAge(firstPublish) { const days = Math.floor((Date.now() - firstPublish) / (1000 * 60 * 60 * 24)); const years = Math.floor(days / 365); const months = Math.floor((days % 365) / 30); if (years > 0) { return `${years}y ${months}m`; } return `${months}m`; } function calculateDownloadTime(sizeInBytes, speed = 1000000) { // 1MB/s default const seconds = sizeInBytes / speed; return `${seconds.toFixed(2)}s @ 1MB/s`; } function getBorderCharacters() { return { topBody: chalk.dim("─"), topJoin: chalk.dim("┬"), topLeft: chalk.dim("┌"), topRight: chalk.dim("┐"), bottomBody: chalk.dim("─"), bottomJoin: chalk.dim("┴"), bottomLeft: chalk.dim("└"), bottomRight: chalk.dim("┘"), bodyLeft: chalk.dim("│"), bodyRight: chalk.dim("│"), bodyJoin: chalk.dim("│"), joinBody: chalk.dim("─"), joinLeft: chalk.dim("├"), joinRight: chalk.dim("┤"), joinJoin: chalk.dim("┼"), }; } // Compatibility check constants const NODE_LTS_VERSIONS = ["20.x", "18.x", "16.x"]; // Check compatibility between packages async function checkCompatibility(options = {}) { const packageJson = readPackageJson(); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, }; console.log( chalk.cyan( boxen("Compatibility Analysis", { padding: 1, margin: 1, borderStyle: "round", }), ), ); const spinner = ora("Analyzing dependencies...").start(); try { // Get all dependencies data const depsData = await Promise.all( Object.entries(dependencies).map(async ([pkg, version]) => { try { const response = await axios.get(`https://registry.npmjs.org/${pkg}`); return { name: pkg, version: version.replace(/[\^~]/g, ""), data: response.data, }; } catch (error) { return null; } }), ); spinner.succeed("Analysis complete!"); // Filter out failed requests const validDeps = depsData.filter((dep) => dep !== null); // 1. Node.js Version Compatibility await checkNodeVersions(validDeps); // 2. Peer Dependencies Analysis await checkPeerDependencies(validDeps, dependencies); // 3. Version Conflicts await checkVersionConflicts(validDeps); // 4. Security Compatibility await checkSecurityCompatibility(validDeps); } catch (error) { spinner.fail("Error analyzing compatibility"); console.error(chalk.red(`Error: ${error.message}`)); } } // Check Node.js version compatibility async function checkNodeVersions(deps) { console.log("\n" + chalk.bold("🟢 Node.js Version Compatibility")); const nodeVersionTable = [ ["Package", ...NODE_LTS_VERSIONS.map((v) => `Node ${v}`)].map((h) => chalk.cyan(h), ), ]; for (const dep of deps) { const engines = dep.data.versions[dep.data["dist-tags"].latest].engines || {}; const nodeRequirement = engines.node || "*"; const row = [chalk.green(dep.name)]; for (const nodeVersion of NODE_LTS_VERSIONS) { const isCompatible = semver.satisfies( nodeVersion.replace("x", "0"), nodeRequirement, ); row.push(isCompatible ? chalk.green("✓") : chalk.red("✗")); } nodeVersionTable.push(row); } console.log(table(nodeVersionTable, { border: getBorderCharacters() })); } // Check peer dependencies async function checkPeerDependencies(deps, currentDeps) { console.log("\n" + chalk.bold("🤝 Peer Dependencies")); let hasPeerDeps = false; for (const dep of deps) { const latest = dep.data.versions[dep.data["dist-tags"].latest]; const peerDeps = latest.peerDependencies || {}; if (Object.keys(peerDeps).length > 0) { hasPeerDeps = true; console.log(chalk.yellow(`\n${dep.name} requires:`)); for (const [peerName, peerVersion] of Object.entries(peerDeps)) { const installed = currentDeps[peerName]; const status = installed ? semver.satisfies(installed.replace(/[\^~]/g, ""), peerVersion) ? chalk.green("✓ Compatible") : chalk.red("✗ Version mismatch") : chalk.yellow("! Not installed"); console.log(` ${chalk.blue(peerName)}: ${peerVersion} ${status}`); } } } if (!hasPeerDeps) { console.log(chalk.dim("No peer dependencies found")); } } // Check version conflicts async function checkVersionConflicts(deps) { console.log("\n" + chalk.bold("🔄 Version Conflicts")); const conflicts = new Map(); // Find packages that might be required by multiple dependencies for (const dep of deps) { const latest = dep.data.versions[dep.data["dist-tags"].latest]; const allDeps = { ...latest.dependencies, ...latest.devDependencies, ...latest.peerDependencies, }; for (const [name, version] of Object.entries(allDeps)) { if (!conflicts.has(name)) { conflicts.set(name, new Set()); } conflicts.get(name).add(version); } } let hasConflicts = false; // Display conflicts for (const [name, versions] of conflicts) { if (versions.size > 1) { hasConflicts = true; console.log( chalk.yellow(`\n${name} has conflicting version requirements:`), ); versions.forEach((version) => { console.log(` ${chalk.blue("├─")} ${version}`); }); } } if (!hasConflicts) { console.log(chalk.green("✓ No version conflicts found")); } } // Check security compatibility async function checkSecurityCompatibility(deps) { console.log("\n" + chalk.bold("🔒 Security Compatibility")); let hasVulnerabilities = false; for (const dep of deps) { const vulnerabilities = await checkVulnerabilities(dep.name, dep.version); if (vulnerabilities.length > 0) { hasVulnerabilities = true; console.log(chalk.red(`\n${dep.name} has vulnerabilities:`)); vulnerabilities.forEach((vuln) => { console.log(chalk.red(` ■ ${vuln.title}`)); console.log(chalk.dim(` Severity: ${vuln.severity}`)); if (vuln.recommendation) { console.log( chalk.green(` Recommendation: ${vuln.recommendation}`), ); } }); } } if (!hasVulnerabilities) { console.log(chalk.green("✓ No security compatibility issues found")); } } // Fetch package trends data async function fetchPackageTrends(packageName, days = 30) { const endDate = new Date(); const startDate = subDays(endDate, days); try { // Fetch NPM download stats const downloads = await axios.get( `https://api.npmjs.org/downloads/range/${format(startDate, "yyyy-MM-dd")}:${format(endDate, "yyyy-MM-dd")}/${packageName}`, ); // Fetch package info from NPM registry const packageInfo = await axios.get( `https://registry.npmjs.org/${packageName}`, ); return { downloads: downloads.data, info: packageInfo.data, }; } catch (error) { throw new Error(`Failed to fetch trends data: ${error.message}`); } } // Analyze package trends async function analyzePackageTrends(packageName, options) { console.log( chalk.cyan( boxen(`Package Trends: ${packageName}`, { padding: 1, margin: 1, borderStyle: "round", }), ), ); const spinner = ora("Fetching trend data...").start(); try { const data = await fetchPackageTrends(packageName, options.days || 30); spinner.succeed("Data fetched successfully!"); // 1. Download Trends Chart await displayDownloadTrends(data.downloads); // 2. Version History await displayVersionHistory(data.info); // 3. Maintenance Score await displayMaintenanceMetrics(data.info); // 4. Community Engagement await displayCommunityMetrics(data.info); // 5. Comparison with Alternatives if (options.compare) { await compareWithAlternatives(packageName, data.downloads); } } catch (error) { spinner.fail("Error analyzing trends"); console.error(chalk.red(`Error: ${error.message}`)); } } // Display download trends chart async function displayDownloadTrends(data) { console.log("\n" + chalk.bold("📈 Download Trends")); const downloads = data.downloads.map((d) => d.downloads); const config = { height: 10, colors: [asciichart.blue], }; console.log("\n" + asciichart.plot(downloads, config)); // Calculate statistics const total = downloads.reduce((a, b) => a + b, 0); const avg = Math.round(total / downloads.length); const max = Math.max(...downloads); const min = Math.min(...downloads); const statsTable = new Table3({ head: ["Metric", "Value"].map((h) => chalk.cyan(h)), style: { head: [], border: [] }, }); statsTable.push( ["Total Downloads", chalk.green(total.toLocaleString())], ["Daily Average", chalk.yellow(avg.toLocaleString())], ["Peak Downloads", chalk.magenta(max.toLocaleString())], ["Lowest Downloads", chalk.blue(min.toLocaleString())], ); console.log("\n" + statsTable.toString()); } // Display version history async function displayVersionHistory(data) { console.log("\n" + chalk.bold("📦 Version History")); const versions = Object.keys(data.versions).slice(-5); const table = new Table3({ head: ["Version", "Release Date", "Changes"].map((h) => chalk.cyan(h)), style: { head: [], border: [] }, }); for (const version of versions) { const vData = data.versions[version]; let releaseDate = "N/A"; try { if (data.time && data.time[version]) { releaseDate = format(new Date(data.time[version]), "yyyy-MM-dd"); } } catch (error) { // Ignore date parsing errors } const changes = vData.description || "No release notes"; table.push([ chalk.green(version), chalk.yellow(releaseDate), chalk.dim(changes.substring(0, 50) + (changes.length > 50 ? "..." : "")), ]); } console.log("\n" + table.toString()); } // Display maintenance metrics async function displayMaintenanceMetrics(data) { console.log("\n" + chalk.bold("🔧 Maintenance Metrics")); let lastUpdate = new Date(); try { if (data.time && data.time.modified) { lastUpdate = new Date(data.time.modified); } else if (data.time && data.time.created) { lastUpdate = new Date(data.time.created); } } catch (error) { // Ignore date parsing errors } const daysSinceUpdate = Math.round( (Date.now() - lastUpdate) / (1000 * 60 * 60 * 24), ); const monthsSinceCreation = data.time && data.time.created ? Math.round( (Date.now() - new Date(data.time.created)) / (1000 * 60 * 60 * 24 * 30), ) : 1; const table = new Table3({ style: { head: [], border: [] }, }); table.push( ["Last Update", chalk.yellow(format(lastUpdate, "yyyy-MM-dd"))], [ "Days Since Update", daysSinceUpdate < 30 ? chalk.green(daysSinceUpdate) : chalk.red(daysSinceUpdate), ], [ "Release Frequency", `${chalk.blue((Object.keys(data.versions).length / Math.max(1, monthsSinceCreation)).toFixed(1))} releases/month`, ], ["Maintainers", chalk.cyan(data.maintainers?.length || 0)], ); console.log("\n" + table.toString()); } // Display community metrics async function displayCommunityMetrics(data) { console.log("\n" + chalk.bold("👥 Community Engagement")); const table = new Table3({ style: { head: [], border: [] }, }); // Get GitHub info if available const githubUrl = data.repository?.url?.match( /github\.com\/([^\/]+\/[^\/]+)/, )?.[1]; let githubData = null; if (githubUrl) { try { const response = await axios.get( `https://api.github.com/repos/${githubUrl}`, ); githubData = response.data; } catch (error) { // Ignore GitHub API errors } } table.push( [ "GitHub Stars", githubData ? chalk.yellow(githubData.stargazers_count) : chalk.dim("N/A"), ], [ "Open Issues", githubData ? chalk.blue(githubData.open_issues_count) : chalk.dim("N/A"), ], [ "Contributors", githubData ? chalk.green(githubData.subscribers_count) : chalk.dim("N/A"), ], ["License", chalk.cyan(data.license || "N/A")], ); console.log("\n" + table.toString()); } // Find alternative packages async function findAlternatives(packageName) { try { // First try to get alternatives from npm search const response = await axios.get( `https://registry.npmjs.org/-/v1/search?text=keywords:${packageName}&size=5`, ); const alternatives = response.data.objects .map((obj) => obj.package.name) .filter((name) => name !== packageName); // If we don't have enough alternatives, add some common alternatives const commonAlternatives = { express: ["fastify", "koa", "hapi"], react: ["vue", "angular", "svelte"], lodash: ["underscore", "ramda", "radash"], axios: ["node-fetch", "got", "superagent"], webpack: ["vite", "rollup", "parcel"], jest: ["mocha", "ava", "vitest"], prettier: ["eslint", "standard", "xo"], }; if (commonAlternatives[packageName]) { return [ ...new Set([...alternatives, ...commonAlternatives[packageName]]), ]; } return alternatives; } catch (error) { return []; } } // Compare with alternative packages async function compareWithAlternatives(packageName, data) { console.log("\n" + chalk.bold("🔄 Comparison with Alternatives")); const alternatives = await findAlternatives(packageName); const spinner = ora("Fetching comparison data...").start(); try { const comparisonData = await Promise.all( alternatives.slice(0, 3).map(async (alt) => { try { const data = await fetchPackageTrends(alt); return { name: alt, downloads: data.downloads.downloads.reduce( (a, b) => a + b.downloads, 0, ), }; } catch (error) { return null; } }), ); spinner.succeed("Comparison complete!"); const table = new Table3({ head: ["Package", "Downloads (30d)", "Relative"].map((h) => chalk.cyan(h), ), style: { head: [], border: [] }, colWidths: [20, 20, 10], }); const mainDownloads = data.downloads.reduce((a, b) => a + b.downloads, 0); table.push([ chalk.green(packageName), chalk.green(mainDownloads.toLocaleString()), chalk.green("100%"), ]); comparisonData .filter((d) => d !== null) .forEach((alt) => { const percentage = ((alt.downloads / mainDownloads) * 100).toFixed(1); const color = percentage > 80 ? chalk.green : percentage > 40 ? chalk.yellow : chalk.red; table.push([ color(alt.name), color(alt.downloads.toLocaleString()), color(percentage + "%"), ]); }); console.log("\n" + table.toString()); } catch (error) { spinner.fail("Error comparing alternatives"); console.error(chalk.red(`Error: ${error.message}`)); } } // Analyze dependencies async function analyzeDependencies(options = {}) { console.log( chalk.cyan( boxen("Dependency Analysis", { padding: 1, margin: 1, borderStyle: "round", }), ), ); const spinner = ora("Analyzing dependencies...").start(); const packageJson = readPackageJson(); try { // 1. Build dependency tree const tree = await buildDependencyTree(packageJson); spinner.succeed("Analysis complete!"); // 2. Show dependency tree await displayDependencyTree(tree); // 3. Show size analysis if requested if (options.size) { await analyzeDependencySizes(tree); } // 4. Check for security issues if (options.security) { await analyzeSecurityIssues(tree); } // 5. Show upgrade suggestions if (options.upgrade) { await suggestUpgrades(tree); } // 6. Show findings summary await showFindings(tree); } catch (error) { spinner.fail("Error analyzing dependencies"); console.error(chalk.red(`Error: ${error.message}`)); } } // Build dependency tree async function buildDependencyTree(packageJson) { const tree = {}; const seen = new Set(); async function addDependency(name, version, depth = 0) { if (depth > 3) return null; // Limit depth to prevent infinite recursion const key = `${name}@${version}`; if (seen.has(key)) return { circular: true, name, version }; seen.add(key); try { const manifest = await pacote.manifest(`${name}@${version}`); const deps = manifest.dependencies || {}; const node = { name, version: manifest.version, size: await getPackageSize(name, manifest.version), dependencies: {}, }; for (const [depName, depVersion] of Object.entries(deps)) { const depNode = await addDependency(depName, depVersion, depth + 1); if (depNode) { node.dependencies[depName] = depNode; } } return node; } catch (error) { return null; } } const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; for (const [name, version] of Object.entries(deps)) { tree[name] = await addDependency(name, version); } return tree; } // Display dependency tree async function displayDependencyTree(tree) { console.log("\n" + chalk.bold("📦 Dependency Tree")); function formatNode(node, prefix = "") { if (!node) return ""; if (node.circular) return chalk.yellow(`${prefix}${node.name}@${node.version} (circular)`); const size = node.size ? chalk.dim(` (${filesize(node.size)})`) : ""; let output = `${prefix}${node.name}@${node.version}${size}\n`; for (const [depName, depNode] of Object.entries(node.dependencies || {})) { output += formatNode(depNode, `${prefix}├─ `); } return output; } for (const [name, node] of Object.entries(tree)) { console.log(formatNode(node)); } } // Analyze dependency sizes async function analyzeDependencySizes(tree) { console.log("\n" + chalk.bold("📊 Size Analysis")); const table = new Table3({ head: ["Package", "Size", "Impact"].map((h) => chalk.cyan(h)), style: { head: [], border: [] }, colWidths: [30, 15, 15], }); const sizes = []; function collectSizes(node) { if (!node || node.circular) return; if (node.size) { sizes.push({ name: `${node.name}@${node.version}`, size: node.size, }); } Object.values(node.dependencies || {}).forEach(collectSizes); } Object.values(tree).forEach(collectSizes); // Sort by size and show top 10 sizes .sort((a, b) => b.size - a.size) .slice(0, 10) .forEach(({ name, size }) => { const impact = ( (size / sizes.reduce((a, b) => a + b.size, 0)) * 100 ).toFixed(1); table.push([ chalk.green(name), chalk.yellow(filesize(size)), chalk.blue(`${impact}%`), ]); }); console.log("\n" + table.toString()); } // Analyze security issues async function analyzeSecurityIssues(tree) { console.log("\n" + chalk.bold("🔒 Security Analysis")); const vulnerabilities = []; async function checkNodeSecurity(node) { if (!node || node.circular) return; try { const vulns = await checkVulnerabilities(node.name, node.version); if (vulns.length > 0) { vulnerabilities.push({ package: `${node.name}@${node.version}`, vulnerabilities: vulns, }); } } catch (error) { // Ignore errors } await Promise.all( Object.values(node.dependencies || {}).map(checkNodeSecurity), ); } await Promise.all(Object.values(tree).map(checkNodeSecurity)); if (vulnerabilities.length > 0) { console.log( chalk.red(`\n${vulnerabilities.length} vulnerable packages found:`), ); vulnerabilities.forEach(({ package: pkg, vulnerabilities: vulns }) => { console.log(chalk.yellow(`\n${pkg}:`)); vulns.forEach((vuln) => { console.log(chalk.red(` ■ ${vuln.title}`)); console.log(chalk.dim(` Severity: ${vuln.severity}`)); if (vuln.recommendation) { console.log( chalk.green(` Recommendation: ${vuln.recommendation}`), ); } }); }); } else { console.log(chalk.green("\n✓ No security vulnerabilities found")); } } // Suggest upgrades async function suggestUpgrades(tree) { console.log("\n" + chalk.bold("💡 Upgrade Suggestions")); const upgrades = []; async function checkNodeUpgrades(node) { if (!node || node.circular) return; try { const manifest = await pacote.manifest(node.name); const latest = manifest.version; if (semver.gt(latest, node.version)) { upgrades.push({ package: node.name, current: node.version, latest, breaking: semver.major(latest) > semver.major(node.version), }); } } catch (error) { // Ignore errors } await Promise.all( Object.values(node.dependencies || {}).map(checkNodeUpgrades), ); } await Promise.all(Object.values(tree).map(checkNodeUpgrades)); if (upgrades.length > 0) { const table = new Table3({ head: ["Package", "Current", "Latest", "Type"].map((h) => chalk.cyan(h)), style: { head: [], border: [] }, }); upgrades.forEach(({ package: pkg, current, latest, breaking }) => { table.push([ chalk.green(pkg), chalk.yellow(current), chalk.blue(latest), breaking ? chalk.red("Major") : chalk.green("Minor/Patch"), ]); }); console.log("\n" + table.toString()); } else { console.log(chalk.green("\n✓ All dependencies are up to date")); } } // Show findings summary async function showFindings(tree) { console.log("\n" + chalk.bold("🔍 Key Findings")); // Count circular dependencies let circularCount = 0; function countCircular(node) { if (!node) return; if (node.circular) circularCount++; Object.values(node.dependencies || {}).forEach(countCircular); } Object.values(tree).forEach(countCircular); // Calculate total size let totalSize = 0; function sumSizes(node) { if (!node || node.circular) return; if (node.size) totalSize += node.size; Object.values(node.dependencies || {}).forEach(sumSizes); } Object.values(tree).forEach(sumSizes); // Count duplicates const versions = new Map(); function countVersions(node) { if (!node || node.circular) return; const key = node.name; if (!versions.has(key)) versions.set(key, new Set()); versions.get(key).add(node.version); Object.values(node.dependencies || {}).forEach(countVersions); } Object.values(tree).forEach(countVersions); const duplicates = Array.from(versions.entries()).filter( ([, vers]) => vers.size > 1, ); const findings = [ [ "Circular Dependencies", circularCount > 0 ? chalk.yellow(circularCount) : chalk.green("None"), ], ["Total Size", chalk.blue(filesize(totalSize))], [ "Duplicate Packages", duplicates.length > 0 ? chalk.yellow(duplicates.length) : chalk.green("None"), ], ["Direct Dependencies", chalk.blue(Object.keys(tree).length)], ]; const table = new Table3({ style: { head: [], border: [] }, }); findings.forEach((finding) => table.push(finding)); console.log("\n" + table.toString()); // Show recommendations if (circularCount > 0 || duplicates.length > 0) { console.log("\n" + chalk.bold("💡 Recommendations:")); if (circularCount > 0) { console.log( chalk.yellow( "• Review circular dependencies to improve build performance", ), ); } if (duplicates.length > 0) { console.log( chalk.yellow("• Consider deduplicating the following packages:"), ); duplicates.slice(0, 3).forEach(([name, versions]) => { console.log( ` ${chalk.blue(name)}: ${Array.from(versions).join(", ")}`, ); }); } } } // Get package size async function getPackageSize(name, version) { try { const response = await axios.get( `https://bundlephobia.com/api/size?package=${name}@${version}`, ); return response.data.size; } catch (error) { return 0; } } // Analyze npm scripts async function analyzeScripts(scriptName, options = {}) { console.log( chalk.cyan( boxen("NPM Scripts Analysis", { padding: 1, margin: 1, borderStyle: "round", }), ), ); const spinner = ora("Analyzing scripts...").start(); const packageJson = readPackageJson(); const scripts = packageJson.scripts || {}; try { // 1. List and analyze scripts await listScripts(scripts, scriptName); // 2. Analyze performance if requested if (options.time) { await analyzeScriptPerformance(scripts, scriptName); } // 3. Validate scripts if requested if (options.validate) { await validateScripts(scripts); } // 4. Show suggestions if requested if (options.suggest) { showScriptSuggestions(scripts); } // 5. Show findings await showScriptFindings(scripts); spinner.succeed("Analysis complete!"); } catch (error) { spinner.fail("Error analyzing scripts"); console.error(chalk.red(`Error: ${error.message}`)); } } // List and analyze scripts async function listScripts(scripts, scriptName) { console.log("\n" + chalk.bold("📜 Available Scripts")); if (Object.keys(scripts).length === 0) { console.log(chalk.yellow("\nNo scripts found in package.json")); return; } const scriptDeps = new Map(); const scriptTree = {}; // Build script dependency tree for (const [name, cmd] of Object.entries(scripts)) { if (scriptName && name !== scriptName) continue; scriptTree[name] = { command: cmd, dependencies: findScriptDependencies(cmd, scripts), }; // Track reverse dependencies scriptTree[name].dependencies.forE