← back to Letsbegin

app/api/processes/start/route.ts

222 lines

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

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

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

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

    // Check if there's a ralph prd.json in the project
    const prdPath = path.join(projectPath, 'scripts', 'ralph', 'prd.json')
    if (!fs.existsSync(prdPath)) {
      return NextResponse.json({
        error: `No prd.json found at ${prdPath}. Run Ralph Converter first.`
      }, { status: 400 })
    }

    // Read the prd.json to get user stories
    const prdData = JSON.parse(fs.readFileSync(prdPath, 'utf-8'))
    const branchName = prdData.branchName || `ralph/${project}`

    // Create initial state
    const storyResults: Record<string, string> = {}

    // Initialize all stories as pending
    for (const story of prdData.userStories || []) {
      storyResults[story.id] = 'PENDING'
    }

    const state = {
      running: true,
      projectPath,
      project,
      branchName,
      startTime: new Date().toISOString(),
      userStories: prdData.userStories || [],
      storyResults,
      pid: 0,
      logs: [
        `> Starting Ralph for ${project}`,
        `> Project path: ${projectPath}`,
        `> Found ${prdData.userStories?.length || 0} user stories`,
        ''
      ]
    }

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

    // Create or update progress.txt
    const progressDir = path.join(projectPath, 'scripts', 'ralph')
    if (!fs.existsSync(progressDir)) {
      fs.mkdirSync(progressDir, { recursive: true })
    }

    const progressContent = `# Ralph Progress
# Project: ${project}
# Branch: ${branchName}
# Started: ${new Date().toISOString()}

## Stories
${state.userStories.map((s: any) => `${s.id}: PENDING - ${s.title}`).join('\n')}

## Log
`
    fs.writeFileSync(path.join(progressDir, 'progress.txt'), progressContent)

    // Ensure ralph user can write to the project
    try {
      execSync(`chown -R ralph:ralph "${projectPath}"`, { stdio: 'pipe' })
    } catch {}

    // Create the Ralph runner script with proper loop
    const scriptPath = '/tmp/ralph-runner.sh'
    const logPath = '/tmp/ralph-output.log'
    const progressFilePath = path.join(progressDir, 'progress.txt')

    const script = `#!/bin/bash
set -e

cd "${projectPath}"
export HOME="/root"
export NVM_DIR="/root/.nvm"
source /root/.nvm/nvm.sh 2>/dev/null || true
export PATH="/root/.nvm/versions/node/v22.19.0/bin:$PATH"

PROGRESS_FILE="${progressFilePath}"
LOG_FILE="${logPath}"
STATE_FILE="${STATE_FILE}"

# Clear log file
> "$LOG_FILE"

echo "=== RALPHY BOY STARTING ===" >> "$LOG_FILE"
echo "Project: ${projectPath}" >> "$LOG_FILE"
echo "Branch: ${branchName}" >> "$LOG_FILE"
echo "Running as: $(whoami)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"

# Try to create git branch
cd "${projectPath}"
git checkout -b "${branchName}" 2>/dev/null || git checkout "${branchName}" 2>/dev/null || echo "Git branch setup skipped" >> "$LOG_FILE"

# Loop through each story
${state.userStories.map((story: any, idx: number) => `
# Story ${story.id}
echo "=== Starting ${story.id}: ${story.title.replace(/"/g, '\\"').replace(/`/g, '\\`')} ===" >> "$LOG_FILE"
sed -i 's/${story.id}: PENDING/${story.id}: RUNNING/' "$PROGRESS_FILE"

# Update state file to show current story
/root/.nvm/versions/node/v22.19.0/bin/node -e "
const fs = require('fs');
const state = JSON.parse(fs.readFileSync('$STATE_FILE', 'utf-8'));
state.storyResults['${story.id}'] = 'RUNNING';
state.currentStoryIndex = ${idx};
state.logs.push('> Starting ${story.id}: ${story.title.replace(/'/g, "\\'")}');
fs.writeFileSync('$STATE_FILE', JSON.stringify(state, null, 2));
"

# Create story-specific prompt
cat > /tmp/ralph-story-${story.id}.txt << 'STORYPROMPT'
You are Ralphy Boy. Implement this ONE story for project ${project}:

STORY: ${story.id} - ${story.title}
DESCRIPTION: ${story.description}

ACCEPTANCE CRITERIA:
${story.acceptanceCriteria.map((c: string) => `- ${c}`).join('\n')}

INSTRUCTIONS:
1. Implement only this story
2. Run typecheck: npm run build (or tsc --noEmit)
3. Commit: git add . && git commit -m "feat(${story.id}): ${story.title.replace(/"/g, '\\"')}"
4. Announce when done: "Completed ${story.id}"

Start implementation now.
STORYPROMPT

# Run Claude for this story
echo "Running Claude for ${story.id}..." >> "$LOG_FILE"
if /root/.nvm/versions/node/v22.19.0/bin/claude --dangerously-skip-permissions -p "$(cat /tmp/ralph-story-${story.id}.txt)" >> "$LOG_FILE" 2>&1; then
  echo "Completed ${story.id}" >> "$LOG_FILE"
  sed -i 's/${story.id}: RUNNING/${story.id}: DONE/' "$PROGRESS_FILE"
  # Update state
  /root/.nvm/versions/node/v22.19.0/bin/node -e "
const fs = require('fs');
const state = JSON.parse(fs.readFileSync('$STATE_FILE', 'utf-8'));
state.storyResults['${story.id}'] = 'DONE';
state.logs.push('> Completed ${story.id}');
fs.writeFileSync('$STATE_FILE', JSON.stringify(state, null, 2));
  "
else
  echo "Failed ${story.id}" >> "$LOG_FILE"
  sed -i 's/${story.id}: RUNNING/${story.id}: FAIL/' "$PROGRESS_FILE"
  # Update state
  /root/.nvm/versions/node/v22.19.0/bin/node -e "
const fs = require('fs');
const state = JSON.parse(fs.readFileSync('$STATE_FILE', 'utf-8'));
state.storyResults['${story.id}'] = 'FAIL';
state.logs.push('> Failed ${story.id}');
fs.writeFileSync('$STATE_FILE', JSON.stringify(state, null, 2));
  "
fi
echo "" >> "$LOG_FILE"
`).join('\n')}

# Mark completion
/root/.nvm/versions/node/v22.19.0/bin/node -e "
const fs = require('fs');
const state = JSON.parse(fs.readFileSync('$STATE_FILE', 'utf-8'));
state.running = false;
state.completed = true;
state.logs.push('> All stories processed');
fs.writeFileSync('$STATE_FILE', JSON.stringify(state, null, 2));
"

echo "=== RALPH_COMPLETE ===" >> "$LOG_FILE"
`

    fs.writeFileSync(scriptPath, script)
    fs.chmodSync(scriptPath, '755')

    // Clear previous log
    if (fs.existsSync(logPath)) {
      fs.unlinkSync(logPath)
    }

    // Spawn the process in background
    const proc = spawn('bash', [scriptPath], {
      detached: true,
      stdio: 'ignore',
      cwd: projectPath,
      env: {
        ...process.env,
        HOME: '/root',
        PATH: '/root/.nvm/versions/node/v22.19.0/bin:' + process.env.PATH
      }
    })
    proc.unref()

    // Update state with PID
    state.pid = proc.pid || 0
    state.logs.push(`> Spawned process with PID: ${state.pid}`)
    fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))

    return NextResponse.json({
      success: true,
      message: `Started Ralph for ${project}`,
      pid: state.pid
    })

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