import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; // --------------------------------------------------------------------------- // GET — return saved architecture (if it exists) // --------------------------------------------------------------------------- export async function GET( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; try { const rows = await query<{ data: any }>( `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId] ); const data = rows[0]?.data ?? {}; return NextResponse.json({ architecture: data.architecture ?? null, prd: data.prd ?? null, }); } catch { return NextResponse.json({ architecture: null, prd: null }); } } // --------------------------------------------------------------------------- // POST — generate architecture recommendation from PRD using AI // --------------------------------------------------------------------------- export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; const body = await req.json().catch(() => ({})); const forceRegenerate = body.forceRegenerate === true; // Load project PRD + phases let prd: string | null = null; let phases: any[] = []; try { const rows = await query<{ data: any }>( `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId] ); const data = rows[0]?.data ?? {}; prd = data.prd ?? null; // Return cached architecture if it exists and not forcing regenerate if (data.architecture && !forceRegenerate) { return NextResponse.json({ architecture: data.architecture, cached: true }); } } catch { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } if (!prd) { return NextResponse.json({ error: "No PRD found — complete discovery first" }, { status: 400 }); } try { const phaseRows = await query<{ phase: string; title: string; summary: string; data: any }>( `SELECT phase, title, summary, data FROM atlas_phases WHERE project_id = $1 ORDER BY saved_at ASC`, [projectId] ); phases = phaseRows; } catch { /* phases optional */ } // Build a concise context string from phases const phaseContext = phases.map(p => `## ${p.title}\n${p.summary}\n${JSON.stringify(p.data, null, 2)}` ).join("\n\n"); const prompt = `You are a senior software architect. Analyse the following Product Requirements Document and recommend a technical architecture for a Turborepo monorepo. Return ONLY a valid JSON object (no markdown, no explanation) with this exact structure: { "productName": "string", "productType": "string (e.g. PWA Game, SaaS, Marketplace, Internal Tool)", "summary": "2-3 sentence plain-English summary of the recommended architecture", "apps": [ { "name": "string (e.g. web, api, simulator)", "type": "string (e.g. Next.js 15, Express API, Node.js service)", "description": "string — what this app does", "tech": ["string array of key technologies"], "screens": ["string array — key screens/routes if applicable, else empty"] } ], "packages": [ { "name": "string (e.g. db, types, ui)", "description": "string — what this shared package contains" } ], "infrastructure": [ { "name": "string (e.g. PostgreSQL, Redis, Background Jobs)", "reason": "string — why this is needed based on the PRD" } ], "integrations": [ { "name": "string (e.g. Ad Network SDK)", "required": true, "notes": "string" } ], "designSurfaces": ["string array — e.g. Web App, Mobile PWA, Admin"], "riskNotes": ["string array — 1-2 key architectural risks from the PRD"] } Be specific to this product. Do not use generic boilerplate — base your decisions on the PRD content. --- DISCOVERY PHASES --- ${phaseContext} --- PRD --- ${prd}`; try { const res = await fetch(`${AGENT_RUNNER_URL}/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }), signal: AbortSignal.timeout(120_000), }); if (!res.ok) { throw new Error(`Agent runner responded ${res.status}`); } const data = await res.json(); const raw = data.reply ?? ""; // Extract JSON from response (strip any accidental markdown) const jsonMatch = raw.match(/\{[\s\S]*\}/); if (!jsonMatch) throw new Error("No JSON in response"); const architecture = JSON.parse(jsonMatch[0]); // Persist to project data await query( `UPDATE fs_projects SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architecture}', $2::jsonb, true), updated_at = NOW() WHERE id = $1`, [projectId, JSON.stringify(architecture)] ); return NextResponse.json({ architecture, cached: false }); } catch (err) { console.error("[architecture] Generation failed:", err); return NextResponse.json( { error: "Architecture generation failed. Please try again." }, { status: 500 } ); } } // --------------------------------------------------------------------------- // PATCH — confirm architecture (sets architectureConfirmed flag) // --------------------------------------------------------------------------- export async function PATCH( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; try { await query( `UPDATE fs_projects SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architectureConfirmed}', 'true'::jsonb, true), updated_at = NOW() WHERE id = $1`, [projectId] ); return NextResponse.json({ confirmed: true }); } catch { return NextResponse.json({ error: "Failed to confirm" }, { status: 500 }); } }