quality-check.js
1 #!/usr/bin/env node 2 3 /** 4 * Quality Check Script 5 * Runs all quality checks and outputs results in JSON format for Cline integration 6 */ 7 8 import { execSync } from 'child_process'; 9 import { writeFileSync, mkdirSync, existsSync } from 'fs'; 10 import { join } from 'path'; 11 import { fileURLToPath } from 'url'; 12 import { dirname } from 'path'; 13 14 const __filename = fileURLToPath(import.meta.url); 15 const __dirname = dirname(__filename); 16 const projectRoot = join(__dirname, '..'); 17 const outputDir = join(projectRoot, '.quality-reports'); 18 19 // Ensure output directory exists 20 if (!existsSync(outputDir)) { 21 mkdirSync(outputDir, { recursive: true }); 22 } 23 24 console.log('๐ Running comprehensive quality checks...\n'); 25 26 const results = { 27 timestamp: new Date().toISOString(), 28 passed: true, 29 checks: {}, 30 }; 31 32 /** 33 * Run a quality check and capture results 34 */ 35 // Safe: command args are hardcoded strings, not user input; file paths from known directories 36 /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */ 37 // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process 38 function runCheck(name, command, options = {}) { 39 const { 40 outputFile = null, 41 required = true, 42 successMessage = `โ ${name} passed`, 43 failureMessage = `โ ${name} failed`, 44 } = options; 45 46 try { 47 const output = execSync(command, { 48 // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process 49 cwd: projectRoot, 50 encoding: 'utf-8', 51 stdio: outputFile ? 'pipe' : 'inherit', 52 }); 53 54 if (outputFile && output) { 55 writeFileSync(join(outputDir, outputFile), output); 56 } 57 58 console.log(successMessage); 59 results.checks[name] = { 60 status: 'passed', 61 output: outputFile ? `.quality-reports/${outputFile}` : null, 62 }; 63 64 return true; 65 } catch (error) { 66 console.error(failureMessage); 67 68 if (error.stdout && outputFile) { 69 writeFileSync(join(outputDir, outputFile), error.stdout); 70 } 71 72 results.checks[name] = { 73 status: 'failed', 74 output: outputFile ? `.quality-reports/${outputFile}` : null, 75 error: error.message, 76 }; 77 78 if (required) { 79 results.passed = false; 80 } 81 82 return false; 83 } 84 } 85 86 // 1. ESLint 87 console.log('๐ Running ESLint...'); 88 runCheck('eslint', './node_modules/.bin/eslint src/ scripts/ --format=json', { 89 outputFile: 'eslint.json', 90 required: true, 91 successMessage: 'โ ESLint: No errors found', 92 failureMessage: 'โ ๏ธ ESLint: Issues found (see .quality-reports/eslint.json)', 93 }); 94 95 // 2. Prettier 96 console.log('\n๐ Checking code formatting...'); 97 runCheck('prettier', './node_modules/.bin/prettier --check "src/**/*.js" "scripts/**/*.js"', { 98 required: false, 99 successMessage: 'โ Prettier: Code is formatted correctly', 100 failureMessage: 'โ ๏ธ Prettier: Code needs formatting (run: npm run format)', 101 }); 102 103 // 3. Unit Tests 104 console.log('\n๐งช Running tests...'); 105 const testsPassed = runCheck('tests', 'node --test src/**/*.test.js 2>&1', { 106 outputFile: 'tests.txt', 107 required: true, 108 successMessage: 'โ Tests: All passing', 109 failureMessage: 'โ Tests: Some tests failed', 110 }); 111 112 // 4. Coverage (only if tests passed) 113 if (testsPassed) { 114 console.log('\n๐ Generating coverage report...'); 115 runCheck( 116 'coverage', 117 './node_modules/.bin/c8 --reporter=json-summary --reporter=text node --test src/**/*.test.js 2>&1', 118 { 119 outputFile: 'coverage.txt', 120 required: false, 121 successMessage: 'โ Coverage: Report generated', 122 failureMessage: 'โ ๏ธ Coverage: Could not generate report', 123 } 124 ); 125 126 // Read coverage summary if it exists 127 try { 128 const coveragePath = join(projectRoot, 'coverage', 'coverage-summary.json'); 129 if (existsSync(coveragePath)) { 130 const { readFileSync } = await import('fs'); 131 const coverage = JSON.parse(readFileSync(coveragePath, 'utf-8')); 132 const totalPct = coverage.total.lines.pct; 133 134 results.checks.coverage.percentage = totalPct; 135 136 if (totalPct >= 70) { 137 console.log(` ๐ Coverage: ${totalPct.toFixed(1)}% (โ target: 70%)`); 138 } else { 139 console.log(` ๐ Coverage: ${totalPct.toFixed(1)}% (โ ๏ธ target: 70%)`); 140 results.checks.coverage.warning = 'Coverage below 70% target'; 141 } 142 } 143 } catch { 144 // Coverage summary not available 145 } 146 } 147 148 // 5. Sage AI Review (optional - only if installed) 149 console.log('\n๐ค Checking for Sage AI review...'); 150 const sagePath = join(projectRoot, 'node_modules', '@tigtech', 'sage'); 151 if (existsSync(sagePath)) { 152 runCheck('sage', './node_modules/.bin/sage review --format json 2>&1', { 153 outputFile: 'sage.json', 154 required: false, 155 successMessage: 'โ Sage: AI review complete', 156 failureMessage: 'โ ๏ธ Sage: Review generated warnings (see .quality-reports/sage.json)', 157 }); 158 } else { 159 console.log('โน๏ธ Sage not installed (optional - install with: npm install -D @tigtech/sage)'); 160 results.checks.sage = { status: 'not_installed' }; 161 } 162 163 // Write summary 164 writeFileSync(join(outputDir, 'summary.json'), JSON.stringify(results, null, 2)); 165 166 console.log(`\n${'='.repeat(60)}`); 167 console.log('\n๐ Quality Check Summary'); 168 console.log('='.repeat(60)); 169 console.log(`\nTimestamp: ${results.timestamp}`); 170 console.log(`Overall Status: ${results.passed ? 'โ PASSED' : 'โ FAILED'}\n`); 171 172 Object.entries(results.checks).forEach(([name, check]) => { 173 const icon = 174 check.status === 'passed' 175 ? 'โ ' 176 : check.status === 'failed' 177 ? 'โ' 178 : check.status === 'not_installed' 179 ? 'โน๏ธ' 180 : 'โ ๏ธ'; 181 console.log(`${icon} ${name}: ${check.status}`); 182 if (check.output) { 183 console.log(` ๐ Details: ${check.output}`); 184 } 185 }); 186 187 console.log(`\n${'='.repeat(60)}`); 188 console.log('\n๐ก Cline Integration:'); 189 console.log(' Cline can read .quality-reports/summary.json to auto-fix issues'); 190 console.log(' Tell Cline: "Fix the issues in .quality-reports/"'); 191 console.log(`\n${'='.repeat(60)}\n`); 192 193 // Exit with appropriate code 194 process.exit(results.passed ? 0 : 1);