UNPKG

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
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'); } }