154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
/**
|
|
* POST /api/projects/[projectId]/dev-server/ensure
|
|
*
|
|
* Lightweight endpoint called by the preview pane when it loads and finds
|
|
* no running dev server. Checks for a previous server config and restarts
|
|
* it in the background, returning immediately so the UI isn't blocked.
|
|
*
|
|
* Response shapes:
|
|
* { status: 'running', previewUrl } — already up, nothing to do
|
|
* { status: 'starting', previewUrl } — was down, restart fired
|
|
* { status: 'no_history' } — never started before, AI needs to do it
|
|
* { status: 'no_container' } — dev container doesn't exist yet
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { authSession } from "@/lib/auth/session-server";
|
|
import { query, queryOne } from "@/lib/db-postgres";
|
|
import { getWorkspaceById } from "@/lib/workspaces";
|
|
import {
|
|
ensureDevContainer,
|
|
startDevServer,
|
|
probeDevServerReadiness,
|
|
} from "@/lib/dev-container";
|
|
|
|
export async function POST(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ projectId: string }> },
|
|
) {
|
|
const { projectId } = await params;
|
|
|
|
const session = await authSession();
|
|
if (!session?.user?.email) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Load project — verify ownership
|
|
const projectRows = await query<{
|
|
id: string;
|
|
vibn_workspace_id: string | null;
|
|
data: Record<string, unknown>;
|
|
}>(
|
|
`SELECT p.id, p.vibn_workspace_id, p.data
|
|
FROM fs_projects p
|
|
JOIN fs_users u ON u.id = p.user_id
|
|
WHERE p.id = $1 AND u.data->>'email' = $2
|
|
LIMIT 1`,
|
|
[projectId, session.user.email],
|
|
);
|
|
|
|
if (projectRows.length === 0) {
|
|
return NextResponse.json({ error: "Project not found" }, { status: 404 });
|
|
}
|
|
|
|
const project = projectRows[0];
|
|
const projectSlug = (project.data?.slug as string) || project.id;
|
|
const projectName = (project.data?.name as string) || "Project";
|
|
|
|
// 1. Is a dev server already running or starting on the primary port?
|
|
const running = await queryOne<{
|
|
id: string;
|
|
state: string;
|
|
preview_url: string;
|
|
command: string;
|
|
port: number;
|
|
}>(
|
|
`SELECT id, state, preview_url, command, port
|
|
FROM fs_dev_servers
|
|
WHERE project_id = $1
|
|
AND port = 3000
|
|
AND (
|
|
state = 'running' OR
|
|
(state = 'starting' AND started_at > NOW() - INTERVAL '15 minutes')
|
|
)
|
|
ORDER BY started_at DESC LIMIT 1`,
|
|
[projectId],
|
|
);
|
|
|
|
if (running) {
|
|
return NextResponse.json({
|
|
status: running.state === "running" ? "running" : "starting",
|
|
previewUrl: running.preview_url,
|
|
command: running.command,
|
|
port: running.port,
|
|
});
|
|
}
|
|
|
|
// 2. Do we have a previous config to restart from?
|
|
// (Limit to port 3000 since that's what the preview pane embeds)
|
|
const last = await queryOne<{
|
|
command: string;
|
|
port: number;
|
|
preview_url: string;
|
|
}>(
|
|
`SELECT command, port, preview_url
|
|
FROM fs_dev_servers
|
|
WHERE project_id = $1 AND port = 3000
|
|
ORDER BY started_at DESC LIMIT 1`,
|
|
[projectId],
|
|
);
|
|
|
|
const forceStart =
|
|
new URL(request.url).searchParams.get("forceStart") === "true";
|
|
|
|
if (!last && !forceStart) {
|
|
return NextResponse.json({ status: "no_history" });
|
|
}
|
|
|
|
// 3. Load workspace
|
|
if (!project.vibn_workspace_id) {
|
|
return NextResponse.json({ status: "no_container" });
|
|
}
|
|
|
|
const workspace = await getWorkspaceById(project.vibn_workspace_id);
|
|
if (!workspace) {
|
|
return NextResponse.json({ status: "no_container" });
|
|
}
|
|
|
|
// 4. Fire restart in background — don't block the response.
|
|
// If forceStart is true but we have no history, default to Next.js start command.
|
|
const restartOpts = {
|
|
projectId: project.id,
|
|
projectSlug,
|
|
command: last?.command || "next dev -H 0.0.0.0 --no-turbopack",
|
|
port: last?.port || 3000,
|
|
workspace,
|
|
};
|
|
|
|
void (async () => {
|
|
try {
|
|
await ensureDevContainer({
|
|
projectId: project.id,
|
|
projectSlug,
|
|
projectName,
|
|
workspace,
|
|
});
|
|
const row = await startDevServer(restartOpts);
|
|
// Run the readiness probe in background so state transitions
|
|
// from 'starting' → 'running' (or 'failed') in the DB.
|
|
probeDevServerReadiness(project.id, row.id, row.port).catch((err) => {
|
|
console.error("[dev-server/ensure] probe failed:", err?.message);
|
|
});
|
|
} catch (err) {
|
|
console.error("[dev-server/ensure] restart failed:", err);
|
|
}
|
|
})();
|
|
|
|
return NextResponse.json({
|
|
status: "starting",
|
|
previewUrl: last?.preview_url ?? null,
|
|
command: restartOpts.command,
|
|
port: restartOpts.port,
|
|
});
|
|
}
|