- New /api/projects/[projectId]/architecture (GET/POST/PATCH) — reads PRD + phases, calls AI to generate structured monorepo architecture JSON, persists to fs_projects.data.architecture; PATCH sets confirmed flag - Rebuilt Build tab to show the AI-generated recommendation: expandable app cards (tech stack, key screens), shared packages, infrastructure, integrations, and risk notes; confirm button + "adjustable later" note Made-with: Cursor
213 lines
6.7 KiB
TypeScript
213 lines
6.7 KiB
TypeScript
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}/atlas/chat`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
message: prompt,
|
|
session_id: `arch_${projectId}_${Date.now()}`,
|
|
history: [],
|
|
is_init: false,
|
|
tools: [], // no tools needed — just structured generation
|
|
}),
|
|
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 });
|
|
}
|
|
}
|