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