UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

96 lines 5.17 kB
import { resolveRunCheckout } from './store/index.js'; import { startPreview, detectServeTargets } from './preview.js'; import { scopedKey } from './runtime-keys.js'; import { errorMessage } from './error-message.js'; /** * On-demand app preview (#475), one long-lived preview process per project (plus one per session that * asks, #797), kept alive across the request that opens it and the Stop that closes it. Split out of * the project runtime because it shares nothing with the run half but the project resolver and the * key scheme: previews know nothing about runs, and the run half reaches in only to stop a finished * run's preview. * * Track a preview under its key and evict it the moment it stops serving (stop, or a self-exit: crash * / build error / the user killing it), so status never reports a dead URL and the idempotent open * below never hands back a corpse instead of restarting. * * Since #797 the key carries the session too, because a session serves its OWN worktree: one preview * per project as before, plus one per session that asks for it, each pointing at the tree it belongs * to. Keyed by project alone, a session's Serve booted the project's checkout and showed you code * that session never wrote. */ export function createPreviewRuntime({ homeId, resolveProject }) { const activePreviews = new Map(); const trackPreview = (key, handle) => { activePreviews.set(key, handle); void handle.exited.then(() => { if (activePreviews.get(key) === handle) activePreviews.delete(key); }); }; // The app the user last served per project (#651), so re-serving a monorepo picks it again // without re-choosing. In-memory: a live preview already rehydrates via onPreviewStatus, and // the pick resets on daemon restart (the picker still lists everything). const lastServeTarget = new Map(); const onServeTargets = async (targetProjectId, runId) => { const projectCwd = await resolveProject(targetProjectId); if (!projectCwd) return []; // Detected in the checkout that will actually be served: a session's branch may have added or // removed a servable package, and offering the project's list would offer apps it cannot serve. const serveCwd = await resolveRunCheckout(projectCwd, runId); return detectServeTargets(serveCwd).catch(() => []); }; const onPreview = async (targetProjectId, targetId, runId) => { const projectKey = targetProjectId ?? homeId; const key = scopedKey(projectKey, runId); const existing = activePreviews.get(key); if (existing) return { ok: true, url: existing.url, command: existing.command }; const projectCwd = await resolveProject(targetProjectId); if (!projectCwd) return { ok: false, error: `unknown project: ${targetProjectId}` }; const serveCwd = await resolveRunCheckout(projectCwd, runId); try { // Resolve the pick: an explicit choice, else the one remembered from last time. Both are // matched against the live target list so a stale/unknown id falls back to the root default. // The memory is per project, not per session: which app you serve is a property of the repo. const wantId = targetId ?? lastServeTarget.get(projectKey); const target = wantId ? (await detectServeTargets(serveCwd).catch(() => [])).find(t => t.id === wantId) : undefined; const handle = await startPreview(target ? { cwd: serveCwd, target } : { cwd: serveCwd }); // A racing second open won the slot while we were booting: keep theirs, drop ours. const raced = activePreviews.get(key); if (raced) { await handle.stop().catch(() => { }); return { ok: true, url: raced.url, command: raced.command }; } if (target) lastServeTarget.set(projectKey, target.id); trackPreview(key, handle); return { ok: true, url: handle.url, command: handle.command }; } catch (err) { return { ok: false, error: errorMessage(err) }; } }; const onStopPreview = async (targetProjectId, runId) => { const key = scopedKey(targetProjectId ?? homeId, runId); const handle = activePreviews.get(key); if (!handle) return; activePreviews.delete(key); await handle.stop().catch(() => { }); }; const onPreviewStatus = (targetProjectId, runId) => { const handle = activePreviews.get(scopedKey(targetProjectId ?? homeId, runId)); return handle ? { running: true, url: handle.url, command: handle.command } : { running: false }; }; const dispose = async () => { await Promise.all([...activePreviews.values()].map(p => p.stop().catch(() => { }))); activePreviews.clear(); }; return { preview: { start: onPreview, targets: onServeTargets, stop: onStopPreview, status: onPreviewStatus }, dispose, }; } //# sourceMappingURL=preview-runtime.js.map