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