UNPKG

claudewatch

Version:

Automated validation and self-healing system for Claude Code projects

299 lines (254 loc) 9.74 kB
const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const chalk = require('chalk'); const ora = require('ora'); const CLAUDEWATCH_DIR = '.claudewatch'; const CLAUDE_DIR = '.claude'; async function initProject(options = {}) { const config = { baseUrl: options.baseUrl || 'http://localhost:3000', selfHealing: options.selfHealing !== false, validationTypes: options.validationTypes || ['visual', 'accessibility', 'performance', 'forms', 'api', 'console', 'links'], ...options }; const dirs = [CLAUDEWATCH_DIR, CLAUDE_DIR, 'validation-logs', 'validation-screenshots']; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); console.log(chalk.green(`Created ${dir}/`)); } }); const templateDir = path.join(__dirname, '..', 'templates'); const configTemplate = fs.readFileSync(path.join(templateDir, 'config.js.template'), 'utf-8'); const configContent = configTemplate .replace('{{BASE_URL}}', config.baseUrl) .replace('{{VALIDATION_TYPES}}', JSON.stringify(config.validationTypes)); fs.writeFileSync(path.join(CLAUDEWATCH_DIR, 'config.js'), configContent); console.log(chalk.green('Created validation config')); const validatorTemplate = fs.readFileSync(path.join(templateDir, 'validator.js.template'), 'utf-8'); fs.writeFileSync(path.join(CLAUDEWATCH_DIR, 'validator.js'), validatorTemplate); console.log(chalk.green('Created validation script')); if (config.selfHealing) { const healerTemplate = fs.readFileSync(path.join(templateDir, 'self-healer.js.template'), 'utf-8'); fs.writeFileSync(path.join(CLAUDEWATCH_DIR, 'self-healer.js'), healerTemplate); console.log(chalk.green('Created self-healing script')); } const hookCommand = config.selfHealing ? 'node .claudewatch/self-healer.js' : 'node .claudewatch/validator.js'; const claudeSettings = { hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCommand, timeout: config.selfHealing ? 300000 : 120000, retries: 1 }] }], PostToolUse: [{ matcher: "Bash|Edit|Write|MultiEdit", hooks: [{ type: "command", command: "echo 'Code changes detected - ClaudeWatch validation recommended'", timeout: 5000, retries: 0 }] }] } }; fs.writeFileSync( path.join(CLAUDE_DIR, 'settings.json'), JSON.stringify(claudeSettings, null, 2) ); console.log(chalk.green('Created Claude settings')); const claudeMdTemplate = fs.readFileSync(path.join(templateDir, 'CLAUDE.md.template'), 'utf-8'); const claudeMdContent = claudeMdTemplate .replace(/{{SELF_HEALING}}/g, config.selfHealing ? 'enabled' : 'disabled'); fs.writeFileSync('CLAUDE.md', claudeMdContent); console.log(chalk.green('Created CLAUDE.md instructions')); const gitignoreEntries = [ '\n# ClaudeWatch', 'validation-logs/', 'validation-screenshots/', '.claudewatch/logs/', '' ].join('\n'); if (fs.existsSync('.gitignore')) { const gitignore = fs.readFileSync('.gitignore', 'utf-8'); if (!gitignore.includes('# ClaudeWatch')) { fs.appendFileSync('.gitignore', gitignoreEntries); console.log(chalk.green('Updated .gitignore')); } } else { fs.writeFileSync('.gitignore', gitignoreEntries); console.log(chalk.green('Created .gitignore')); } const spinner = ora('Installing Playwright...').start(); try { execSync('npx playwright install chromium', { stdio: 'ignore' }); spinner.succeed('Playwright installed'); } catch (error) { spinner.fail('Failed to install Playwright'); console.log(chalk.yellow('Run `npx playwright install` manually')); } console.log(chalk.green('\nClaudeWatch initialized successfully!')); if (config.claudeOptimized) { console.log(chalk.blue('\nClaude-specific optimizations applied:')); console.log(' - Self-healing enabled by default'); console.log(' - Automatic validation on task completion'); console.log(' - Instructions added to CLAUDE.md'); } console.log(chalk.gray('\nNext steps:')); console.log(' 1. Edit .claudewatch/config.js to customize validations'); console.log(' 2. Run `claudewatch validate` to test'); console.log(' 3. Claude will now automatically validate your work!'); } async function runValidation(configPath) { const config = configPath || path.join(CLAUDEWATCH_DIR, 'config.js'); const validatorPath = path.join(CLAUDEWATCH_DIR, 'validator.js'); if (!fs.existsSync(validatorPath)) { throw new Error('Validator not found. Run `claudewatch init` first.'); } try { execSync(`node ${validatorPath} ${config}`, { stdio: 'inherit' }); return { success: true }; } catch (error) { return { success: false, error }; } } async function runSelfHealing(options = {}) { const healerPath = path.join(CLAUDEWATCH_DIR, 'self-healer.js'); if (!fs.existsSync(healerPath)) { throw new Error('Self-healer not found. Run `claudewatch init` with self-healing enabled.'); } try { const env = { ...process.env, MAX_ATTEMPTS: options.maxAttempts || 5 }; execSync(`node ${healerPath}`, { stdio: 'inherit', env }); return { success: true }; } catch (error) { return { success: false, error }; } } async function showStatus() { console.log(chalk.blue('\nClaudeWatch Status\n')); const checks = [ { name: 'Config file', path: path.join(CLAUDEWATCH_DIR, 'config.js') }, { name: 'Validator script', path: path.join(CLAUDEWATCH_DIR, 'validator.js') }, { name: 'Self-healer script', path: path.join(CLAUDEWATCH_DIR, 'self-healer.js') }, { name: 'Claude settings', path: path.join(CLAUDE_DIR, 'settings.json') }, { name: 'Instructions', path: 'CLAUDE.md' } ]; checks.forEach(check => { const exists = fs.existsSync(check.path); const status = exists ? chalk.green('[OK]') : chalk.red('[MISSING]'); console.log(`${status} ${check.name}`); }); const configPath = path.join(CLAUDEWATCH_DIR, 'config.js'); if (fs.existsSync(configPath)) { console.log(chalk.blue('\nConfiguration:')); try { delete require.cache[require.resolve(path.resolve(configPath))]; const config = require(path.resolve(configPath)); console.log(` Base URL: ${config.baseUrl}`); console.log(` Pages: ${config.pages.length}`); console.log(` Viewports: ${config.viewports.map(v => v.name).join(', ')}`); } catch (error) { console.log(chalk.red(' Error reading config')); } } console.log(chalk.blue('\nRecent Activity:')); const logsDir = 'validation-logs'; if (fs.existsSync(logsDir)) { const logs = fs.readdirSync(logsDir) .filter(f => f.startsWith('report-')) .sort() .slice(-5); if (logs.length > 0) { logs.forEach(log => { const stat = fs.statSync(path.join(logsDir, log)); const time = stat.mtime.toLocaleString(); console.log(` ${time} - ${log}`); }); } else { console.log(' No validation logs found'); } } } async function generateReport(count = 10) { console.log(chalk.blue('\nValidation Report\n')); const logsDir = 'validation-logs'; if (!fs.existsSync(logsDir)) { console.log(chalk.yellow('No validation logs found')); return; } const reports = fs.readdirSync(logsDir) .filter(f => f.startsWith('report-')) .sort() .slice(-count); if (reports.length === 0) { console.log(chalk.yellow('No reports found')); return; } let totalTests = 0; let totalPassed = 0; let totalFailed = 0; reports.forEach(file => { try { const report = JSON.parse(fs.readFileSync(path.join(logsDir, file), 'utf-8')); const { timestamp, total, passed, failed } = report.summary; const time = new Date(timestamp).toLocaleString(); const status = failed === 0 ? chalk.green('[PASS]') : chalk.red('[FAIL]'); console.log(`${status} ${time} - ${passed}/${total} passed`); totalTests += total; totalPassed += passed; totalFailed += failed; } catch (error) { console.log(chalk.red(`[INVALID] ${file} - Invalid report`)); } }); console.log(chalk.blue('\nSummary:')); console.log(` Total validations: ${reports.length}`); console.log(` Tests run: ${totalTests}`); console.log(` Passed: ${totalPassed}`); console.log(` Failed: ${totalFailed}`); console.log(` Success rate: ${totalTests > 0 ? ((totalPassed / totalTests) * 100).toFixed(1) : 0}%`); } async function cleanup(options = {}) { const { keepDays = 7, dryRun = false } = options; const cutoffDate = new Date(Date.now() - (keepDays * 24 * 60 * 60 * 1000)); console.log(chalk.blue(`\nCleaning files older than ${keepDays} days...\n`)); const dirs = ['validation-logs', 'validation-screenshots']; let totalDeleted = 0; dirs.forEach(dir => { if (!fs.existsSync(dir)) return; const files = fs.readdirSync(dir).map(file => ({ name: file, path: path.join(dir, file), stat: fs.statSync(path.join(dir, file)) })).filter(file => file.stat.mtime < cutoffDate); console.log(`${dir}: ${files.length} old files`); if (!dryRun) { files.forEach(file => fs.unlinkSync(file.path)); } totalDeleted += files.length; }); if (dryRun) { console.log(chalk.yellow(`\nDry run: Would delete ${totalDeleted} files`)); } else { console.log(chalk.green(`\nDeleted ${totalDeleted} files`)); } } module.exports = { initProject, runValidation, runSelfHealing, showStatus, generateReport, cleanup };