UNPKG

vibe-code-build

Version:

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

247 lines (210 loc) 7.43 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 BuildChecker { constructor(projectPath = process.cwd(), options = {}) { this.projectPath = projectPath; this.options = options; this.results = { build: null, typescript: null, linting: null, tests: null }; } async checkAll() { const spinner = this.options.silent ? null : ora('Running build checks...').start(); try { if (spinner) spinner.text = 'Checking build process...'; this.results.build = await this.checkBuild(); if (spinner) spinner.text = 'Checking TypeScript...'; this.results.typescript = await this.checkTypeScript(); if (spinner) spinner.text = 'Checking linting...'; this.results.linting = await this.checkLinting(); if (spinner) spinner.text = 'Checking tests...'; this.results.tests = await this.checkTests(); if (spinner) spinner.succeed('Build checks completed'); return this.results; } catch (error) { if (spinner) spinner.fail('Build checks failed'); throw error; } } async checkBuild() { try { const packageJsonPath = path.join(this.projectPath, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); if (!packageJson.scripts?.build) { return { status: 'skipped', message: 'No build script found in package.json' }; } const { stdout, stderr } = await execAsync('npm run build', { cwd: this.projectPath, env: { ...process.env, CI: 'true' } }); return { status: 'passed', message: 'Build completed successfully', output: stdout, warnings: stderr ? stderr.split('\n').filter(line => line.includes('warning')) : [] }; } catch (error) { return { status: 'failed', message: 'Build failed', error: error.message, output: error.stdout, stderr: error.stderr }; } } async checkTypeScript() { try { const tsconfigPath = path.join(this.projectPath, 'tsconfig.json'); try { await fs.access(tsconfigPath); } catch { return { status: 'skipped', message: 'No tsconfig.json found' }; } const { stdout, stderr } = await execAsync('npx tsc --noEmit', { cwd: this.projectPath }); const errors = stderr ? stderr.split('\n').filter(line => line.includes('error')) : []; const warnings = stderr ? stderr.split('\n').filter(line => line.includes('warning')) : []; return { status: errors.length > 0 ? 'failed' : 'passed', message: errors.length > 0 ? `Found ${errors.length} TypeScript errors` : 'TypeScript check passed', errors: errors.length, warnings: warnings.length, output: stdout + stderr }; } catch (error) { if (error.code === 2) { const errorCount = (error.stdout + error.stderr).match(/error TS/g)?.length || 0; return { status: 'failed', message: `Found ${errorCount} TypeScript errors`, errors: errorCount, output: error.stdout + error.stderr }; } return { status: 'error', message: 'TypeScript check failed', error: error.message }; } } async checkLinting() { try { const eslintConfigFiles = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']; let hasEslintConfig = false; for (const configFile of eslintConfigFiles) { try { await fs.access(path.join(this.projectPath, configFile)); hasEslintConfig = true; break; } catch {} } if (!hasEslintConfig) { return { status: 'skipped', message: 'No ESLint configuration found' }; } const { stdout, stderr } = await execAsync('npx eslint . --ext .js,.jsx,.ts,.tsx --format json', { cwd: this.projectPath }); const results = JSON.parse(stdout); const errorCount = results.reduce((sum, file) => sum + file.errorCount, 0); const warningCount = results.reduce((sum, file) => sum + file.warningCount, 0); return { status: errorCount > 0 ? 'failed' : 'passed', message: errorCount > 0 ? `Found ${errorCount} linting errors` : 'Linting passed', errors: errorCount, warnings: warningCount, files: results.filter(file => file.errorCount > 0 || file.warningCount > 0) }; } catch (error) { if (error.stdout) { try { const results = JSON.parse(error.stdout); const errorCount = results.reduce((sum, file) => sum + file.errorCount, 0); const warningCount = results.reduce((sum, file) => sum + file.warningCount, 0); return { status: 'failed', message: `Found ${errorCount} linting errors`, errors: errorCount, warnings: warningCount, files: results.filter(file => file.errorCount > 0 || file.warningCount > 0) }; } catch {} } return { status: 'error', message: 'Linting check failed', error: error.message }; } } async checkTests() { try { const packageJsonPath = path.join(this.projectPath, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); if (!packageJson.scripts?.test) { return { status: 'skipped', message: 'No test script found in package.json' }; } const { stdout, stderr } = await execAsync('npm test', { cwd: this.projectPath, env: { ...process.env, CI: 'true' } }); const isPassing = !stderr.includes('failed') && !stdout.includes('failed'); return { status: isPassing ? 'passed' : 'failed', message: isPassing ? 'All tests passed' : 'Some tests failed', output: stdout, stderr: stderr }; } catch (error) { return { status: 'failed', message: 'Tests failed', error: error.message, output: error.stdout, stderr: error.stderr }; } } 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 === 'skipped' ? '⏭️' : '⚠️'; const color = result.status === 'passed' ? chalk.green : result.status === 'failed' ? chalk.red : result.status === 'skipped' ? chalk.gray : chalk.yellow; sections.push(color(`${icon} ${check.toUpperCase()}: ${result.message}`)); if (result.errors > 0) { sections.push(chalk.red(` Errors: ${result.errors}`)); } if (result.warnings > 0) { sections.push(chalk.yellow(` Warnings: ${result.warnings}`)); } } return sections.join('\n'); } }