UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

213 lines โ€ข 8.17 kB
import * as readline from 'readline'; import { AutoFixer } from './AutoFixer.js'; /** * Interactive fixer that prompts user for each fix. */ export class InteractiveFixer { promptFn; outputFn; contextLines; constructor(options = {}) { this.promptFn = options.promptFn ?? this.createDefaultPrompt(); this.outputFn = options.outputFn ?? console.log; this.contextLines = options.contextLines ?? 3; } /** * Run interactive fix session for violations */ async fix(content, violations, customRules = []) { const fixes = AutoFixer.generateFixesForViolations(violations, content, customRules); if (fixes.length === 0) { this.outputFn('No fixes available.'); return { fixed: false, content, appliedCount: 0, skippedCount: 0, quitEarly: false, }; } // Sort fixes by position (start from beginning for display) const sortedFixes = [...fixes].sort((a, b) => { if (a.range.start.line !== b.range.start.line) { return a.range.start.line - b.range.start.line; } return a.range.start.column - b.range.start.column; }); this.outputFn(`\n๐Ÿ“ Found ${sortedFixes.length} fixable issue(s)\n`); let currentContent = content; let appliedCount = 0; let skippedCount = 0; let applyAll = false; let quitEarly = false; for (let i = 0; i < sortedFixes.length; i++) { const fix = sortedFixes[i]; if (!fix) continue; // Recalculate fix position based on current content const adjustedFix = this.adjustFixForContent(fix, content, currentContent); if (!adjustedFix) { skippedCount++; continue; } if (!applyAll) { this.showFixPreview(currentContent, adjustedFix, i + 1, sortedFixes.length); const action = await this.promptForAction(); switch (action) { case 'yes': break; case 'no': skippedCount++; this.outputFn('โญ๏ธ Skipped\n'); continue; case 'all': applyAll = true; break; case 'quit': quitEarly = true; skippedCount += sortedFixes.length - i; this.outputFn('\n๐Ÿ›‘ Quit interactive mode\n'); break; } if (quitEarly) break; } // Apply the fix const result = AutoFixer.applyFixes(currentContent, [adjustedFix]); if (result.fixed) { currentContent = result.content; appliedCount++; this.outputFn('โœ… Applied\n'); } else { skippedCount++; this.outputFn('โš ๏ธ Could not apply fix\n'); } } this.showSummary(appliedCount, skippedCount, quitEarly); return { fixed: appliedCount > 0, content: currentContent, appliedCount, skippedCount, quitEarly, }; } /** * Show a preview of the fix with diff */ showFixPreview(content, fix, current, total) { const lines = content.split('\n'); const startLine = fix.range.start.line; const endLine = fix.range.end.line; this.outputFn(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`); this.outputFn(`Fix ${current}/${total}: ${fix.description}`); this.outputFn(`Location: Line ${startLine}${endLine !== startLine ? `-${endLine}` : ''}`); this.outputFn(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`); // Calculate context range const contextStart = Math.max(0, startLine - 1 - this.contextLines); const contextEnd = Math.min(lines.length, endLine + this.contextLines); // Show before context for (let i = contextStart; i < startLine - 1; i++) { this.outputFn(` ${this.padLineNumber(i + 1)} โ”‚ ${lines[i]}`); } // Show affected lines (to be removed) for (let i = startLine - 1; i < endLine; i++) { const line = lines[i] ?? ''; this.outputFn(`\x1b[31m- ${this.padLineNumber(i + 1)} โ”‚ ${this.visualizeWhitespace(line)}\x1b[0m`); } // Show replacement (to be added) if (fix.text) { const newLines = fix.text.split('\n'); newLines.forEach((newLine, idx) => { const lineNum = idx === 0 ? startLine : '+'; this.outputFn(`\x1b[32m+ ${this.padLineNumber(lineNum)} โ”‚ ${this.visualizeWhitespace(newLine)}\x1b[0m`); }); } // Show after context for (let i = endLine; i < contextEnd; i++) { this.outputFn(` ${this.padLineNumber(i + 1)} โ”‚ ${lines[i]}`); } this.outputFn(''); } /** * Prompt user for action */ async promptForAction() { const response = await this.promptFn('Apply this fix? [y]es / [n]o / [a]ll / [q]uit: '); const normalized = response.toLowerCase().trim(); switch (normalized) { case 'y': case 'yes': return 'yes'; case 'n': case 'no': return 'no'; case 'a': case 'all': return 'all'; case 'q': case 'quit': return 'quit'; default: this.outputFn('Invalid choice. Please enter y, n, a, or q.'); return this.promptForAction(); } } /** * Show summary of the fix session */ showSummary(applied, skipped, quitEarly) { this.outputFn('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); this.outputFn('Summary:'); this.outputFn(` โœ… Applied: ${applied}`); this.outputFn(` โญ๏ธ Skipped: ${skipped}`); if (quitEarly) { this.outputFn(' ๐Ÿ›‘ Session ended early'); } this.outputFn('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n'); } /** * Pad line number for consistent display */ padLineNumber(num) { return String(num).padStart(4, ' '); } /** * Visualize whitespace characters for clarity */ visualizeWhitespace(line) { // Show trailing spaces as dots return line.replace(/( +)$/, match => 'ยท'.repeat(match.length)); } /** * Adjust fix position for content changes */ adjustFixForContent(fix, originalContent, currentContent) { // Simple implementation: if content hasn't changed, return as-is if (originalContent === currentContent) { return fix; } // For now, return the fix as-is // A more sophisticated implementation would track line changes return fix; } /** * Create default readline prompt */ createDefaultPrompt() { return (question) => { return new Promise(resolve => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question(question, answer => { rl.close(); resolve(answer); }); }); }; } } //# sourceMappingURL=InteractiveFixer.js.map