265 lines
9.9 KiB
JavaScript
265 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* OpenQode Smart Repair Agent
|
|
* A specialized TUI for diagnosing and fixing bugs in the main TUI IDE
|
|
*
|
|
* This agent has ONE mission: Repair the TUI when it crashes
|
|
*/
|
|
import { createRequire } from 'module';
|
|
const require = createRequire(import.meta.url);
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync, spawnSync } = require('child_process');
|
|
const readline = require('readline');
|
|
|
|
// File paths relative to package root
|
|
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..');
|
|
const MAIN_TUI = path.join(ROOT, 'bin', 'opencode-ink.mjs');
|
|
const PACKAGE_JSON = path.join(ROOT, 'package.json');
|
|
|
|
// Colors for terminal output
|
|
const C = {
|
|
reset: '\x1b[0m',
|
|
cyan: '\x1b[36m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
red: '\x1b[31m',
|
|
magenta: '\x1b[35m',
|
|
bold: '\x1b[1m',
|
|
dim: '\x1b[2m'
|
|
};
|
|
|
|
const banner = () => {
|
|
console.clear();
|
|
console.log(C.magenta + C.bold);
|
|
console.log(' ╔═══════════════════════════════════════════╗');
|
|
console.log(' ║ 🔧 OpenQode Smart Repair Agent 🔧 ║');
|
|
console.log(' ║ TUI Self-Healing System ║');
|
|
console.log(' ╚═══════════════════════════════════════════╝');
|
|
console.log(C.reset);
|
|
console.log(C.dim + ' This agent can ONLY repair the TUI. No other tasks.' + C.reset);
|
|
console.log('');
|
|
};
|
|
|
|
// Parse error from user input
|
|
const parseError = (errorText) => {
|
|
const issues = [];
|
|
|
|
// Extract file and line number
|
|
const lineMatch = errorText.match(/opencode-ink\.mjs:(\d+)/);
|
|
const line = lineMatch ? parseInt(lineMatch[1]) : null;
|
|
|
|
// Common error patterns
|
|
if (errorText.includes('Cannot read properties of null')) {
|
|
issues.push({
|
|
type: 'NULL_REFERENCE',
|
|
desc: 'Null reference error - attempting to access property on null object',
|
|
line
|
|
});
|
|
}
|
|
if (errorText.includes('Cannot read properties of undefined')) {
|
|
issues.push({
|
|
type: 'UNDEFINED_REFERENCE',
|
|
desc: 'Undefined reference - variable or property does not exist',
|
|
line
|
|
});
|
|
}
|
|
if (errorText.includes('useMemo') || errorText.includes('useEffect') || errorText.includes('useState')) {
|
|
issues.push({
|
|
type: 'REACT_HOOKS',
|
|
desc: 'React hooks error - possible multiple React versions or hooks called outside component',
|
|
line
|
|
});
|
|
}
|
|
if (errorText.includes('ink-syntax-highlight')) {
|
|
issues.push({
|
|
type: 'INK_SYNTAX_HIGHLIGHT',
|
|
desc: 'Extraneous ink-syntax-highlight package causing React conflict',
|
|
line
|
|
});
|
|
}
|
|
if (errorText.includes('ENOENT')) {
|
|
issues.push({
|
|
type: 'FILE_NOT_FOUND',
|
|
desc: 'File or directory not found',
|
|
line
|
|
});
|
|
}
|
|
if (errorText.includes('SyntaxError')) {
|
|
issues.push({
|
|
type: 'SYNTAX_ERROR',
|
|
desc: 'JavaScript syntax error in code',
|
|
line
|
|
});
|
|
}
|
|
|
|
return issues;
|
|
};
|
|
|
|
// Automatic repair functions
|
|
const repairs = {
|
|
REACT_HOOKS: () => {
|
|
console.log(C.yellow + '[*] Fixing React hooks conflict...' + C.reset);
|
|
// Delete node_modules and reinstall
|
|
const nmPath = path.join(ROOT, 'node_modules');
|
|
const lockPath = path.join(ROOT, 'package-lock.json');
|
|
if (fs.existsSync(nmPath)) {
|
|
console.log(' Removing node_modules...');
|
|
fs.rmSync(nmPath, { recursive: true, force: true });
|
|
}
|
|
if (fs.existsSync(lockPath)) {
|
|
console.log(' Removing package-lock.json...');
|
|
fs.unlinkSync(lockPath);
|
|
}
|
|
console.log(' Running npm install...');
|
|
try {
|
|
execSync('npm install --legacy-peer-deps', { cwd: ROOT, stdio: 'inherit' });
|
|
return { success: true, msg: 'Dependencies reinstalled with React overrides' };
|
|
} catch (e) {
|
|
return { success: false, msg: 'npm install failed: ' + e.message };
|
|
}
|
|
},
|
|
|
|
INK_SYNTAX_HIGHLIGHT: () => {
|
|
console.log(C.yellow + '[*] Removing extraneous ink-syntax-highlight...' + C.reset);
|
|
const ish = path.join(ROOT, 'node_modules', 'ink-syntax-highlight');
|
|
if (fs.existsSync(ish)) {
|
|
fs.rmSync(ish, { recursive: true, force: true });
|
|
return { success: true, msg: 'Removed ink-syntax-highlight package' };
|
|
}
|
|
// If not found, do full reinstall
|
|
return repairs.REACT_HOOKS();
|
|
},
|
|
|
|
NULL_REFERENCE: (issue) => {
|
|
if (!issue.line) {
|
|
return { success: false, msg: 'Could not determine line number for null reference fix' };
|
|
}
|
|
console.log(C.yellow + `[*] Checking line ${issue.line} for null safety...` + C.reset);
|
|
// Read the file and show the problematic line
|
|
const code = fs.readFileSync(MAIN_TUI, 'utf8');
|
|
const lines = code.split('\n');
|
|
const problemLine = lines[issue.line - 1];
|
|
console.log(C.dim + ` Line ${issue.line}: ${problemLine.trim().substring(0, 60)}...` + C.reset);
|
|
return { success: false, msg: 'Manual review needed. Please report to developer with error details.' };
|
|
},
|
|
|
|
UNDEFINED_REFERENCE: (issue) => repairs.NULL_REFERENCE(issue),
|
|
|
|
FILE_NOT_FOUND: () => {
|
|
console.log(C.yellow + '[*] Checking file structure...' + C.reset);
|
|
if (!fs.existsSync(MAIN_TUI)) {
|
|
return { success: false, msg: 'Main TUI file missing! Please re-clone the repository.' };
|
|
}
|
|
return { success: true, msg: 'File structure appears intact' };
|
|
},
|
|
|
|
SYNTAX_ERROR: () => {
|
|
console.log(C.yellow + '[*] Checking syntax...' + C.reset);
|
|
try {
|
|
execSync(`node -c "${MAIN_TUI}"`, { cwd: ROOT });
|
|
return { success: true, msg: 'Syntax check passed' };
|
|
} catch (e) {
|
|
return { success: false, msg: 'Syntax error detected. Please pull latest code: git pull origin main' };
|
|
}
|
|
}
|
|
};
|
|
|
|
// Main repair function
|
|
const attemptRepair = async (errorText) => {
|
|
console.log(C.cyan + '\n[ANALYZING ERROR...]' + C.reset);
|
|
const issues = parseError(errorText);
|
|
|
|
if (issues.length === 0) {
|
|
console.log(C.yellow + '[!] Could not identify specific issue from error.' + C.reset);
|
|
console.log(' Generic repair: Reinstalling dependencies...');
|
|
const result = repairs.REACT_HOOKS();
|
|
return result;
|
|
}
|
|
|
|
console.log(C.green + `[✓] Identified ${issues.length} issue(s):` + C.reset);
|
|
issues.forEach((issue, i) => {
|
|
console.log(` ${i + 1}. ${issue.type}: ${issue.desc}`);
|
|
if (issue.line) console.log(` At line: ${issue.line}`);
|
|
});
|
|
|
|
console.log(C.cyan + '\n[ATTEMPTING REPAIRS...]' + C.reset);
|
|
|
|
for (const issue of issues) {
|
|
const repairFn = repairs[issue.type];
|
|
if (repairFn) {
|
|
const result = repairFn(issue);
|
|
if (result.success) {
|
|
console.log(C.green + `[✓] ${result.msg}` + C.reset);
|
|
} else {
|
|
console.log(C.red + `[✗] ${result.msg}` + C.reset);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify the fix
|
|
console.log(C.cyan + '\n[VERIFYING FIX...]' + C.reset);
|
|
try {
|
|
execSync(`node -c "${MAIN_TUI}"`, { cwd: ROOT });
|
|
console.log(C.green + '[✓] Syntax check passed!' + C.reset);
|
|
return { success: true };
|
|
} catch (e) {
|
|
console.log(C.red + '[✗] Still has issues.' + C.reset);
|
|
return { success: false };
|
|
}
|
|
};
|
|
|
|
// Interactive mode
|
|
const runInteractive = async () => {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
const question = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
|
|
while (true) {
|
|
banner();
|
|
console.log(C.yellow + ' Paste the error message you received from TUI crash.' + C.reset);
|
|
console.log(C.dim + ' (Type "quit" to exit, or press Ctrl+C)' + C.reset);
|
|
console.log('');
|
|
|
|
const errorText = await question(C.magenta + '> ' + C.reset);
|
|
|
|
if (errorText.toLowerCase() === 'quit' || errorText.toLowerCase() === 'exit') {
|
|
console.log(C.cyan + '\nGoodbye! Try launching TUI again.' + C.reset);
|
|
rl.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
// Check if user is asking for something other than repair
|
|
const nonRepairKeywords = ['create', 'build', 'write code', 'make a', 'help me', 'how to', 'what is'];
|
|
if (nonRepairKeywords.some(kw => errorText.toLowerCase().includes(kw))) {
|
|
console.log(C.red + '\n[!] I can ONLY repair the TUI. For other tasks, use the main TUI IDE.' + C.reset);
|
|
await question('\nPress Enter to continue...');
|
|
continue;
|
|
}
|
|
|
|
const result = await attemptRepair(errorText);
|
|
|
|
console.log('');
|
|
if (result.success) {
|
|
console.log(C.green + C.bold + ' ╔═══════════════════════════════════════════╗' + C.reset);
|
|
console.log(C.green + C.bold + ' ║ ✅ REPAIR COMPLETE ✅ ║' + C.reset);
|
|
console.log(C.green + C.bold + ' ╚═══════════════════════════════════════════╝' + C.reset);
|
|
console.log(C.cyan + '\n Try launching the TUI again!' + C.reset);
|
|
} else {
|
|
console.log(C.yellow + ' If the error persists, paste the new error message.' + C.reset);
|
|
}
|
|
|
|
await question('\nPress Enter to continue...');
|
|
}
|
|
};
|
|
|
|
// Entry point
|
|
runInteractive().catch(e => {
|
|
console.error(C.red + 'Smart Repair crashed: ' + e.message + C.reset);
|
|
process.exit(1);
|
|
});
|