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