← 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 }
)
}
}