← back to Letsbegin

app/api/ralph/control/route.ts

195 lines

import { NextRequest, NextResponse } from 'next/server'
import { execSync, exec } from 'child_process'
import * as fs from 'fs'

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

interface RalphState {
  running: boolean
  paused?: boolean
  pid?: number
  projectPath?: string
  logs: string[]
  [key: string]: any
}

function getState(): RalphState {
  if (fs.existsSync(STATE_FILE)) {
    return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'))
  }
  return { running: false, logs: [] }
}

function saveState(state: RalphState) {
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
}

function findProcessByProject(projectName: string): number | null {
  try {
    // Find ralph/claude processes for specific project
    const output = execSync(
      `ps aux | grep -E 'ralph-runner|claude.*dangerously' | grep '${projectName}' | grep -v grep | awk '{print $2}' | head -1`,
      { encoding: 'utf-8', timeout: 5000 }
    ).trim()
    return output ? parseInt(output) : null
  } catch {
    return null
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { action, project, pid } = body

    if (!action || !['stop', 'pause', 'resume'].includes(action)) {
      return NextResponse.json(
        { error: 'Invalid action. Must be stop, pause, or resume' },
        { status: 400 }
      )
    }

    const state = getState()
    let targetPid = pid

    // If no PID provided, try to find it from project name or state
    if (!targetPid && project) {
      targetPid = findProcessByProject(project)
    }
    if (!targetPid && state.pid) {
      targetPid = state.pid
    }

    if (!targetPid) {
      return NextResponse.json(
        { error: 'No process found to control' },
        { status: 404 }
      )
    }

    switch (action) {
      case 'stop': {
        try {
          const killed: string[] = []

          // Try to kill process group first (negative PID) with SIGKILL
          try {
            process.kill(-targetPid, 'SIGKILL')
            killed.push(`pid ${targetPid}`)
          } catch {
            try {
              process.kill(targetPid, 'SIGKILL')
              killed.push(`pid ${targetPid}`)
            } catch {}
          }

          // Kill ralph-runner script
          try {
            execSync('pkill -9 -f "ralph-runner" || true', { stdio: 'pipe' })
            killed.push('ralph-runner')
          } catch {}

          // Kill claude processes with dangerously-skip-permissions
          try {
            execSync('pkill -9 -f "claude.*dangerously-skip-permissions" || true', { stdio: 'pipe' })
            killed.push('claude')
          } catch {}

          // Kill ralph-story processes
          try {
            execSync('pkill -9 -f "ralph-story" || true', { stdio: 'pipe' })
            killed.push('ralph-story')
          } catch {}

          // Kill sudo ralph processes
          try {
            execSync('pkill -9 -f "sudo.*ralph" || true', { stdio: 'pipe' })
            killed.push('sudo ralph')
          } catch {}

          state.running = false
          state.paused = false
          state.pid = undefined
          state.logs.push(`> STOPPED by user (killed: ${killed.join(', ')})`)
          saveState(state)

          return NextResponse.json({
            success: true,
            message: `Stopped. Killed: ${killed.join(', ')}`,
            action: 'stop'
          })
        } catch (e: any) {
          return NextResponse.json(
            { error: `Failed to stop: ${e.message}` },
            { status: 500 }
          )
        }
      }

      case 'pause': {
        try {
          // Send SIGSTOP to pause the process
          process.kill(targetPid, 'SIGSTOP')

          // Also pause child processes
          try {
            execSync(`pkill -STOP -P ${targetPid} || true`, { stdio: 'pipe' })
          } catch {}

          state.paused = true
          state.logs.push(`> Paused process ${targetPid}`)
          saveState(state)

          return NextResponse.json({
            success: true,
            message: `Paused process ${targetPid}`,
            action: 'pause'
          })
        } catch (e: any) {
          return NextResponse.json(
            { error: `Failed to pause: ${e.message}` },
            { status: 500 }
          )
        }
      }

      case 'resume': {
        try {
          // Send SIGCONT to resume the process
          process.kill(targetPid, 'SIGCONT')

          // Also resume child processes
          try {
            execSync(`pkill -CONT -P ${targetPid} || true`, { stdio: 'pipe' })
          } catch {}

          state.paused = false
          state.logs.push(`> Resumed process ${targetPid}`)
          saveState(state)

          return NextResponse.json({
            success: true,
            message: `Resumed process ${targetPid}`,
            action: 'resume'
          })
        } catch (e: any) {
          return NextResponse.json(
            { error: `Failed to resume: ${e.message}` },
            { status: 500 }
          )
        }
      }

      default:
        return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
    }

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