← back to Letsbegin

app/api/processes/fix/route.ts

288 lines

import { NextRequest, NextResponse } from 'next/server'
import { logGemini } from '@/lib/cost-track'
import * as fs from 'fs'
import * as path from 'path'
import { execSync, spawn } from 'child_process'

// Gemini API configuration (FREE - no credits needed!)
const GEMINI_API_KEY = process.env.GEMINI_API_KEY
if (!GEMINI_API_KEY) {
  throw new Error('GEMINI_API_KEY environment variable is required')
}
const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'

async function callGemini(prompt: string): Promise<string> {
  const response = await fetch(`${GEMINI_ENDPOINT}?key=${GEMINI_API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: { temperature: 0.3, maxOutputTokens: 2000 }
    })
  })

  if (!response.ok) {
    const error = await response.text()
    throw new Error(`Gemini API error: ${error}`)
  }

  const data = await response.json()
  logGemini(data, { note: 'processes-fix' })
  return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
}

const STATE_FILE = '/tmp/ralph-state.json'

export async function POST(request: NextRequest) {
  try {
    let body: { projectPath?: string; project?: string; analysis?: string; attempt?: number }
    try {
      body = await request.json()
    } catch {
      return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
    }
    const { projectPath, project, analysis, attempt = 1 } = body

    if (!projectPath || !project) {
      return NextResponse.json({ error: 'Missing projectPath or project' }, { status: 400 })
    }

    const logs: string[] = [`=== FIX ATTEMPT ${attempt} ===`]
    logs.push(`Project: ${project}`)
    logs.push(`Time: ${new Date().toISOString()}`)
    logs.push('')

    // Collect error context
    let errorContext = analysis || ''

    // Read progress.txt for current state
    const progressFile = path.join(projectPath, 'scripts', 'ralph', 'progress.txt')
    if (fs.existsSync(progressFile)) {
      errorContext += '\n\n=== PROGRESS FILE ===\n' + fs.readFileSync(progressFile, 'utf-8')
    }

    // Read any error logs
    const errorLogPaths = [
      path.join(projectPath, 'ralph-error.log'),
      path.join(projectPath, 'scripts', 'ralph', 'error.log'),
    ]
    for (const logPath of errorLogPaths) {
      if (fs.existsSync(logPath)) {
        const content = fs.readFileSync(logPath, 'utf-8').split('\n').slice(-50).join('\n')
        errorContext += `\n\n=== ${path.basename(logPath)} ===\n${content}`
      }
    }

    // Read prd.json to understand the task
    const prdFile = path.join(projectPath, 'scripts', 'ralph', 'prd.json')
    let prdData: any = null
    if (fs.existsSync(prdFile)) {
      prdData = JSON.parse(fs.readFileSync(prdFile, 'utf-8'))
      errorContext += `\n\n=== PRD SUMMARY ===\nProject: ${prdData.project}\nStories: ${prdData.userStories?.length || 0}`
    }

    // Use Gemini to analyze and generate fix commands
    logs.push('Analyzing errors with Gemini...')

    const aiPrompt = `You are a debugging expert. Analyze these errors from a Ralph (automated code generator) process and provide SPECIFIC fix commands.

PROJECT PATH: ${projectPath}
PROJECT: ${project}
ATTEMPT: ${attempt}

ERROR CONTEXT:
${errorContext}

Respond with a JSON object containing:
1. "diagnosis": Brief explanation of the root cause
2. "fixes": Array of fix objects, each with:
   - "type": "command" | "file_edit" | "file_create" | "file_delete"
   - "description": What this fix does
   - For "command": "command": the bash command to run
   - For "file_edit": "path", "find", "replace"
   - For "file_create": "path", "content"
   - For "file_delete": "path"
3. "should_restart": boolean - whether to restart Ralph after fixes
4. "confidence": "high" | "medium" | "low"

Common fixes:
- Missing dependencies: npm install
- Type errors: Fix the specific type issue
- Missing files: Create them
- Permission issues: chmod
- Port conflicts: Kill process on port
- Stalled stories: Reset story status in progress.txt

IMPORTANT: Only suggest safe, reversible fixes. Never suggest destructive commands.
Respond ONLY with valid JSON, no markdown.`

    const aiResponse = await callGemini(aiPrompt)
    logs.push('Claude response received')

    let fixPlan: any
    try {
      // Clean up response - remove any markdown formatting
      const cleanJson = aiResponse.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
      fixPlan = JSON.parse(cleanJson)
    } catch (e) {
      logs.push(`Failed to parse fix plan: ${e}`)
      logs.push(`Raw response: ${aiResponse.substring(0, 500)}`)
      return NextResponse.json({
        success: false,
        logs,
        error: 'Failed to parse AI fix suggestions'
      })
    }

    logs.push('')
    logs.push(`DIAGNOSIS: ${fixPlan.diagnosis}`)
    logs.push(`CONFIDENCE: ${fixPlan.confidence}`)
    logs.push(`FIXES TO APPLY: ${fixPlan.fixes?.length || 0}`)
    logs.push('')

    // Apply fixes
    const appliedFixes: string[] = []
    const failedFixes: string[] = []

    for (const fix of (fixPlan.fixes || [])) {
      logs.push(`Applying: ${fix.description}`)

      try {
        switch (fix.type) {
          case 'command':
            if (fix.command && !fix.command.includes('rm -rf') && !fix.command.includes('sudo rm')) {
              execSync(fix.command, {
                cwd: projectPath,
                encoding: 'utf-8',
                timeout: 30000
              })
              appliedFixes.push(fix.description)
              logs.push(`  ✓ Command executed`)
            } else {
              logs.push(`  ✗ Skipped unsafe command`)
              failedFixes.push(`Unsafe: ${fix.description}`)
            }
            break

          case 'file_edit':
            if (fix.path && fix.find && fix.replace !== undefined) {
              const filePath = path.isAbsolute(fix.path) ? fix.path : path.join(projectPath, fix.path)
              if (fs.existsSync(filePath)) {
                let content = fs.readFileSync(filePath, 'utf-8')
                if (content.includes(fix.find)) {
                  content = content.replace(fix.find, fix.replace)
                  fs.writeFileSync(filePath, content)
                  appliedFixes.push(fix.description)
                  logs.push(`  ✓ File edited`)
                } else {
                  logs.push(`  ✗ Pattern not found in file`)
                  failedFixes.push(`Pattern not found: ${fix.description}`)
                }
              } else {
                logs.push(`  ✗ File not found: ${filePath}`)
                failedFixes.push(`File not found: ${fix.description}`)
              }
            }
            break

          case 'file_create':
            if (fix.path && fix.content !== undefined) {
              const filePath = path.isAbsolute(fix.path) ? fix.path : path.join(projectPath, fix.path)
              const dir = path.dirname(filePath)
              if (!fs.existsSync(dir)) {
                fs.mkdirSync(dir, { recursive: true })
              }
              fs.writeFileSync(filePath, fix.content)
              appliedFixes.push(fix.description)
              logs.push(`  ✓ File created`)
            }
            break

          case 'file_delete':
            // Skip file deletions for safety
            logs.push(`  ✗ Skipped file deletion for safety`)
            failedFixes.push(`Skipped deletion: ${fix.description}`)
            break

          default:
            logs.push(`  ✗ Unknown fix type: ${fix.type}`)
        }
      } catch (e: any) {
        logs.push(`  ✗ Error: ${e.message}`)
        failedFixes.push(`Error: ${fix.description} - ${e.message}`)
      }
    }

    logs.push('')
    logs.push(`Applied: ${appliedFixes.length} fixes`)
    logs.push(`Failed: ${failedFixes.length} fixes`)

    // Restart if suggested
    let restarted = false
    if (fixPlan.should_restart && appliedFixes.length > 0) {
      logs.push('')
      logs.push('Restarting Ralph process...')

      // Reset failed stories to PENDING in progress.txt
      if (fs.existsSync(progressFile)) {
        let content = fs.readFileSync(progressFile, 'utf-8')
        content = content.replace(/: FAIL/g, ': PENDING')
        content = content.replace(/: STOPPED/g, ': PENDING')
        fs.writeFileSync(progressFile, content)
        logs.push('Reset failed stories to PENDING')
      }

      // Update state file
      if (fs.existsSync(STATE_FILE)) {
        try {
          const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'))
          state.running = true
          state.logs = state.logs || []
          state.logs.push(`> Auto-fix attempt ${attempt} - restarting`)

          // Reset failed story results
          if (state.storyResults) {
            for (const [id, status] of Object.entries(state.storyResults)) {
              if (status === 'FAIL' || status === 'fail' || status === 'STOPPED') {
                state.storyResults[id] = 'PENDING'
              }
            }
          }

          fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
        } catch {}
      }

      // Try to start the ralph runner
      const runnerScript = path.join(projectPath, 'scripts', 'ralph', 'ralph-runner.sh')
      if (fs.existsSync(runnerScript)) {
        const proc = spawn('bash', [runnerScript], {
          cwd: projectPath,
          detached: true,
          stdio: 'ignore'
        })
        proc.unref()
        restarted = true
        logs.push(`Ralph restarted with PID ${proc.pid}`)
      } else {
        logs.push('No ralph-runner.sh found - manual restart required')
      }
    }

    return NextResponse.json({
      success: appliedFixes.length > 0,
      logs,
      diagnosis: fixPlan.diagnosis,
      confidence: fixPlan.confidence,
      appliedFixes,
      failedFixes,
      restarted,
      attempt
    })

  } catch (error: any) {
    console.error('Fix process error:', error)
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}