sage-auto-fix.js
1 #!/usr/bin/env node 2 3 /** 4 * Sage Auto-Fix Script 5 * 6 * Runs Sage AI review, then uses Claude CLI to automatically fix identified issues. 7 * Creates fixes in a review branch for safety (doesn't commit directly to main). 8 * 9 * Workflow: 10 * 1. Run quality-check to generate reports 11 * 2. Parse ESLint, Prettier, Sage findings 12 * 3. For each issue, use Claude CLI to generate fix 13 * 4. Apply fixes to files 14 * 5. Run tests to verify 15 * 6. Commit to review branch if tests pass 16 * 7. Roll back if tests fail 17 * 18 * Environment Variables: 19 * - SAGE_AUTOFIX_MODEL: Model to use with claude CLI (default: sonnet) 20 * - SAGE_AUTOFIX_BRANCH: Branch name (default: sage-autofix-YYYY-MM-DD) 21 */ 22 23 import { execSync, execFileSync } from 'child_process'; 24 import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; 25 import { join } from 'path'; 26 import { fileURLToPath } from 'url'; 27 import { dirname } from 'path'; 28 29 const __filename = fileURLToPath(import.meta.url); 30 const __dirname = dirname(__filename); 31 const projectRoot = join(__dirname, '..'); 32 33 // Configuration 34 const MODEL = process.env.SAGE_AUTOFIX_MODEL || 'sonnet'; 35 const BRANCH_NAME = 36 process.env.SAGE_AUTOFIX_BRANCH || `sage-autofix-${new Date().toISOString().split('T')[0]}`; 37 const REPORT_DIR = join(projectRoot, '.quality-reports'); 38 const FIX_LOG_PATH = join(REPORT_DIR, 'auto-fix-log.json'); 39 40 // Stats tracking 41 const stats = { 42 startTime: new Date().toISOString(), 43 issues: { 44 eslint: 0, 45 prettier: 0, 46 sage: 0, 47 total: 0, 48 }, 49 fixes: { 50 applied: 0, 51 failed: 0, 52 skipped: 0, 53 }, 54 files: { 55 modified: [], 56 }, 57 branch: BRANCH_NAME, 58 testsPassedAfterFixes: false, 59 rollback: false, 60 }; 61 62 console.log('๐ค Sage Auto-Fix Script\n'); 63 console.log(`Model: ${MODEL}`); 64 console.log(`Branch: ${BRANCH_NAME}\n`); 65 66 // Validate claude CLI is available 67 try { 68 execSync('which claude', { stdio: 'ignore' }); 69 } catch { 70 console.error('Error: claude CLI not found in PATH'); 71 console.error(' Install at: https://claude.ai/claude-code'); 72 process.exit(1); 73 } 74 75 /** 76 * Run quality checks and generate reports 77 */ 78 function runQualityChecks() { 79 console.log('๐ Running quality checks...\n'); 80 try { 81 execSync('npm run quality-check', { 82 cwd: projectRoot, 83 stdio: 'inherit', 84 }); 85 console.log('โ Quality checks completed\n'); 86 return true; 87 } catch (error) { 88 // Quality check may exit with error if issues found - that's expected 89 console.log('โ ๏ธ Quality checks found issues - proceeding to fix them\n'); 90 return true; 91 } 92 } 93 94 /** 95 * Parse quality check reports 96 */ 97 function parseReports() { 98 const issues = []; 99 100 // Parse ESLint report 101 const eslintPath = join(REPORT_DIR, 'eslint.json'); 102 if (existsSync(eslintPath)) { 103 const eslintReport = JSON.parse(readFileSync(eslintPath, 'utf-8')); 104 eslintReport.forEach(fileResult => { 105 if (fileResult.messages && fileResult.messages.length > 0) { 106 fileResult.messages.forEach(msg => { 107 issues.push({ 108 type: 'eslint', 109 file: fileResult.filePath, 110 line: msg.line, 111 column: msg.column, 112 rule: msg.ruleId, 113 severity: msg.severity === 2 ? 'error' : 'warning', 114 message: msg.message, 115 fixable: msg.fix !== undefined, 116 }); 117 stats.issues.eslint++; 118 }); 119 } 120 }); 121 } 122 123 // Parse Sage report (if available) 124 const sagePath = join(REPORT_DIR, 'sage.json'); 125 if (existsSync(sagePath)) { 126 try { 127 const sageReport = JSON.parse(readFileSync(sagePath, 'utf-8')); 128 // Sage format varies - adapt parsing based on actual structure 129 if (Array.isArray(sageReport.findings)) { 130 sageReport.findings.forEach(finding => { 131 issues.push({ 132 type: 'sage', 133 file: finding.file || finding.filePath, 134 line: finding.line, 135 severity: finding.severity || 'warning', 136 message: finding.message || finding.description, 137 category: finding.category, 138 }); 139 stats.issues.sage++; 140 }); 141 } 142 } catch (error) { 143 console.log('โ ๏ธ Could not parse Sage report:', error.message); 144 } 145 } 146 147 stats.issues.total = issues.length; 148 return issues; 149 } 150 151 /** 152 * Group issues by file 153 */ 154 function groupIssuesByFile(issues) { 155 const grouped = {}; 156 issues.forEach(issue => { 157 if (!grouped[issue.file]) { 158 grouped[issue.file] = []; 159 } 160 grouped[issue.file].push(issue); 161 }); 162 return grouped; 163 } 164 165 /** 166 * Use Claude API to fix issues in a file 167 */ 168 async function fixFileIssues(filePath, issues) { 169 console.log(`\n๐ง Fixing ${issues.length} issues in ${filePath.replace(projectRoot, '.')}`); 170 171 // Read current file content 172 let fileContent; 173 try { 174 fileContent = readFileSync(filePath, 'utf-8'); 175 } catch (error) { 176 console.error(` โ Could not read file: ${error.message}`); 177 stats.fixes.failed += issues.length; 178 return false; 179 } 180 181 // Build prompt for Claude 182 const issuesDescription = issues 183 .map((issue, idx) => { 184 return `${idx + 1}. [Line ${issue.line || '?'}] ${issue.type.toUpperCase()}: ${issue.message}${issue.rule ? ` (${issue.rule})` : ''}`; 185 }) 186 .join('\n'); 187 188 const prompt = `You are a code quality expert. Fix the following issues in this file: 189 190 FILE: ${filePath.replace(projectRoot, '.')} 191 192 ISSUES TO FIX: 193 ${issuesDescription} 194 195 CURRENT FILE CONTENT: 196 \`\`\`javascript 197 ${fileContent} 198 \`\`\` 199 200 INSTRUCTIONS: 201 - Fix ALL listed issues 202 - Maintain code functionality - only change what's necessary 203 - Follow existing code style and patterns 204 - If an issue is already fixed in the code, skip it 205 - Respond ONLY with the complete fixed file content, no explanations 206 - Do not add comments explaining what you fixed 207 - Preserve all existing comments and documentation 208 209 OUTPUT THE COMPLETE FIXED FILE:`; 210 211 try { 212 const fixedContent = execFileSync('claude', ['-p', '--model', MODEL, '--output-format', 'text'], { 213 input: prompt, 214 encoding: 'utf-8', 215 timeout: 120000, 216 maxBuffer: 10 * 1024 * 1024, 217 }).trim(); 218 219 // Basic validation: check if response looks like code 220 if (!fixedContent.includes('import') && !fixedContent.includes('function')) { 221 console.error(' โ ๏ธ Claude response does not look like code - skipping'); 222 stats.fixes.skipped += issues.length; 223 return false; 224 } 225 226 // Write fixed content 227 writeFileSync(filePath, fixedContent); 228 stats.files.modified.push(filePath); 229 stats.fixes.applied += issues.length; 230 231 console.log(` โ Fixed ${issues.length} issues`); 232 return true; 233 } catch (error) { 234 console.error(` โ Claude API error: ${error.message}`); 235 stats.fixes.failed += issues.length; 236 return false; 237 } 238 } 239 240 /** 241 * Apply auto-fixable ESLint rules 242 */ 243 function applyEslintAutofix() { 244 console.log('๐ง Applying ESLint auto-fix...\n'); 245 try { 246 execSync('npm run lint:fix', { 247 cwd: projectRoot, 248 stdio: 'inherit', 249 }); 250 console.log('โ ESLint auto-fix completed\n'); 251 return true; 252 } catch (error) { 253 console.log('โ ๏ธ Some ESLint issues may remain\n'); 254 return false; 255 } 256 } 257 258 /** 259 * Apply Prettier formatting 260 */ 261 function applyPrettierFormat() { 262 console.log('๐ง Applying Prettier formatting...\n'); 263 try { 264 execSync('npm run format', { 265 cwd: projectRoot, 266 stdio: 'inherit', 267 }); 268 console.log('โ Prettier formatting completed\n'); 269 return true; 270 } catch (error) { 271 console.error('โ Prettier formatting failed:', error.message); 272 return false; 273 } 274 } 275 276 /** 277 * Run tests to verify fixes 278 */ 279 function runTests() { 280 console.log('\n๐งช Running tests to verify fixes...\n'); 281 try { 282 execSync('npm test', { 283 cwd: projectRoot, 284 stdio: 'inherit', 285 }); 286 console.log('โ All tests passed!\n'); 287 stats.testsPassedAfterFixes = true; 288 return true; 289 } catch (error) { 290 console.error('โ Tests failed after applying fixes!\n'); 291 stats.testsPassedAfterFixes = false; 292 return false; 293 } 294 } 295 296 /** 297 * Create git branch and commit fixes 298 */ 299 function commitFixes() { 300 console.log('๐ Committing fixes to review branch...\n'); 301 302 try { 303 // Check if we're in a git repo 304 execSync('git rev-parse --git-dir', { cwd: projectRoot, stdio: 'ignore' }); 305 306 // Check if branch already exists 307 try { 308 execSync(`git rev-parse --verify ${BRANCH_NAME}`, { cwd: projectRoot, stdio: 'ignore' }); 309 console.log(`โ ๏ธ Branch ${BRANCH_NAME} already exists - using timestamp suffix`); 310 const timestamp = Date.now(); 311 stats.branch = `${BRANCH_NAME}-${timestamp}`; 312 } catch { 313 // Branch doesn't exist - good to go 314 } 315 316 // Create branch 317 execSync(`git checkout -b ${stats.branch}`, { 318 cwd: projectRoot, 319 stdio: 'inherit', 320 }); 321 322 // Stage modified files 323 stats.files.modified.forEach(file => { 324 execSync(`git add "${file}"`, { cwd: projectRoot }); 325 }); 326 327 // Commit 328 const commitMessage = `fix: auto-fix quality issues via Sage AI review 329 330 Applied ${stats.fixes.applied} fixes across ${stats.files.modified.length} files 331 332 Issues fixed: 333 - ESLint: ${stats.issues.eslint} issues 334 - Sage AI: ${stats.issues.sage} issues 335 336 All tests passing after fixes. 337 338 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`; 339 340 execSync(`git commit -m "${commitMessage}"`, { 341 cwd: projectRoot, 342 stdio: 'inherit', 343 }); 344 345 console.log(`โ Fixes committed to branch: ${stats.branch}\n`); 346 console.log('๐ Next steps:'); 347 console.log(` 1. Review changes: git diff main ${stats.branch}`); 348 console.log(` 2. Merge if satisfied: git checkout main && git merge ${stats.branch}`); 349 console.log(` 3. Delete branch: git branch -d ${stats.branch}\n`); 350 351 return true; 352 } catch (error) { 353 console.error('โ Git commit failed:', error.message); 354 return false; 355 } 356 } 357 358 /** 359 * Roll back changes 360 */ 361 function rollback() { 362 console.log('\nโ ๏ธ Rolling back changes due to test failures...\n'); 363 stats.rollback = true; 364 365 try { 366 // Reset modified files 367 stats.files.modified.forEach(file => { 368 execSync(`git checkout HEAD -- "${file}"`, { cwd: projectRoot }); 369 }); 370 console.log('โ Rollback complete - files restored to original state\n'); 371 } catch (error) { 372 console.error('โ Rollback failed:', error.message); 373 console.error(' You may need to manually restore files with: git checkout HEAD -- .'); 374 } 375 } 376 377 /** 378 * Save fix log 379 */ 380 function saveLogs() { 381 stats.endTime = new Date().toISOString(); 382 const duration = new Date(stats.endTime) - new Date(stats.startTime); 383 stats.durationSeconds = (duration / 1000).toFixed(2); 384 385 writeFileSync(FIX_LOG_PATH, JSON.stringify(stats, null, 2)); 386 console.log(`\n๐ Fix log saved: ${FIX_LOG_PATH.replace(projectRoot, '.')}\n`); 387 } 388 389 /** 390 * Main execution 391 */ 392 async function main() { 393 // 1. Run quality checks 394 runQualityChecks(); 395 396 // 2. Parse reports 397 console.log('๐ Parsing quality reports...\n'); 398 const issues = parseReports(); 399 400 if (issues.length === 0) { 401 console.log('โ No issues found - code quality is excellent!\n'); 402 stats.endTime = new Date().toISOString(); 403 saveLogs(); 404 return; 405 } 406 407 console.log(`Found ${stats.issues.total} total issues:`); 408 console.log(` - ESLint: ${stats.issues.eslint}`); 409 console.log(` - Sage AI: ${stats.issues.sage}\n`); 410 411 // 3. Apply auto-fixable rules first 412 applyEslintAutofix(); 413 applyPrettierFormat(); 414 415 // 4. Parse reports again to see what remains 416 console.log('๐ Re-checking after auto-fixes...\n'); 417 const remainingIssues = parseReports(); 418 419 if (remainingIssues.length === 0) { 420 console.log('โ All issues resolved with auto-fix!\n'); 421 422 // Verify with tests 423 const testsPass = runTests(); 424 if (testsPass) { 425 commitFixes(); 426 } else { 427 rollback(); 428 } 429 430 saveLogs(); 431 return; 432 } 433 434 console.log(`${remainingIssues.length} issues require AI-assisted fixes\n`); 435 436 // 5. Group by file and fix with Claude 437 const groupedIssues = groupIssuesByFile(remainingIssues); 438 const filesToFix = Object.keys(groupedIssues); 439 440 console.log(`Fixing issues across ${filesToFix.length} files...\n`); 441 442 for (const filePath of filesToFix) { 443 const fileIssues = groupedIssues[filePath]; 444 await fixFileIssues(filePath, fileIssues); 445 } 446 447 // 6. Apply formatting after AI fixes 448 applyPrettierFormat(); 449 450 // 7. Run tests 451 const testsPass = runTests(); 452 453 // 8. Commit or rollback 454 if (testsPass && stats.fixes.applied > 0) { 455 commitFixes(); 456 } else if (!testsPass) { 457 rollback(); 458 } else { 459 console.log('โ ๏ธ No fixes were applied\n'); 460 } 461 462 // 9. Save logs 463 saveLogs(); 464 465 // Print summary 466 console.log('โ'.repeat(60)); 467 console.log('\n๐ Auto-Fix Summary\n'); 468 console.log('โ'.repeat(60)); 469 console.log(`Total Issues Found: ${stats.issues.total}`); 470 console.log(`Fixes Applied: ${stats.fixes.applied}`); 471 console.log(`Fixes Failed: ${stats.fixes.failed}`); 472 console.log(`Fixes Skipped: ${stats.fixes.skipped}`); 473 console.log(`Files Modified: ${stats.files.modified.length}`); 474 console.log(`Tests Passed: ${stats.testsPassedAfterFixes ? 'โ Yes' : 'โ No'}`); 475 console.log(`Rollback: ${stats.rollback ? 'โ ๏ธ Yes' : 'โ No'}`); 476 console.log(`Branch: ${stats.branch}`); 477 console.log(`Duration: ${stats.durationSeconds}s`); 478 console.log(`\n${'โ'.repeat(60)}\n`); 479 480 // Exit with appropriate code 481 process.exit(stats.testsPassedAfterFixes && stats.fixes.applied > 0 ? 0 : 1); 482 } 483 484 // Run 485 main().catch(error => { 486 console.error('โ Fatal error:', error); 487 stats.error = error.message; 488 stats.endTime = new Date().toISOString(); 489 saveLogs(); 490 process.exit(1); 491 });