vibe-code-build
Version:
Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat
341 lines (289 loc) • 11.1 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
const execAsync = promisify(exec);
export class DependencyChecker {
constructor(projectPath = process.cwd(), options = {}) {
this.projectPath = projectPath;
this.options = options;
this.results = {
vulnerabilities: null,
outdated: null,
unused: null,
missing: null
};
}
async checkAll() {
const spinner = this.options.silent ? null : ora('Running dependency checks...').start();
try {
if (spinner) spinner.text = 'Checking for vulnerabilities...';
this.results.vulnerabilities = await this.checkVulnerabilities();
if (spinner) spinner.text = 'Checking for outdated dependencies...';
this.results.outdated = await this.checkOutdated();
if (spinner) spinner.text = 'Checking for unused dependencies...';
this.results.unused = await this.checkUnused();
if (spinner) spinner.text = 'Checking for missing dependencies...';
this.results.missing = await this.checkMissing();
if (spinner) spinner.succeed('Dependency checks completed');
return this.results;
} catch (error) {
if (spinner) spinner.fail('Dependency checks failed');
throw error;
}
}
async checkVulnerabilities() {
try {
const { stdout } = await execAsync('npm audit --json', {
cwd: this.projectPath
});
const audit = JSON.parse(stdout);
const vulnerabilities = audit.vulnerabilities || {};
const severityCounts = {
critical: 0,
high: 0,
moderate: 0,
low: 0,
info: 0
};
Object.values(vulnerabilities).forEach(vuln => {
if (vuln.severity && severityCounts.hasOwnProperty(vuln.severity)) {
severityCounts[vuln.severity]++;
}
});
const total = Object.values(severityCounts).reduce((sum, count) => sum + count, 0);
return {
status: severityCounts.critical > 0 || severityCounts.high > 0 ? 'failed' :
total > 0 ? 'warning' : 'passed',
message: total > 0 ? `Found ${total} vulnerabilities` : 'No vulnerabilities found',
severityCounts,
total,
vulnerabilities: Object.entries(vulnerabilities).map(([name, vuln]) => ({
name,
severity: vuln.severity,
via: vuln.via,
fixAvailable: vuln.fixAvailable
}))
};
} catch (error) {
if (error.stdout) {
try {
const audit = JSON.parse(error.stdout);
const total = audit.metadata?.vulnerabilities ?
Object.values(audit.metadata.vulnerabilities).reduce((sum, count) => sum + count, 0) : 0;
return {
status: 'failed',
message: `Found ${total} vulnerabilities`,
severityCounts: audit.metadata?.vulnerabilities || {},
total
};
} catch {}
}
return {
status: 'error',
message: 'Vulnerability check failed',
error: error.message
};
}
}
async checkOutdated() {
try {
const { stdout } = await execAsync('npm outdated --json', {
cwd: this.projectPath
});
const outdated = stdout ? JSON.parse(stdout) : {};
const outdatedCount = Object.keys(outdated).length;
const dependencies = Object.entries(outdated).map(([name, info]) => ({
name,
current: info.current,
wanted: info.wanted,
latest: info.latest,
type: info.type,
majorBehind: info.latest && info.current ?
parseInt(info.latest.split('.')[0]) - parseInt(info.current.split('.')[0]) : 0
}));
const majorUpdates = dependencies.filter(dep => dep.majorBehind > 0).length;
return {
status: majorUpdates > 5 ? 'warning' : 'passed',
message: outdatedCount > 0 ?
`Found ${outdatedCount} outdated dependencies (${majorUpdates} major updates)` :
'All dependencies are up to date',
total: outdatedCount,
majorUpdates,
dependencies
};
} catch (error) {
if (error.stdout) {
try {
const outdated = JSON.parse(error.stdout);
const outdatedCount = Object.keys(outdated).length;
return {
status: 'warning',
message: `Found ${outdatedCount} outdated dependencies`,
total: outdatedCount,
dependencies: Object.entries(outdated).map(([name, info]) => ({
name,
current: info.current,
wanted: info.wanted,
latest: info.latest
}))
};
} catch {}
}
return {
status: 'skipped',
message: 'No outdated dependencies found',
total: 0
};
}
}
async checkUnused() {
try {
const packageJsonPath = path.join(this.projectPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const allDeps = {
...packageJson.dependencies || {},
...packageJson.devDependencies || {}
};
const jsFiles = await this.findJSFiles();
const usedDeps = new Set();
for (const file of jsFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const requireMatches = content.matchAll(/require\(['"]([^'"]+)['"]\)/g);
const importMatches = content.matchAll(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g);
const dynamicImportMatches = content.matchAll(/import\(['"]([^'"]+)['"]\)/g);
for (const match of [...requireMatches, ...importMatches, ...dynamicImportMatches]) {
const dep = match[1].split('/')[0].replace('@', '');
if (allDeps[dep] || allDeps[`@${dep}`]) {
usedDeps.add(dep.startsWith('@') ? dep : match[1].split('/')[0]);
}
}
} catch {}
}
const unusedDeps = Object.keys(allDeps).filter(dep =>
!usedDeps.has(dep) &&
!dep.includes('eslint') &&
!dep.includes('prettier') &&
!dep.includes('types') &&
dep !== 'vibe-code'
);
return {
status: unusedDeps.length > 10 ? 'warning' : 'passed',
message: unusedDeps.length > 0 ?
`Found ${unusedDeps.length} potentially unused dependencies` :
'All dependencies appear to be used',
total: unusedDeps.length,
dependencies: unusedDeps
};
} catch (error) {
return {
status: 'error',
message: 'Unused dependency check failed',
error: error.message
};
}
}
async checkMissing() {
try {
const jsFiles = await this.findJSFiles();
const missingDeps = new Set();
const packageJsonPath = path.join(this.projectPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const allDeps = {
...packageJson.dependencies || {},
...packageJson.devDependencies || {}
};
for (const file of jsFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const requireMatches = content.matchAll(/require\(['"]([^'"]+)['"]\)/g);
const importMatches = content.matchAll(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g);
for (const match of [...requireMatches, ...importMatches]) {
const dep = match[1];
if (!dep.startsWith('.') && !dep.startsWith('/') && !dep.includes('node:')) {
const packageName = dep.startsWith('@') ?
dep.split('/').slice(0, 2).join('/') :
dep.split('/')[0];
if (!allDeps[packageName] && !this.isBuiltinModule(packageName)) {
missingDeps.add(packageName);
}
}
}
} catch {}
}
return {
status: missingDeps.size > 0 ? 'failed' : 'passed',
message: missingDeps.size > 0 ?
`Found ${missingDeps.size} missing dependencies` :
'All required dependencies are installed',
total: missingDeps.size,
dependencies: Array.from(missingDeps)
};
} catch (error) {
return {
status: 'error',
message: 'Missing dependency check failed',
error: error.message
};
}
}
async findJSFiles() {
const files = [];
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
async function scan(dir) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() &&
!entry.name.startsWith('.') &&
entry.name !== 'node_modules' &&
entry.name !== 'dist' &&
entry.name !== 'build') {
await scan(fullPath);
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
} catch {}
}
await scan(this.projectPath);
return files;
}
isBuiltinModule(name) {
const builtins = [
'fs', 'path', 'http', 'https', 'crypto', 'os', 'util', 'stream',
'buffer', 'child_process', 'cluster', 'dgram', 'dns', 'events',
'net', 'querystring', 'readline', 'repl', 'tls', 'tty', 'url',
'v8', 'vm', 'zlib', 'assert', 'console', 'constants', 'domain',
'process', 'punycode', 'string_decoder', 'timers', 'worker_threads'
];
return builtins.includes(name);
}
formatResults() {
const sections = [];
for (const [check, result] of Object.entries(this.results)) {
if (!result) continue;
const icon = result.status === 'passed' ? '✅' :
result.status === 'failed' ? '❌' :
result.status === 'warning' ? '⚠️' :
result.status === 'skipped' ? '⏭️' : '❓';
const color = result.status === 'passed' ? chalk.green :
result.status === 'failed' ? chalk.red :
result.status === 'warning' ? chalk.yellow :
result.status === 'skipped' ? chalk.gray : chalk.gray;
sections.push(color(`${icon} ${check.toUpperCase()}: ${result.message}`));
if (check === 'vulnerabilities' && result.severityCounts) {
const sevs = result.severityCounts;
if (sevs.critical > 0) sections.push(chalk.red(` Critical: ${sevs.critical}`));
if (sevs.high > 0) sections.push(chalk.red(` High: ${sevs.high}`));
if (sevs.moderate > 0) sections.push(chalk.yellow(` Moderate: ${sevs.moderate}`));
if (sevs.low > 0) sections.push(chalk.gray(` Low: ${sevs.low}`));
}
}
return sections.join('\n');
}
}