vibe-code-build
Version:
Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat
404 lines (352 loc) • 13 kB
JavaScript
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');
}
}