UNPKG

vibe-code-build

Version:

Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat

404 lines (352 loc) 13 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import ora from 'ora'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); export class ClaudeChecker { constructor(projectPath = process.cwd(), options = {}) { this.projectPath = projectPath; this.options = options; this.results = { claudeMdExists: null, structure: null, commands: null, compliance: null, aiPatterns: null }; } async checkAll() { const spinner = this.options.silent ? null : ora('Running CLAUDE.md checks...').start(); try { if (spinner) spinner.text = 'Checking CLAUDE.md presence...'; this.results.claudeMdExists = await this.checkClaudeMdExists(); if (this.results.claudeMdExists.status === 'passed') { if (spinner) spinner.text = 'Validating CLAUDE.md structure...'; this.results.structure = await this.checkStructure(); if (spinner) spinner.text = 'Testing documented commands...'; this.results.commands = await this.checkCommands(); if (spinner) spinner.text = 'Checking codebase compliance...'; this.results.compliance = await this.checkCompliance(); } if (spinner) spinner.text = 'Checking for AI patterns...'; this.results.aiPatterns = await this.checkAIPatterns(); if (spinner) spinner.succeed('CLAUDE.md checks completed'); return this.results; } catch (error) { if (spinner) spinner.fail('CLAUDE.md checks failed'); throw error; } } async checkClaudeMdExists() { try { const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md'); const stats = await fs.stat(claudeMdPath); if (stats.size < 100) { return { status: 'warning', message: 'CLAUDE.md exists but appears to be empty or minimal', size: stats.size }; } return { status: 'passed', message: 'CLAUDE.md file exists', size: stats.size, path: claudeMdPath }; } catch (error) { return { status: 'failed', message: 'CLAUDE.md file not found', recommendation: 'Create a CLAUDE.md file to guide AI assistants working with your codebase' }; } } async checkStructure() { try { const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md'); const content = await fs.readFile(claudeMdPath, 'utf8'); const requiredSections = [ { pattern: /#+\s*(Overview|Introduction|About)/i, name: 'Overview' }, { pattern: /#+\s*(Development|Commands|Scripts)/i, name: 'Development Commands' }, { pattern: /#+\s*(Architecture|Structure)/i, name: 'Architecture' } ]; const recommendedSections = [ { pattern: /#+\s*(Testing|Tests)/i, name: 'Testing' }, { pattern: /#+\s*(API|Endpoints)/i, name: 'API Documentation' }, { pattern: /#+\s*(Configuration|Config|Environment)/i, name: 'Configuration' }, { pattern: /#+\s*(Deployment|Deploy)/i, name: 'Deployment' } ]; const foundRequired = requiredSections.filter(section => section.pattern.test(content) ); const foundRecommended = recommendedSections.filter(section => section.pattern.test(content) ); const missingRequired = requiredSections.filter(section => !section.pattern.test(content) ); return { status: missingRequired.length > 0 ? 'warning' : 'passed', message: missingRequired.length > 0 ? `Missing ${missingRequired.length} required sections` : 'All required sections present', requiredSections: { found: foundRequired.map(s => s.name), missing: missingRequired.map(s => s.name) }, recommendedSections: { found: foundRecommended.map(s => s.name), missing: recommendedSections.filter(s => !foundRecommended.includes(s)).map(s => s.name) }, lineCount: content.split('\n').length }; } catch (error) { return { status: 'error', message: 'Failed to check CLAUDE.md structure', error: error.message }; } } async checkCommands() { try { const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md'); const content = await fs.readFile(claudeMdPath, 'utf8'); const codeBlocks = content.match(/```(?:bash|sh|shell)?\n([\s\S]*?)```/g) || []; const commands = []; for (const block of codeBlocks) { const code = block.replace(/```(?:bash|sh|shell)?\n/, '').replace(/```$/, ''); const lines = code.split('\n').filter(line => line.trim() && !line.trim().startsWith('#') && (line.includes('npm') || line.includes('yarn') || line.includes('pnpm')) ); commands.push(...lines); } const testedCommands = []; const failedCommands = []; // Only test first 3 commands to avoid long delays for (const command of commands.slice(0, 3)) { try { const testCommand = command.replace(/&&.*$/, '').trim(); if (testCommand.includes('run') || testCommand.includes('test') || testCommand.includes('build')) { // Set a shorter timeout await execAsync(`${testCommand} --help`, { cwd: this.projectPath, timeout: 5000 // 5 second timeout }); testedCommands.push(command); } } catch { failedCommands.push(command); } } return { status: failedCommands.length > commands.length / 2 ? 'warning' : 'passed', message: `Found ${commands.length} commands, tested ${testedCommands.length + failedCommands.length}`, totalCommands: commands.length, testedCommands: testedCommands.length, failedCommands: failedCommands.length, examples: commands.slice(0, 3) }; } catch (error) { return { status: 'error', message: 'Failed to check commands', error: error.message }; } } async checkCompliance() { try { const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md'); const content = await fs.readFile(claudeMdPath, 'utf8'); const rules = this.extractRules(content); const violations = []; for (const rule of rules) { if (rule.includes('naming convention')) { const jsFiles = await this.findFiles(['.js', '.jsx', '.ts', '.tsx']); for (const file of jsFiles.slice(0, 10)) { const filename = path.basename(file); if (rule.includes('camelCase') && !/^[a-z][a-zA-Z0-9]*\.(js|jsx|ts|tsx)$/.test(filename)) { violations.push({ rule: 'Naming convention', file, issue: 'File not in camelCase' }); } } } if (rule.includes('no console.log')) { const jsFiles = await this.findFiles(['.js', '.jsx', '.ts', '.tsx']); for (const file of jsFiles.slice(0, 10)) { try { const code = await fs.readFile(file, 'utf8'); if (code.includes('console.log')) { violations.push({ rule: 'No console.log', file, issue: 'Contains console.log statements' }); } } catch {} } } } return { status: violations.length > 5 ? 'warning' : 'passed', message: violations.length > 0 ? `Found ${violations.length} compliance violations` : 'Codebase complies with CLAUDE.md rules', rulesFound: rules.length, violations: violations.slice(0, 5), totalViolations: violations.length }; } catch (error) { return { status: 'error', message: 'Failed to check compliance', error: error.message }; } } async checkAIPatterns() { try { const patterns = { godMode: [ /sudo\s+rm\s+-rf\s+\//, /process\.exit\(\)/, /require\(['"]child_process['"]\)\.exec\(/, /eval\(/, /Function\(/, /\.writeFileSync\(['"]\/etc\// ], suspicious: [ /OPENAI_API_KEY/, /sk-[a-zA-Z0-9]{48}/, /Bearer\s+[a-zA-Z0-9-._~+/]+=*/, /password\s*=\s*["'][^"']+["']/i, /api[_-]?key\s*=\s*["'][^"']+["']/i ], aiGenerated: [ /Generated by AI/i, /This code was automatically generated/i, /\[AI-GENERATED\]/, /Created by Claude/i, /Generated with ChatGPT/i ] }; const findings = { godMode: [], suspicious: [], aiGenerated: [] }; const jsFiles = await this.findFiles(['.js', '.jsx', '.ts', '.tsx']); for (const file of jsFiles.slice(0, 20)) { try { const content = await fs.readFile(file, 'utf8'); for (const [category, categoryPatterns] of Object.entries(patterns)) { for (const pattern of categoryPatterns) { if (pattern.test(content)) { const match = content.match(pattern); findings[category].push({ file, pattern: pattern.toString(), match: match ? match[0].substring(0, 50) : '' }); } } } } catch {} } const totalFindings = Object.values(findings).reduce((sum, arr) => sum + arr.length, 0); return { status: findings.godMode.length > 0 ? 'failed' : findings.suspicious.length > 3 ? 'warning' : 'passed', message: totalFindings > 0 ? `Found ${totalFindings} AI-related patterns` : 'No concerning AI patterns detected', findings: { godMode: findings.godMode.slice(0, 3), suspicious: findings.suspicious.slice(0, 3), aiGenerated: findings.aiGenerated.slice(0, 3) }, totals: { godMode: findings.godMode.length, suspicious: findings.suspicious.length, aiGenerated: findings.aiGenerated.length } }; } catch (error) { return { status: 'error', message: 'Failed to check AI patterns', error: error.message }; } } extractRules(content) { const rules = []; const rulePatterns = [ /must\s+([^.]+)/gi, /should\s+([^.]+)/gi, /always\s+([^.]+)/gi, /never\s+([^.]+)/gi, /rule:\s*([^.\n]+)/gi ]; for (const pattern of rulePatterns) { const matches = content.matchAll(pattern); for (const match of matches) { rules.push(match[1].trim()); } } return rules.slice(0, 10); } async findFiles(extensions) { const files = []; 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; } 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' ? '⚠️' : '❓'; const color = result.status === 'passed' ? chalk.green : result.status === 'failed' ? chalk.red : result.status === 'warning' ? chalk.yellow : chalk.gray; sections.push(color(`${icon} ${check.replace(/([A-Z])/g, ' $1').toUpperCase()}: ${result.message}`)); if (check === 'aiPatterns' && result.totals) { if (result.totals.godMode > 0) { sections.push(chalk.red(` ⚠️ God Mode Patterns: ${result.totals.godMode}`)); } if (result.totals.suspicious > 0) { sections.push(chalk.yellow(` ⚠️ Suspicious Patterns: ${result.totals.suspicious}`)); } } } return sections.join('\n'); } }