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
JavaScript
#!/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