← back to Letsbegin
app/api/processes/route.ts
278 lines
import { NextRequest, NextResponse } from 'next/server'
import { execSync } from 'child_process'
import * as fs from 'fs'
import * as path from 'path'
interface RalphProcess {
project: string
projectPath: string
pid: number
cpu: string
memory: string
status: 'running' | 'idle' | 'completed' | 'failed' | 'paused'
currentStory: string
startTime: string
progress: string
}
const LOG_FILE = '/tmp/ralph-output.log'
// Parse log file for live story updates (same logic as /api/ralph/status)
function parseLogForUpdates(state: any) {
if (!fs.existsSync(LOG_FILE)) return state
try {
const logContent = fs.readFileSync(LOG_FILE, 'utf-8')
const lines = logContent.split('\n').slice(-100)
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine) continue
// Check for story start
const startMatch = trimmedLine.match(/(?:Starting|Working on)\s+(US-\d+)/i)
if (startMatch) {
const storyId = startMatch[1]
state.storyResults = state.storyResults || {}
state.storyResults[storyId] = 'running'
}
// Check for story completion
const doneMatch = trimmedLine.match(/(?:Completed|Done|DONE)\s+(US-\d+)/i)
if (doneMatch) {
const storyId = doneMatch[1]
state.storyResults = state.storyResults || {}
state.storyResults[storyId] = 'pass'
}
// Check for story failure
const failMatch = trimmedLine.match(/(?:FAIL|Failed)\s+(US-\d+)/i)
if (failMatch) {
const storyId = failMatch[1]
state.storyResults = state.storyResults || {}
state.storyResults[storyId] = 'fail'
}
}
// Check if completed
if (logContent.includes('RALPH_COMPLETE')) {
state.completed = true
state.running = false
}
} catch (e) {
// Ignore parse errors
}
return state
}
export async function GET(request: NextRequest) {
try {
const ralphProcesses: RalphProcess[] = []
// Check for ralph state files in /tmp
const stateFile = '/tmp/ralph-state.json'
if (fs.existsSync(stateFile)) {
try {
let state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'))
// Parse log file for live updates
state = parseLogForUpdates(state)
if (state.running || state.completed) {
const projectName = path.basename(state.projectPath || '')
// Count completed stories from updated storyResults
const completed = Object.values(state.storyResults || {}).filter((s: any) => s === 'pass' || s === 'DONE').length
const total = state.userStories?.length || 0
// Find current story
let currentStory = ''
for (const [id, status] of Object.entries(state.storyResults || {})) {
if (status === 'running' || status === 'RUNNING') {
currentStory = id
break
}
}
// Determine status with paused check
let status: 'running' | 'idle' | 'completed' | 'failed' | 'paused' = 'idle'
if (state.completed) status = 'completed'
else if (state.paused) status = 'paused'
else if (state.running) status = 'running'
ralphProcesses.push({
project: projectName,
projectPath: state.projectPath,
pid: state.pid || 0,
cpu: '0',
memory: '0',
status,
currentStory: currentStory || 'idle',
startTime: state.startTime ? new Date(state.startTime).toLocaleTimeString() : '',
progress: `${completed}/${total}`
})
}
} catch (e) {
// Ignore parse errors
}
}
// Scan /root/Projects for any project with scripts/ralph/progress.txt
try {
const projectsDir = '/root/Projects'
const projects = fs.readdirSync(projectsDir)
for (const proj of projects) {
const progressFile = path.join(projectsDir, proj, 'scripts', 'ralph', 'progress.txt')
if (fs.existsSync(progressFile)) {
// Check if already added from state file
if (ralphProcesses.some(p => p.project === proj)) continue
const content = fs.readFileSync(progressFile, 'utf-8')
// Parse progress file
const lines = content.split('\n')
let running = 0, done = 0, pending = 0, failed = 0
let currentStory = ''
for (const line of lines) {
if (line.includes(': RUNNING')) {
running++
const match = line.match(/(US-\d+)/)
if (match) currentStory = match[1]
} else if (line.includes(': DONE')) {
done++
} else if (line.includes(': PENDING')) {
pending++
} else if (line.includes(': FAIL')) {
failed++
}
}
const total = running + done + pending + failed
if (total === 0) continue
// Determine status
let status: 'running' | 'idle' | 'completed' | 'failed' = 'idle'
if (running > 0) status = 'running'
else if (done === total) status = 'completed'
else if (failed > 0 && pending === 0 && running === 0) status = 'failed'
// Get start time from file
const startMatch = content.match(/Started: (.+)/)
const startTime = startMatch ? new Date(startMatch[1]).toLocaleTimeString() : ''
ralphProcesses.push({
project: proj,
projectPath: path.join(projectsDir, proj),
pid: 0,
cpu: '0',
memory: '0',
status,
currentStory: currentStory || (status === 'completed' ? 'done' : 'idle'),
startTime,
progress: `${done}/${total}`
})
}
}
} catch (e) {
// Ignore scan errors
}
// Check for actual ralph-runner processes running (NOT general node processes)
try {
// Only look for ralph-runner.sh processes, not all node processes
const psOutput = execSync(
"ps aux | grep -E 'ralph-runner|ralph-story|claude.*dangerously-skip' | grep -v grep || true",
{ encoding: 'utf-8', timeout: 5000 }
)
const lines = psOutput.trim().split('\n').filter(l => l.length > 0)
for (const line of lines) {
const parts = line.split(/\s+/)
if (parts.length >= 11) {
const pid = parseInt(parts[1])
const cpu = parts[2]
const mem = parts[3]
const procState = parts[7] // Process state (S, R, T, etc.)
const cmd = parts.slice(10).join(' ')
// Try to extract project from command
const projectMatch = cmd.match(/\/root\/Projects\/([^\/\s]+)/)
const projectName = projectMatch ? projectMatch[1] : null
if (!projectName) continue
// Update existing entry or add new
const existing = ralphProcesses.find(p =>
p.project.toLowerCase() === projectName.toLowerCase()
)
if (existing) {
existing.pid = pid
existing.cpu = cpu
existing.memory = mem
// Check if process is stopped/paused (T state)
if (procState === 'T') {
existing.status = 'paused'
} else if (existing.status !== 'completed' && existing.status !== 'failed') {
existing.status = 'running'
}
} else {
ralphProcesses.push({
project: projectName,
projectPath: `/root/Projects/${projectName}`,
pid,
cpu,
memory: mem,
status: procState === 'T' ? 'paused' : 'running',
currentStory: 'active',
startTime: 'now',
progress: '?/?'
})
}
}
}
} catch (e) {
// Ignore ps errors
}
// Also check if any tracked process has specific PID and get its real CPU/memory
for (const proc of ralphProcesses) {
if (proc.pid > 0 && proc.cpu === '0') {
try {
const pidInfo = execSync(
`ps -p ${proc.pid} -o %cpu,%mem,state --no-headers 2>/dev/null || true`,
{ encoding: 'utf-8', timeout: 3000 }
).trim()
if (pidInfo) {
const [cpu, mem, state] = pidInfo.split(/\s+/)
proc.cpu = cpu || '0'
proc.memory = mem || '0'
if (state === 'T') {
proc.status = 'paused'
}
}
} catch {
// Process may have ended
}
}
}
return NextResponse.json({
processes: ralphProcesses,
timestamp: new Date().toISOString()
})
} catch (error: any) {
console.error('Ralph process fetch error:', error)
return NextResponse.json({
processes: [],
error: error.message
})
}
}