/** * Persistent dev-server configuration store. * Closes BETA_LAUNCH_PLAN P6.B1. * * When `dev_server_start` succeeds, the MCP tool should call * `upsertDevServerConfig` so the project page can auto-resume the * server on next mount without requiring the user to re-type the * command (see P6.B2 for the auto-resume hook). * * Schema: * fs_project_dev_servers * project_id UUID PK → fs_projects.id * command TEXT NOT NULL e.g. "cd myapp && npm run dev" * port INT NOT NULL e.g. 3000 * framework TEXT e.g. "nextjs", "vite", "express" * preview_url TEXT last known *.preview.vibnai.com URL * last_started_at TIMESTAMPTZ * status TEXT CHECK IN ('running','stopped','crashed') * updated_at TIMESTAMPTZ DEFAULT NOW() */ import { query } from "@/lib/db-postgres"; import { log } from "@/lib/server/logger"; let tableReady = false; async function ensureTable() { if (tableReady) return; await query(` CREATE TABLE IF NOT EXISTS fs_project_dev_servers ( project_id TEXT PRIMARY KEY, command TEXT NOT NULL, port INT NOT NULL, framework TEXT, preview_url TEXT, last_started_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'stopped' CHECK (status IN ('running', 'stopped', 'crashed')), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); tableReady = true; } export interface DevServerConfig { projectId: string; command: string; port: number; framework?: string; previewUrl?: string; status: "running" | "stopped" | "crashed"; } /** Called by the MCP dev_server_start handler after a successful start. */ export async function upsertDevServerConfig( cfg: DevServerConfig, ): Promise { try { await ensureTable(); await query( `INSERT INTO fs_project_dev_servers (project_id, command, port, framework, preview_url, last_started_at, status, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), $6, NOW()) ON CONFLICT (project_id) DO UPDATE SET command = EXCLUDED.command, port = EXCLUDED.port, framework = COALESCE(EXCLUDED.framework, fs_project_dev_servers.framework), preview_url = COALESCE(EXCLUDED.preview_url, fs_project_dev_servers.preview_url), last_started_at = NOW(), status = EXCLUDED.status, updated_at = NOW()`, [ cfg.projectId, cfg.command, cfg.port, cfg.framework ?? null, cfg.previewUrl ?? null, cfg.status, ], ); } catch (err) { log.warn("dev-server-state: upsert failed (non-fatal)", { projectId: cfg.projectId, err: err instanceof Error ? err.message : String(err), }); } } /** Update just the status (e.g. on stop / crash). */ export async function setDevServerStatus( projectId: string, status: "running" | "stopped" | "crashed", ): Promise { try { await ensureTable(); await query( `UPDATE fs_project_dev_servers SET status = $2, updated_at = NOW() WHERE project_id = $1`, [projectId, status], ); } catch (err) { log.warn("dev-server-state: status update failed (non-fatal)", { projectId, err: err instanceof Error ? err.message : String(err), }); } } /** Returns the last-known dev server config for a project, or null. */ export async function getDevServerConfig( projectId: string, ): Promise { try { await ensureTable(); const rows = await query<{ project_id: string; command: string; port: number; framework: string | null; preview_url: string | null; status: string; }>( `SELECT project_id, command, port, framework, preview_url, status FROM fs_project_dev_servers WHERE project_id = $1`, [projectId], ); if (!rows[0]) return null; const r = rows[0]; return { projectId: r.project_id, command: r.command, port: r.port, framework: r.framework ?? undefined, previewUrl: r.preview_url ?? undefined, status: r.status as "running" | "stopped" | "crashed", }; } catch { return null; } }