@dfosco/storyboard 0.5.0-beta.44 → 0.5.0-beta.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/client/index.js +15 -0
- package/dist/runtime/client/index.js.map +1 -1
- package/dist/runtime/index.js +15 -0
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
- package/src/internals/ArtifactForm/ArtifactForm.jsx +52 -1
- package/src/internals/ArtifactForm/ArtifactForm.module.css +6 -0
- package/src/internals/ArtifactForm/artifactSchemas.js +35 -35
- package/src/runtime/client/index.ts +22 -0
|
@@ -221,6 +221,21 @@ var RuntimeClient = class {
|
|
|
221
221
|
try {
|
|
222
222
|
const result = await request(this.baseUrl, "GET", "/health", void 0, Health);
|
|
223
223
|
if (this.autoStart && CLIENT_VERSION !== "0.0.0" && result.version !== "0.0.0" && result.version !== CLIENT_VERSION) {
|
|
224
|
+
let activeCount = 0;
|
|
225
|
+
try {
|
|
226
|
+
const listRes = await fetch(`${this.baseUrl}/devserver/list`);
|
|
227
|
+
if (listRes.ok) {
|
|
228
|
+
const data = await listRes.json();
|
|
229
|
+
activeCount = Array.isArray(data.devServers) ? data.devServers.length : 0;
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
if (activeCount > 0) {
|
|
234
|
+
console.warn(
|
|
235
|
+
`[storyboard] daemon version ${result.version} differs from client ${CLIENT_VERSION}, but ${activeCount} dev server(s) are active. Reusing the existing daemon. Run \`sb reset\` to restart it once those are stopped.`
|
|
236
|
+
);
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
224
239
|
killExistingDaemon();
|
|
225
240
|
await new Promise((r) => setTimeout(r, 250));
|
|
226
241
|
await spawnDaemon(this.baseUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/runtime/client/index.ts","../../../src/runtime/schema/identity.ts","../../../src/runtime/schema/devserver.ts","../../../src/runtime/schema/api.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport {\n AcquireRequest,\n AcquireResponse,\n Health,\n PoolStatus,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n} from '../schema/index.js'\nimport type { z } from 'zod'\n\n/**\n * Typed JS/TS client for the Storyboard Runtime daemon.\n *\n * Consumers should always go through this client rather than hand-rolling\n * `fetch` calls — the client is the only place where the daemon's URL is\n * known, and it's the only place where on-demand daemon spawning happens.\n */\n\nconst RUNTIME_BASE = 'http://127.0.0.1:4321'\n\nexport interface RuntimeClientOptions {\n /** Override base URL (mostly for tests). */\n baseUrl?: string\n /** Auto-start the daemon if it isn't running. Default: true. */\n autoStart?: boolean\n}\n\nexport class RuntimeRequestError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: z.infer<typeof RuntimeError>['code'],\n public readonly details?: unknown,\n ) {\n super(message)\n this.name = 'RuntimeRequestError'\n }\n}\n\nasync function request<S extends z.ZodTypeAny>(\n baseUrl: string,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n responseSchema: S | null,\n): Promise<S extends z.ZodTypeAny ? z.output<S> : void> {\n const init: RequestInit = {\n method,\n headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n }\n let res: Response\n try {\n res = await fetch(`${baseUrl}${path}`, init)\n } catch (err) {\n throw new RuntimeRequestError(\n `Cannot reach Storyboard Runtime at ${baseUrl} — is the daemon running? (${(err as Error).message})`,\n 0,\n 'INTERNAL',\n )\n }\n const text = await res.text()\n let parsed: unknown\n try { parsed = text ? JSON.parse(text) : {} }\n catch { parsed = { error: text, code: 'INTERNAL' } }\n\n if (!res.ok) {\n const err = RuntimeError.safeParse(parsed)\n if (err.success) {\n throw new RuntimeRequestError(err.data.error, res.status, err.data.code, err.data.details)\n }\n throw new RuntimeRequestError(`HTTP ${res.status}`, res.status, 'INTERNAL', parsed)\n }\n if (responseSchema === null) return undefined as never\n return responseSchema.parse(parsed) as never\n}\n\n/**\n * Spawn the daemon as a detached child. Resolves once the health endpoint\n * answers (or rejects after a short timeout).\n */\nasync function spawnDaemon(baseUrl: string): Promise<void> {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin/storyboard-runtime.js lives next to dist/, two levels up from\n // dist/runtime/client/index.js (the published path).\n const binPath = resolve(here, '..', '..', '..', 'bin', 'storyboard-runtime.js')\n const child = spawn(process.execPath, [binPath], {\n detached: true,\n stdio: 'ignore',\n env: process.env,\n })\n child.unref()\n\n // Poll /health until the daemon is up.\n const deadline = Date.now() + 5000\n while (Date.now() < deadline) {\n try {\n const r = await fetch(`${baseUrl}/health`)\n if (r.ok) return\n } catch { /* not up yet */ }\n await new Promise(r => setTimeout(r, 100))\n }\n throw new Error(\n `Storyboard Runtime did not become ready within 5s ` +\n `(tried to spawn ${binPath})`,\n )\n}\n\n/**\n * Read the @dfosco/storyboard package.json version that this client is\n * shipping with. Used to detect mismatches against a long-lived daemon\n * that may have been spawned by a previous install.\n */\nfunction readClientVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n }\n }\n } catch { /* ignore */ }\n return '0.0.0'\n}\n\nconst CLIENT_VERSION = readClientVersion()\n\n/**\n * Send SIGTERM to the daemon PID and clear its lock/pid files so\n * spawnDaemon() can start a fresh one.\n */\nfunction killExistingDaemon(): void {\n try {\n const pidPath = resolve(process.env.HOME || '', '.storyboard', 'runtime.pid')\n if (!existsSync(pidPath)) return\n const pid = Number(readFileSync(pidPath, 'utf8').trim())\n if (Number.isFinite(pid) && pid > 0) {\n try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }\n }\n } catch { /* ignore */ }\n}\n\nexport class RuntimeClient {\n readonly baseUrl: string\n readonly autoStart: boolean\n\n constructor(opts: RuntimeClientOptions = {}) {\n this.baseUrl = opts.baseUrl ?? RUNTIME_BASE\n this.autoStart = opts.autoStart !== false\n }\n\n async health(): Promise<Health> {\n try {\n const result = await request(this.baseUrl, 'GET', '/health', undefined, Health)\n // Auto-respawn on version mismatch — a long-lived daemon from a\n // previous install otherwise keeps serving stale code after upgrade.\n // Skip when client reports 0.0.0 (dev/source layout where package\n // version isn't meaningful).\n if (\n this.autoStart &&\n CLIENT_VERSION !== '0.0.0' &&\n result.version !== '0.0.0' &&\n result.version !== CLIENT_VERSION\n ) {\n killExistingDaemon()\n // Give the OS a moment to release port 4321\n await new Promise(r => setTimeout(r, 250))\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n return result\n } catch (err) {\n if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n throw err\n }\n }\n\n async acquire(input: z.input<typeof AcquireRequest>): Promise<AcquireResponse> {\n // Force a health check first — this is the sole codepath that detects\n // a stale daemon (different version, crashed mid-restart, etc.) and\n // respawns it. Without this, `sb run` would happily POST against an\n // outdated daemon and inherit all of its bugs.\n if (this.autoStart) {\n try { await this.health() } catch { /* health() will throw on hard failure; let acquire surface it */ }\n }\n const body = AcquireRequest.parse(input)\n return request(this.baseUrl, 'POST', '/devserver/acquire', body, AcquireResponse)\n }\n\n async release(input: z.input<typeof ReleaseRequest>): Promise<void> {\n const body = ReleaseRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/release', body, null)\n }\n\n async proxyState(): Promise<ProxyState> {\n return request(this.baseUrl, 'GET', '/proxy/state', undefined, ProxyState)\n }\n\n async proxyUpsert(input: z.input<typeof ProxyUpsertRequest>): Promise<ProxyState> {\n const body = ProxyUpsertRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/upsert', body, ProxyState)\n }\n\n async proxyRemove(input: z.input<typeof ProxyRemoveRequest>): Promise<ProxyState> {\n const body = ProxyRemoveRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/remove', body, ProxyState)\n }\n\n async poolStatus(): Promise<PoolStatus> {\n return request(this.baseUrl, 'GET', '/pool/status', undefined, PoolStatus)\n }\n}\n\n/** Default singleton client for casual callers. */\nexport const runtime = new RuntimeClient()\n","import { z } from 'zod'\n\n/**\n * The legacy/default devDomain. Acquire requests using this value are rejected\n * unless `allowDefaultDomain` is set — see DevServerOrchestrator for details.\n */\nexport const DEFAULT_DEV_DOMAIN = 'storyboard'\n\n/**\n * A devDomain identifies a Storyboard repo on this machine.\n *\n * The literal default value `\"storyboard\"` is intentionally *not* allowed by\n * `acquire` (see schema/acquire.ts) — every checkout MUST set its own\n * `devDomain` in `storyboard.config.json`. This is the structural fix for H3\n * in the server-state RCA: two repos can never share a host space.\n *\n * Allowed: lowercase letters, digits, hyphens. Must start with a letter.\n * 1–32 chars. The runtime constructs the public host as `${devDomain}.localhost`.\n */\nexport const DevDomain = z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z][a-z0-9-]*$/, 'devDomain must match /^[a-z][a-z0-9-]*$/')\n .brand<'DevDomain'>()\nexport type DevDomain = z.infer<typeof DevDomain>\n\n/**\n * A worktree name. `\"main\"` is reserved for the repo root.\n *\n * Names are URL-safe by construction so we never have to escape them when\n * building branch URLs (`/branch--<name>/...`).\n */\nexport const WorktreeName = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9._-]*$/i, 'worktree name must be URL-safe')\n .brand<'WorktreeName'>()\nexport type WorktreeName = z.infer<typeof WorktreeName>\n\n/**\n * A TCP port the runtime has leased to a devserver. The runtime is the sole\n * authority for port allocation; clients never pick their own port.\n */\nexport const Port = z.number().int().min(1024).max(65535).brand<'Port'>()\nexport type Port = z.infer<typeof Port>\n\n/**\n * The composite key `(devDomain, worktree)` uniquely identifies a devserver.\n *\n * The runtime guarantees at most one devserver per slot — illegal collisions\n * (e.g. two repos trying to claim `(storyboard, main)`) are rejected with\n * `409 CONFLICT` rather than silently overwriting routes.\n */\nexport const DevServerSlot = z.object({\n devDomain: DevDomain,\n worktree: WorktreeName,\n})\nexport type DevServerSlot = z.infer<typeof DevServerSlot>\n\n/**\n * Convert a slot to its canonical string key, used for map lookups and\n * logging. Format: `${devDomain}::${worktree}`.\n */\nexport function slotKey(slot: DevServerSlot): string {\n return `${slot.devDomain}::${slot.worktree}`\n}\n","import { z } from 'zod'\nimport { DevDomain, DevServerSlot, Port, WorktreeName } from './identity.js'\n\n/**\n * DevServer lifecycle FSM.\n *\n * ```\n * spawning → ready → stopped\n * └────────────────┘ (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'stopped', // process exited, slot freed, port returned to the pool\n])\nexport type DevServerStatus = z.infer<typeof DevServerStatus>\n\n/** Allowed FSM transitions. Centralised so misuse is a one-line review catch. */\nexport const ALLOWED_TRANSITIONS: Record<DevServerStatus, readonly DevServerStatus[]> = {\n spawning: ['ready', 'stopped'],\n ready: ['stopped'],\n stopped: [],\n} as const\n\nexport class IllegalTransitionError extends Error {\n constructor(from: DevServerStatus, to: DevServerStatus) {\n super(`Illegal devserver transition: ${from} → ${to}`)\n this.name = 'IllegalTransitionError'\n }\n}\n\nexport function assertTransition(from: DevServerStatus, to: DevServerStatus): void {\n if (!ALLOWED_TRANSITIONS[from].includes(to)) {\n throw new IllegalTransitionError(from, to)\n }\n}\n\n/**\n * A devserver record as exposed by the runtime API. Always bound to a slot\n * after acquire (no pre-spawned pool members), so `slot` and `cwd` are required.\n */\nexport const DevServer = z.object({\n id: z.string().uuid(),\n pid: z.number().int().positive(),\n port: Port,\n status: DevServerStatus,\n slot: DevServerSlot,\n /** Absolute path of the worktree directory. */\n cwd: z.string(),\n /** ISO timestamp; immutable after spawn. */\n spawnedAt: z.string().datetime(),\n /** ISO timestamp of last status change. */\n updatedAt: z.string().datetime(),\n})\nexport type DevServer = z.infer<typeof DevServer>\n\n/**\n * A lease handed to a CLI client when it acquires a devserver.\n *\n * Leases are the *only* way a client controls a devserver — the runtime\n * refuses commands without a valid leaseId. They live as long as the\n * acquiring CLI process; expiry is a far-future sentinel.\n */\nexport const Lease = z.object({\n id: z.string().uuid(),\n devServerId: z.string().uuid(),\n slot: DevServerSlot,\n /** Public proxy URL the client should print to the user. Authoritative. */\n url: z.string().url(),\n /** Far-future sentinel — leases don't actually expire. */\n expiresAt: z.string().datetime(),\n})\nexport type Lease = z.infer<typeof Lease>\n\n/**\n * A Caddy proxy route owned by the runtime. The `@id` is always the devDomain;\n * this lets the runtime PATCH a single route in place without touching others.\n *\n * `upstreams` is keyed by plain string (validated elsewhere as WorktreeName)\n * to avoid `Partial<Record<branded, …>>` shenanigans at the value-spread sites.\n */\nexport const ProxyRoute = z.object({\n devDomain: DevDomain,\n host: z.string(),\n /** worktree name → upstream port. `main` is the host's catch-all. */\n upstreams: z.record(z.string(), Port),\n})\nexport type ProxyRoute = z.infer<typeof ProxyRoute>\n","import { z } from 'zod'\nimport { DevServerSlot, Port } from './identity.js'\nimport { DevServer, Lease, ProxyRoute } from './devserver.js'\n\n/**\n * `POST /devserver/acquire` — request a devserver for a `(devDomain, worktree)` slot.\n *\n * If a devserver already exists for the slot, the runtime returns its existing\n * lease. Otherwise it spawns a new Vite process. The slot is locked for the\n * duration of the call.\n */\nexport const AcquireRequest = z.object({\n slot: DevServerSlot,\n /** Absolute path of the worktree directory; the runtime spawns Vite with `cwd: targetCwd`. */\n targetCwd: z.string().min(1),\n /**\n * Escape hatch for the deprecated default devDomain `\"storyboard\"`. CI and\n * one-off scripts may pass true; the CLI never does.\n */\n allowDefaultDomain: z.boolean().default(false),\n})\nexport type AcquireRequest = z.infer<typeof AcquireRequest>\n\nexport const AcquireResponse = z.object({\n lease: Lease,\n devServer: DevServer,\n})\nexport type AcquireResponse = z.infer<typeof AcquireResponse>\n\n/** `POST /devserver/release` — relinquish the lease and stop the devserver. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `GET /proxy/state` — current routing table the runtime believes Caddy holds. */\nexport const ProxyState = z.object({\n routes: z.array(ProxyRoute),\n caddyReachable: z.boolean(),\n})\nexport type ProxyState = z.infer<typeof ProxyState>\n\n/** `GET /pool/status` — kept for backward compat; always reports zeros. */\nexport const PoolStatus = z.object({\n warm: z.number().int().nonnegative(),\n bound: z.number().int().nonnegative(),\n capacity: z.number().int().nonnegative(),\n})\nexport type PoolStatus = z.infer<typeof PoolStatus>\n\n/** `POST /proxy/upsert` — bind (devDomain, worktree) → port in the proxy. */\nexport const ProxyUpsertRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n port: z.number(),\n})\nexport type ProxyUpsertRequest = z.infer<typeof ProxyUpsertRequest>\n\n/** `POST /proxy/remove` — drop a worktree's route from the proxy. */\nexport const ProxyRemoveRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n})\nexport type ProxyRemoveRequest = z.infer<typeof ProxyRemoveRequest>\n\n/** `GET /health` — daemon liveness probe. */\nexport const Health = z.object({\n ok: z.literal(true),\n version: z.string(),\n uptimeSeconds: z.number().nonnegative(),\n port: Port,\n})\nexport type Health = z.infer<typeof Health>\n\n/** Runtime error envelope. All non-2xx responses share this shape. */\nexport const RuntimeError = z.object({\n error: z.string(),\n code: z.enum([\n 'NOT_IMPLEMENTED',\n 'BAD_REQUEST',\n 'CONFLICT',\n 'NOT_FOUND',\n 'FORBIDDEN_DEFAULT_DOMAIN',\n 'INTERNAL',\n 'CADDY_UNREACHABLE',\n 'PORT_EXHAUSTED',\n 'TIMEOUT',\n ]),\n details: z.unknown().optional(),\n})\nexport type RuntimeError = z.infer<typeof RuntimeError>\n"],"mappings":"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","names":["z","z","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/runtime/client/index.ts","../../../src/runtime/schema/identity.ts","../../../src/runtime/schema/devserver.ts","../../../src/runtime/schema/api.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport {\n AcquireRequest,\n AcquireResponse,\n Health,\n PoolStatus,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n} from '../schema/index.js'\nimport type { z } from 'zod'\n\n/**\n * Typed JS/TS client for the Storyboard Runtime daemon.\n *\n * Consumers should always go through this client rather than hand-rolling\n * `fetch` calls — the client is the only place where the daemon's URL is\n * known, and it's the only place where on-demand daemon spawning happens.\n */\n\nconst RUNTIME_BASE = 'http://127.0.0.1:4321'\n\nexport interface RuntimeClientOptions {\n /** Override base URL (mostly for tests). */\n baseUrl?: string\n /** Auto-start the daemon if it isn't running. Default: true. */\n autoStart?: boolean\n}\n\nexport class RuntimeRequestError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: z.infer<typeof RuntimeError>['code'],\n public readonly details?: unknown,\n ) {\n super(message)\n this.name = 'RuntimeRequestError'\n }\n}\n\nasync function request<S extends z.ZodTypeAny>(\n baseUrl: string,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n responseSchema: S | null,\n): Promise<S extends z.ZodTypeAny ? z.output<S> : void> {\n const init: RequestInit = {\n method,\n headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n }\n let res: Response\n try {\n res = await fetch(`${baseUrl}${path}`, init)\n } catch (err) {\n throw new RuntimeRequestError(\n `Cannot reach Storyboard Runtime at ${baseUrl} — is the daemon running? (${(err as Error).message})`,\n 0,\n 'INTERNAL',\n )\n }\n const text = await res.text()\n let parsed: unknown\n try { parsed = text ? JSON.parse(text) : {} }\n catch { parsed = { error: text, code: 'INTERNAL' } }\n\n if (!res.ok) {\n const err = RuntimeError.safeParse(parsed)\n if (err.success) {\n throw new RuntimeRequestError(err.data.error, res.status, err.data.code, err.data.details)\n }\n throw new RuntimeRequestError(`HTTP ${res.status}`, res.status, 'INTERNAL', parsed)\n }\n if (responseSchema === null) return undefined as never\n return responseSchema.parse(parsed) as never\n}\n\n/**\n * Spawn the daemon as a detached child. Resolves once the health endpoint\n * answers (or rejects after a short timeout).\n */\nasync function spawnDaemon(baseUrl: string): Promise<void> {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin/storyboard-runtime.js lives next to dist/, two levels up from\n // dist/runtime/client/index.js (the published path).\n const binPath = resolve(here, '..', '..', '..', 'bin', 'storyboard-runtime.js')\n const child = spawn(process.execPath, [binPath], {\n detached: true,\n stdio: 'ignore',\n env: process.env,\n })\n child.unref()\n\n // Poll /health until the daemon is up.\n const deadline = Date.now() + 5000\n while (Date.now() < deadline) {\n try {\n const r = await fetch(`${baseUrl}/health`)\n if (r.ok) return\n } catch { /* not up yet */ }\n await new Promise(r => setTimeout(r, 100))\n }\n throw new Error(\n `Storyboard Runtime did not become ready within 5s ` +\n `(tried to spawn ${binPath})`,\n )\n}\n\n/**\n * Read the @dfosco/storyboard package.json version that this client is\n * shipping with. Used to detect mismatches against a long-lived daemon\n * that may have been spawned by a previous install.\n */\nfunction readClientVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n }\n }\n } catch { /* ignore */ }\n return '0.0.0'\n}\n\nconst CLIENT_VERSION = readClientVersion()\n\n/**\n * Send SIGTERM to the daemon PID and clear its lock/pid files so\n * spawnDaemon() can start a fresh one.\n */\nfunction killExistingDaemon(): void {\n try {\n const pidPath = resolve(process.env.HOME || '', '.storyboard', 'runtime.pid')\n if (!existsSync(pidPath)) return\n const pid = Number(readFileSync(pidPath, 'utf8').trim())\n if (Number.isFinite(pid) && pid > 0) {\n try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }\n }\n } catch { /* ignore */ }\n}\n\nexport class RuntimeClient {\n readonly baseUrl: string\n readonly autoStart: boolean\n\n constructor(opts: RuntimeClientOptions = {}) {\n this.baseUrl = opts.baseUrl ?? RUNTIME_BASE\n this.autoStart = opts.autoStart !== false\n }\n\n async health(): Promise<Health> {\n try {\n const result = await request(this.baseUrl, 'GET', '/health', undefined, Health)\n // Auto-respawn on version mismatch — a long-lived daemon from a\n // previous install otherwise keeps serving stale code after upgrade.\n // Skip when client reports 0.0.0 (dev/source layout where package\n // version isn't meaningful).\n if (\n this.autoStart &&\n CLIENT_VERSION !== '0.0.0' &&\n result.version !== '0.0.0' &&\n result.version !== CLIENT_VERSION\n ) {\n // Don't kill a daemon that's actively hosting other dev servers —\n // doing so would SIGTERM every Vite child including ones owned by\n // unrelated repos. Multi-repo coexistence is the entire point of\n // running a shared daemon. Warn and reuse instead; the user can\n // manually `sb reset` when they're ready to upgrade.\n let activeCount = 0\n try {\n const listRes = await fetch(`${this.baseUrl}/devserver/list`)\n if (listRes.ok) {\n const data = await listRes.json() as { devServers?: unknown[] }\n activeCount = Array.isArray(data.devServers) ? data.devServers.length : 0\n }\n } catch { /* fall through; treat as zero */ }\n if (activeCount > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `[storyboard] daemon version ${result.version} differs from client ${CLIENT_VERSION}, ` +\n `but ${activeCount} dev server(s) are active. Reusing the existing daemon. ` +\n `Run \\`sb reset\\` to restart it once those are stopped.`,\n )\n return result\n }\n killExistingDaemon()\n // Give the OS a moment to release port 4321\n await new Promise(r => setTimeout(r, 250))\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n return result\n } catch (err) {\n if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n throw err\n }\n }\n\n async acquire(input: z.input<typeof AcquireRequest>): Promise<AcquireResponse> {\n // Force a health check first — this is the sole codepath that detects\n // a stale daemon (different version, crashed mid-restart, etc.) and\n // respawns it. Without this, `sb run` would happily POST against an\n // outdated daemon and inherit all of its bugs.\n if (this.autoStart) {\n try { await this.health() } catch { /* health() will throw on hard failure; let acquire surface it */ }\n }\n const body = AcquireRequest.parse(input)\n return request(this.baseUrl, 'POST', '/devserver/acquire', body, AcquireResponse)\n }\n\n async release(input: z.input<typeof ReleaseRequest>): Promise<void> {\n const body = ReleaseRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/release', body, null)\n }\n\n async proxyState(): Promise<ProxyState> {\n return request(this.baseUrl, 'GET', '/proxy/state', undefined, ProxyState)\n }\n\n async proxyUpsert(input: z.input<typeof ProxyUpsertRequest>): Promise<ProxyState> {\n const body = ProxyUpsertRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/upsert', body, ProxyState)\n }\n\n async proxyRemove(input: z.input<typeof ProxyRemoveRequest>): Promise<ProxyState> {\n const body = ProxyRemoveRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/remove', body, ProxyState)\n }\n\n async poolStatus(): Promise<PoolStatus> {\n return request(this.baseUrl, 'GET', '/pool/status', undefined, PoolStatus)\n }\n}\n\n/** Default singleton client for casual callers. */\nexport const runtime = new RuntimeClient()\n","import { z } from 'zod'\n\n/**\n * The legacy/default devDomain. Acquire requests using this value are rejected\n * unless `allowDefaultDomain` is set — see DevServerOrchestrator for details.\n */\nexport const DEFAULT_DEV_DOMAIN = 'storyboard'\n\n/**\n * A devDomain identifies a Storyboard repo on this machine.\n *\n * The literal default value `\"storyboard\"` is intentionally *not* allowed by\n * `acquire` (see schema/acquire.ts) — every checkout MUST set its own\n * `devDomain` in `storyboard.config.json`. This is the structural fix for H3\n * in the server-state RCA: two repos can never share a host space.\n *\n * Allowed: lowercase letters, digits, hyphens. Must start with a letter.\n * 1–32 chars. The runtime constructs the public host as `${devDomain}.localhost`.\n */\nexport const DevDomain = z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z][a-z0-9-]*$/, 'devDomain must match /^[a-z][a-z0-9-]*$/')\n .brand<'DevDomain'>()\nexport type DevDomain = z.infer<typeof DevDomain>\n\n/**\n * A worktree name. `\"main\"` is reserved for the repo root.\n *\n * Names are URL-safe by construction so we never have to escape them when\n * building branch URLs (`/branch--<name>/...`).\n */\nexport const WorktreeName = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9._-]*$/i, 'worktree name must be URL-safe')\n .brand<'WorktreeName'>()\nexport type WorktreeName = z.infer<typeof WorktreeName>\n\n/**\n * A TCP port the runtime has leased to a devserver. The runtime is the sole\n * authority for port allocation; clients never pick their own port.\n */\nexport const Port = z.number().int().min(1024).max(65535).brand<'Port'>()\nexport type Port = z.infer<typeof Port>\n\n/**\n * The composite key `(devDomain, worktree)` uniquely identifies a devserver.\n *\n * The runtime guarantees at most one devserver per slot — illegal collisions\n * (e.g. two repos trying to claim `(storyboard, main)`) are rejected with\n * `409 CONFLICT` rather than silently overwriting routes.\n */\nexport const DevServerSlot = z.object({\n devDomain: DevDomain,\n worktree: WorktreeName,\n})\nexport type DevServerSlot = z.infer<typeof DevServerSlot>\n\n/**\n * Convert a slot to its canonical string key, used for map lookups and\n * logging. Format: `${devDomain}::${worktree}`.\n */\nexport function slotKey(slot: DevServerSlot): string {\n return `${slot.devDomain}::${slot.worktree}`\n}\n","import { z } from 'zod'\nimport { DevDomain, DevServerSlot, Port, WorktreeName } from './identity.js'\n\n/**\n * DevServer lifecycle FSM.\n *\n * ```\n * spawning → ready → stopped\n * └────────────────┘ (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'stopped', // process exited, slot freed, port returned to the pool\n])\nexport type DevServerStatus = z.infer<typeof DevServerStatus>\n\n/** Allowed FSM transitions. Centralised so misuse is a one-line review catch. */\nexport const ALLOWED_TRANSITIONS: Record<DevServerStatus, readonly DevServerStatus[]> = {\n spawning: ['ready', 'stopped'],\n ready: ['stopped'],\n stopped: [],\n} as const\n\nexport class IllegalTransitionError extends Error {\n constructor(from: DevServerStatus, to: DevServerStatus) {\n super(`Illegal devserver transition: ${from} → ${to}`)\n this.name = 'IllegalTransitionError'\n }\n}\n\nexport function assertTransition(from: DevServerStatus, to: DevServerStatus): void {\n if (!ALLOWED_TRANSITIONS[from].includes(to)) {\n throw new IllegalTransitionError(from, to)\n }\n}\n\n/**\n * A devserver record as exposed by the runtime API. Always bound to a slot\n * after acquire (no pre-spawned pool members), so `slot` and `cwd` are required.\n */\nexport const DevServer = z.object({\n id: z.string().uuid(),\n pid: z.number().int().positive(),\n port: Port,\n status: DevServerStatus,\n slot: DevServerSlot,\n /** Absolute path of the worktree directory. */\n cwd: z.string(),\n /** ISO timestamp; immutable after spawn. */\n spawnedAt: z.string().datetime(),\n /** ISO timestamp of last status change. */\n updatedAt: z.string().datetime(),\n})\nexport type DevServer = z.infer<typeof DevServer>\n\n/**\n * A lease handed to a CLI client when it acquires a devserver.\n *\n * Leases are the *only* way a client controls a devserver — the runtime\n * refuses commands without a valid leaseId. They live as long as the\n * acquiring CLI process; expiry is a far-future sentinel.\n */\nexport const Lease = z.object({\n id: z.string().uuid(),\n devServerId: z.string().uuid(),\n slot: DevServerSlot,\n /** Public proxy URL the client should print to the user. Authoritative. */\n url: z.string().url(),\n /** Far-future sentinel — leases don't actually expire. */\n expiresAt: z.string().datetime(),\n})\nexport type Lease = z.infer<typeof Lease>\n\n/**\n * A Caddy proxy route owned by the runtime. The `@id` is always the devDomain;\n * this lets the runtime PATCH a single route in place without touching others.\n *\n * `upstreams` is keyed by plain string (validated elsewhere as WorktreeName)\n * to avoid `Partial<Record<branded, …>>` shenanigans at the value-spread sites.\n */\nexport const ProxyRoute = z.object({\n devDomain: DevDomain,\n host: z.string(),\n /** worktree name → upstream port. `main` is the host's catch-all. */\n upstreams: z.record(z.string(), Port),\n})\nexport type ProxyRoute = z.infer<typeof ProxyRoute>\n","import { z } from 'zod'\nimport { DevServerSlot, Port } from './identity.js'\nimport { DevServer, Lease, ProxyRoute } from './devserver.js'\n\n/**\n * `POST /devserver/acquire` — request a devserver for a `(devDomain, worktree)` slot.\n *\n * If a devserver already exists for the slot, the runtime returns its existing\n * lease. Otherwise it spawns a new Vite process. The slot is locked for the\n * duration of the call.\n */\nexport const AcquireRequest = z.object({\n slot: DevServerSlot,\n /** Absolute path of the worktree directory; the runtime spawns Vite with `cwd: targetCwd`. */\n targetCwd: z.string().min(1),\n /**\n * Escape hatch for the deprecated default devDomain `\"storyboard\"`. CI and\n * one-off scripts may pass true; the CLI never does.\n */\n allowDefaultDomain: z.boolean().default(false),\n})\nexport type AcquireRequest = z.infer<typeof AcquireRequest>\n\nexport const AcquireResponse = z.object({\n lease: Lease,\n devServer: DevServer,\n})\nexport type AcquireResponse = z.infer<typeof AcquireResponse>\n\n/** `POST /devserver/release` — relinquish the lease and stop the devserver. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `GET /proxy/state` — current routing table the runtime believes Caddy holds. */\nexport const ProxyState = z.object({\n routes: z.array(ProxyRoute),\n caddyReachable: z.boolean(),\n})\nexport type ProxyState = z.infer<typeof ProxyState>\n\n/** `GET /pool/status` — kept for backward compat; always reports zeros. */\nexport const PoolStatus = z.object({\n warm: z.number().int().nonnegative(),\n bound: z.number().int().nonnegative(),\n capacity: z.number().int().nonnegative(),\n})\nexport type PoolStatus = z.infer<typeof PoolStatus>\n\n/** `POST /proxy/upsert` — bind (devDomain, worktree) → port in the proxy. */\nexport const ProxyUpsertRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n port: z.number(),\n})\nexport type ProxyUpsertRequest = z.infer<typeof ProxyUpsertRequest>\n\n/** `POST /proxy/remove` — drop a worktree's route from the proxy. */\nexport const ProxyRemoveRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n})\nexport type ProxyRemoveRequest = z.infer<typeof ProxyRemoveRequest>\n\n/** `GET /health` — daemon liveness probe. */\nexport const Health = z.object({\n ok: z.literal(true),\n version: z.string(),\n uptimeSeconds: z.number().nonnegative(),\n port: Port,\n})\nexport type Health = z.infer<typeof Health>\n\n/** Runtime error envelope. All non-2xx responses share this shape. */\nexport const RuntimeError = z.object({\n error: z.string(),\n code: z.enum([\n 'NOT_IMPLEMENTED',\n 'BAD_REQUEST',\n 'CONFLICT',\n 'NOT_FOUND',\n 'FORBIDDEN_DEFAULT_DOMAIN',\n 'INTERNAL',\n 'CADDY_UNREACHABLE',\n 'PORT_EXHAUSTED',\n 'TIMEOUT',\n ]),\n details: z.unknown().optional(),\n})\nexport type RuntimeError = z.infer<typeof RuntimeError>\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,eAAe;AACjC,SAAS,YAAY,oBAAoB;;;ACHzC,SAAS,SAAS;AAmBX,IAAM,YAAY,EACtB,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,qBAAqB,0CAA0C,EACrE,MAAmB;AASf,IAAM,eAAe,EACzB,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,2BAA2B,gCAAgC,EACjE,MAAsB;AAOlB,IAAM,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,KAAK,EAAE,MAAc;AAUjE,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,WAAW;AAAA,EACX,UAAU;AACZ,CAAC;;;AC1DD,SAAS,KAAAA,UAAS;AAWX,IAAM,kBAAkBC,GAAE,KAAK;AAAA,EACpC;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AA2BM,IAAM,YAAYC,GAAE,OAAO;AAAA,EAChC,IAAIA,GAAE,OAAO,EAAE,KAAK;AAAA,EACpB,KAAKA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAC/B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA;AAAA,EAEN,KAAKA,GAAE,OAAO;AAAA;AAAA,EAEd,WAAWA,GAAE,OAAO,EAAE,SAAS;AAAA;AAAA,EAE/B,WAAWA,GAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAUM,IAAM,QAAQA,GAAE,OAAO;AAAA,EAC5B,IAAIA,GAAE,OAAO,EAAE,KAAK;AAAA,EACpB,aAAaA,GAAE,OAAO,EAAE,KAAK;AAAA,EAC7B,MAAM;AAAA;AAAA,EAEN,KAAKA,GAAE,OAAO,EAAE,IAAI;AAAA;AAAA,EAEpB,WAAWA,GAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAUM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,WAAW;AAAA,EACX,MAAMA,GAAE,OAAO;AAAA;AAAA,EAEf,WAAWA,GAAE,OAAOA,GAAE,OAAO,GAAG,IAAI;AACtC,CAAC;;;ACvFD,SAAS,KAAAC,UAAS;AAWX,IAAM,iBAAiBC,GAAE,OAAO;AAAA,EACrC,MAAM;AAAA;AAAA,EAEN,WAAWA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3B,oBAAoBA,GAAE,QAAQ,EAAE,QAAQ,KAAK;AAC/C,CAAC;AAGM,IAAM,kBAAkBA,GAAE,OAAO;AAAA,EACtC,OAAO;AAAA,EACP,WAAW;AACb,CAAC;AAIM,IAAM,iBAAiBA,GAAE,OAAO;AAAA,EACrC,SAASA,GAAE,OAAO,EAAE,KAAK;AAC3B,CAAC;AAIM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,QAAQA,GAAE,MAAM,UAAU;AAAA,EAC1B,gBAAgBA,GAAE,QAAQ;AAC5B,CAAC;AAIM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,MAAMA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACnC,OAAOA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,UAAUA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACzC,CAAC;AAIM,IAAM,qBAAqBA,GAAE,OAAO;AAAA,EACzC,WAAWA,GAAE,OAAO;AAAA,EACpB,UAAUA,GAAE,OAAO;AAAA,EACnB,MAAMA,GAAE,OAAO;AACjB,CAAC;AAIM,IAAM,qBAAqBA,GAAE,OAAO;AAAA,EACzC,WAAWA,GAAE,OAAO;AAAA,EACpB,UAAUA,GAAE,OAAO;AACrB,CAAC;AAIM,IAAM,SAASA,GAAE,OAAO;AAAA,EAC7B,IAAIA,GAAE,QAAQ,IAAI;AAAA,EAClB,SAASA,GAAE,OAAO;AAAA,EAClB,eAAeA,GAAE,OAAO,EAAE,YAAY;AAAA,EACtC,MAAM;AACR,CAAC;AAIM,IAAM,eAAeA,GAAE,OAAO;AAAA,EACnC,OAAOA,GAAE,OAAO;AAAA,EAChB,MAAMA,GAAE,KAAK;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAAA,EACD,SAASA,GAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;;;AHhED,IAAM,eAAe;AASd,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAC7C,YACE,SACgB,QACA,MACA,SAChB;AACA,UAAM,OAAO;AAJG;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,QACb,SACA,QACA,MACA,MACA,gBACsD;AACtD,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,SAAS,SAAS,SAAY,EAAE,gBAAgB,mBAAmB,IAAI;AAAA,IACvE,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,EACpD;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,GAAG,OAAO,GAAG,IAAI,IAAI,IAAI;AAAA,EAC7C,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,sCAAsC,OAAO,mCAA+B,IAAc,OAAO;AAAA,MACjG;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI;AACJ,MAAI;AAAE,aAAS,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,EAAE,QACtC;AAAE,aAAS,EAAE,OAAO,MAAM,MAAM,WAAW;AAAA,EAAE;AAEnD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,MAAM,aAAa,UAAU,MAAM;AACzC,QAAI,IAAI,SAAS;AACf,YAAM,IAAI,oBAAoB,IAAI,KAAK,OAAO,IAAI,QAAQ,IAAI,KAAK,MAAM,IAAI,KAAK,OAAO;AAAA,IAC3F;AACA,UAAM,IAAI,oBAAoB,QAAQ,IAAI,MAAM,IAAI,IAAI,QAAQ,YAAY,MAAM;AAAA,EACpF;AACA,MAAI,mBAAmB,KAAM,QAAO;AACpC,SAAO,eAAe,MAAM,MAAM;AACpC;AAMA,eAAe,YAAY,SAAgC;AACzD,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAGnD,QAAM,UAAU,QAAQ,MAAM,MAAM,MAAM,MAAM,OAAO,uBAAuB;AAC9E,QAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,OAAO,GAAG;AAAA,IAC/C,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK,QAAQ;AAAA,EACf,CAAC;AACD,QAAM,MAAM;AAGZ,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI;AACF,YAAM,IAAI,MAAM,MAAM,GAAG,OAAO,SAAS;AACzC,UAAI,EAAE,GAAI;AAAA,IACZ,QAAQ;AAAA,IAAmB;AAC3B,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAG,CAAC;AAAA,EAC3C;AACA,QAAM,IAAI;AAAA,IACR,qEACmB,OAAO;AAAA,EAC5B;AACF;AAOA,SAAS,oBAA4B;AACnC,MAAI;AACF,UAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,UAAM,aAAa;AAAA,MACjB,QAAQ,MAAM,MAAM,MAAM,MAAM,cAAc;AAAA,MAC9C,QAAQ,MAAM,MAAM,MAAM,cAAc;AAAA,IAC1C;AACA,eAAW,KAAK,YAAY;AAC1B,UAAI,WAAW,CAAC,GAAG;AACjB,cAAM,MAAM,KAAK,MAAM,aAAa,GAAG,MAAM,CAAC;AAC9C,YAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAAA,MAClD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAe;AACvB,SAAO;AACT;AAEA,IAAM,iBAAiB,kBAAkB;AAMzC,SAAS,qBAA2B;AAClC,MAAI;AACF,UAAM,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,eAAe,aAAa;AAC5E,QAAI,CAAC,WAAW,OAAO,EAAG;AAC1B,UAAM,MAAM,OAAO,aAAa,SAAS,MAAM,EAAE,KAAK,CAAC;AACvD,QAAI,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACnC,UAAI;AAAE,gBAAQ,KAAK,KAAK,SAAS;AAAA,MAAE,QAAQ;AAAA,MAAqB;AAAA,IAClE;AAAA,EACF,QAAQ;AAAA,EAAe;AACzB;AAEO,IAAM,gBAAN,MAAoB;AAAA,EAChB;AAAA,EACA;AAAA,EAET,YAAY,OAA6B,CAAC,GAAG;AAC3C,SAAK,UAAU,KAAK,WAAW;AAC/B,SAAK,YAAY,KAAK,cAAc;AAAA,EACtC;AAAA,EAEA,MAAM,SAA0B;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAK9E,UACE,KAAK,aACL,mBAAmB,WACnB,OAAO,YAAY,WACnB,OAAO,YAAY,gBACnB;AAMA,YAAI,cAAc;AAClB,YAAI;AACF,gBAAM,UAAU,MAAM,MAAM,GAAG,KAAK,OAAO,iBAAiB;AAC5D,cAAI,QAAQ,IAAI;AACd,kBAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,0BAAc,MAAM,QAAQ,KAAK,UAAU,IAAI,KAAK,WAAW,SAAS;AAAA,UAC1E;AAAA,QACF,QAAQ;AAAA,QAAoC;AAC5C,YAAI,cAAc,GAAG;AAEnB,kBAAQ;AAAA,YACN,+BAA+B,OAAO,OAAO,wBAAwB,cAAc,SAC5E,WAAW;AAAA,UAEpB;AACA,iBAAO;AAAA,QACT;AACA,2BAAmB;AAEnB,cAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAG,CAAC;AACzC,cAAM,YAAY,KAAK,OAAO;AAC9B,eAAO,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAAA,MAClE;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,UAAI,KAAK,aAAa,eAAe,uBAAuB,IAAI,WAAW,GAAG;AAC5E,cAAM,YAAY,KAAK,OAAO;AAC9B,eAAO,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAAA,MAClE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAAiE;AAK7E,QAAI,KAAK,WAAW;AAClB,UAAI;AAAE,cAAM,KAAK,OAAO;AAAA,MAAE,QAAQ;AAAA,MAAoE;AAAA,IACxG;AACA,UAAM,OAAO,eAAe,MAAM,KAAK;AACvC,WAAO,QAAQ,KAAK,SAAS,QAAQ,sBAAsB,MAAM,eAAe;AAAA,EAClF;AAAA,EAEA,MAAM,QAAQ,OAAsD;AAClE,UAAM,OAAO,eAAe,MAAM,KAAK;AACvC,UAAM,QAAQ,KAAK,SAAS,QAAQ,sBAAsB,MAAM,IAAI;AAAA,EACtE;AAAA,EAEA,MAAM,aAAkC;AACtC,WAAO,QAAQ,KAAK,SAAS,OAAO,gBAAgB,QAAW,UAAU;AAAA,EAC3E;AAAA,EAEA,MAAM,YAAY,OAAgE;AAChF,UAAM,OAAO,mBAAmB,MAAM,KAAK;AAC3C,WAAO,QAAQ,KAAK,SAAS,QAAQ,iBAAiB,MAAM,UAAU;AAAA,EACxE;AAAA,EAEA,MAAM,YAAY,OAAgE;AAChF,UAAM,OAAO,mBAAmB,MAAM,KAAK;AAC3C,WAAO,QAAQ,KAAK,SAAS,QAAQ,iBAAiB,MAAM,UAAU;AAAA,EACxE;AAAA,EAEA,MAAM,aAAkC;AACtC,WAAO,QAAQ,KAAK,SAAS,OAAO,gBAAgB,QAAW,UAAU;AAAA,EAC3E;AACF;AAGO,IAAM,UAAU,IAAI,cAAc;","names":["z","z","z","z","z"]}
|
package/dist/runtime/index.js
CHANGED
|
@@ -239,6 +239,21 @@ var RuntimeClient = class {
|
|
|
239
239
|
try {
|
|
240
240
|
const result = await request(this.baseUrl, "GET", "/health", void 0, Health);
|
|
241
241
|
if (this.autoStart && CLIENT_VERSION !== "0.0.0" && result.version !== "0.0.0" && result.version !== CLIENT_VERSION) {
|
|
242
|
+
let activeCount = 0;
|
|
243
|
+
try {
|
|
244
|
+
const listRes = await fetch(`${this.baseUrl}/devserver/list`);
|
|
245
|
+
if (listRes.ok) {
|
|
246
|
+
const data = await listRes.json();
|
|
247
|
+
activeCount = Array.isArray(data.devServers) ? data.devServers.length : 0;
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
if (activeCount > 0) {
|
|
252
|
+
console.warn(
|
|
253
|
+
`[storyboard] daemon version ${result.version} differs from client ${CLIENT_VERSION}, but ${activeCount} dev server(s) are active. Reusing the existing daemon. Run \`sb reset\` to restart it once those are stopped.`
|
|
254
|
+
);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
242
257
|
killExistingDaemon();
|
|
243
258
|
await new Promise((r) => setTimeout(r, 250));
|
|
244
259
|
await spawnDaemon(this.baseUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/schema/identity.ts","../../src/runtime/schema/devserver.ts","../../src/runtime/schema/api.ts","../../src/runtime/client/index.ts","../../src/runtime/server/http.ts","../../src/runtime/proxy/caddy.ts","../../src/runtime/proxy/controller.ts","../../src/runtime/devserver/port-pool.ts","../../src/runtime/server/constants.ts","../../src/runtime/devserver/orchestrator.ts","../../src/runtime/server/lock.ts","../../src/runtime/server/main.ts"],"sourcesContent":["import { z } from 'zod'\n\n/**\n * The legacy/default devDomain. Acquire requests using this value are rejected\n * unless `allowDefaultDomain` is set — see DevServerOrchestrator for details.\n */\nexport const DEFAULT_DEV_DOMAIN = 'storyboard'\n\n/**\n * A devDomain identifies a Storyboard repo on this machine.\n *\n * The literal default value `\"storyboard\"` is intentionally *not* allowed by\n * `acquire` (see schema/acquire.ts) — every checkout MUST set its own\n * `devDomain` in `storyboard.config.json`. This is the structural fix for H3\n * in the server-state RCA: two repos can never share a host space.\n *\n * Allowed: lowercase letters, digits, hyphens. Must start with a letter.\n * 1–32 chars. The runtime constructs the public host as `${devDomain}.localhost`.\n */\nexport const DevDomain = z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z][a-z0-9-]*$/, 'devDomain must match /^[a-z][a-z0-9-]*$/')\n .brand<'DevDomain'>()\nexport type DevDomain = z.infer<typeof DevDomain>\n\n/**\n * A worktree name. `\"main\"` is reserved for the repo root.\n *\n * Names are URL-safe by construction so we never have to escape them when\n * building branch URLs (`/branch--<name>/...`).\n */\nexport const WorktreeName = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9._-]*$/i, 'worktree name must be URL-safe')\n .brand<'WorktreeName'>()\nexport type WorktreeName = z.infer<typeof WorktreeName>\n\n/**\n * A TCP port the runtime has leased to a devserver. The runtime is the sole\n * authority for port allocation; clients never pick their own port.\n */\nexport const Port = z.number().int().min(1024).max(65535).brand<'Port'>()\nexport type Port = z.infer<typeof Port>\n\n/**\n * The composite key `(devDomain, worktree)` uniquely identifies a devserver.\n *\n * The runtime guarantees at most one devserver per slot — illegal collisions\n * (e.g. two repos trying to claim `(storyboard, main)`) are rejected with\n * `409 CONFLICT` rather than silently overwriting routes.\n */\nexport const DevServerSlot = z.object({\n devDomain: DevDomain,\n worktree: WorktreeName,\n})\nexport type DevServerSlot = z.infer<typeof DevServerSlot>\n\n/**\n * Convert a slot to its canonical string key, used for map lookups and\n * logging. Format: `${devDomain}::${worktree}`.\n */\nexport function slotKey(slot: DevServerSlot): string {\n return `${slot.devDomain}::${slot.worktree}`\n}\n","import { z } from 'zod'\nimport { DevDomain, DevServerSlot, Port, WorktreeName } from './identity.js'\n\n/**\n * DevServer lifecycle FSM.\n *\n * ```\n * spawning → ready → stopped\n * └────────────────┘ (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'stopped', // process exited, slot freed, port returned to the pool\n])\nexport type DevServerStatus = z.infer<typeof DevServerStatus>\n\n/** Allowed FSM transitions. Centralised so misuse is a one-line review catch. */\nexport const ALLOWED_TRANSITIONS: Record<DevServerStatus, readonly DevServerStatus[]> = {\n spawning: ['ready', 'stopped'],\n ready: ['stopped'],\n stopped: [],\n} as const\n\nexport class IllegalTransitionError extends Error {\n constructor(from: DevServerStatus, to: DevServerStatus) {\n super(`Illegal devserver transition: ${from} → ${to}`)\n this.name = 'IllegalTransitionError'\n }\n}\n\nexport function assertTransition(from: DevServerStatus, to: DevServerStatus): void {\n if (!ALLOWED_TRANSITIONS[from].includes(to)) {\n throw new IllegalTransitionError(from, to)\n }\n}\n\n/**\n * A devserver record as exposed by the runtime API. Always bound to a slot\n * after acquire (no pre-spawned pool members), so `slot` and `cwd` are required.\n */\nexport const DevServer = z.object({\n id: z.string().uuid(),\n pid: z.number().int().positive(),\n port: Port,\n status: DevServerStatus,\n slot: DevServerSlot,\n /** Absolute path of the worktree directory. */\n cwd: z.string(),\n /** ISO timestamp; immutable after spawn. */\n spawnedAt: z.string().datetime(),\n /** ISO timestamp of last status change. */\n updatedAt: z.string().datetime(),\n})\nexport type DevServer = z.infer<typeof DevServer>\n\n/**\n * A lease handed to a CLI client when it acquires a devserver.\n *\n * Leases are the *only* way a client controls a devserver — the runtime\n * refuses commands without a valid leaseId. They live as long as the\n * acquiring CLI process; expiry is a far-future sentinel.\n */\nexport const Lease = z.object({\n id: z.string().uuid(),\n devServerId: z.string().uuid(),\n slot: DevServerSlot,\n /** Public proxy URL the client should print to the user. Authoritative. */\n url: z.string().url(),\n /** Far-future sentinel — leases don't actually expire. */\n expiresAt: z.string().datetime(),\n})\nexport type Lease = z.infer<typeof Lease>\n\n/**\n * A Caddy proxy route owned by the runtime. The `@id` is always the devDomain;\n * this lets the runtime PATCH a single route in place without touching others.\n *\n * `upstreams` is keyed by plain string (validated elsewhere as WorktreeName)\n * to avoid `Partial<Record<branded, …>>` shenanigans at the value-spread sites.\n */\nexport const ProxyRoute = z.object({\n devDomain: DevDomain,\n host: z.string(),\n /** worktree name → upstream port. `main` is the host's catch-all. */\n upstreams: z.record(z.string(), Port),\n})\nexport type ProxyRoute = z.infer<typeof ProxyRoute>\n","import { z } from 'zod'\nimport { DevServerSlot, Port } from './identity.js'\nimport { DevServer, Lease, ProxyRoute } from './devserver.js'\n\n/**\n * `POST /devserver/acquire` — request a devserver for a `(devDomain, worktree)` slot.\n *\n * If a devserver already exists for the slot, the runtime returns its existing\n * lease. Otherwise it spawns a new Vite process. The slot is locked for the\n * duration of the call.\n */\nexport const AcquireRequest = z.object({\n slot: DevServerSlot,\n /** Absolute path of the worktree directory; the runtime spawns Vite with `cwd: targetCwd`. */\n targetCwd: z.string().min(1),\n /**\n * Escape hatch for the deprecated default devDomain `\"storyboard\"`. CI and\n * one-off scripts may pass true; the CLI never does.\n */\n allowDefaultDomain: z.boolean().default(false),\n})\nexport type AcquireRequest = z.infer<typeof AcquireRequest>\n\nexport const AcquireResponse = z.object({\n lease: Lease,\n devServer: DevServer,\n})\nexport type AcquireResponse = z.infer<typeof AcquireResponse>\n\n/** `POST /devserver/release` — relinquish the lease and stop the devserver. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `GET /proxy/state` — current routing table the runtime believes Caddy holds. */\nexport const ProxyState = z.object({\n routes: z.array(ProxyRoute),\n caddyReachable: z.boolean(),\n})\nexport type ProxyState = z.infer<typeof ProxyState>\n\n/** `GET /pool/status` — kept for backward compat; always reports zeros. */\nexport const PoolStatus = z.object({\n warm: z.number().int().nonnegative(),\n bound: z.number().int().nonnegative(),\n capacity: z.number().int().nonnegative(),\n})\nexport type PoolStatus = z.infer<typeof PoolStatus>\n\n/** `POST /proxy/upsert` — bind (devDomain, worktree) → port in the proxy. */\nexport const ProxyUpsertRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n port: z.number(),\n})\nexport type ProxyUpsertRequest = z.infer<typeof ProxyUpsertRequest>\n\n/** `POST /proxy/remove` — drop a worktree's route from the proxy. */\nexport const ProxyRemoveRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n})\nexport type ProxyRemoveRequest = z.infer<typeof ProxyRemoveRequest>\n\n/** `GET /health` — daemon liveness probe. */\nexport const Health = z.object({\n ok: z.literal(true),\n version: z.string(),\n uptimeSeconds: z.number().nonnegative(),\n port: Port,\n})\nexport type Health = z.infer<typeof Health>\n\n/** Runtime error envelope. All non-2xx responses share this shape. */\nexport const RuntimeError = z.object({\n error: z.string(),\n code: z.enum([\n 'NOT_IMPLEMENTED',\n 'BAD_REQUEST',\n 'CONFLICT',\n 'NOT_FOUND',\n 'FORBIDDEN_DEFAULT_DOMAIN',\n 'INTERNAL',\n 'CADDY_UNREACHABLE',\n 'PORT_EXHAUSTED',\n 'TIMEOUT',\n ]),\n details: z.unknown().optional(),\n})\nexport type RuntimeError = z.infer<typeof RuntimeError>\n","import { spawn } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport {\n AcquireRequest,\n AcquireResponse,\n Health,\n PoolStatus,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n} from '../schema/index.js'\nimport type { z } from 'zod'\n\n/**\n * Typed JS/TS client for the Storyboard Runtime daemon.\n *\n * Consumers should always go through this client rather than hand-rolling\n * `fetch` calls — the client is the only place where the daemon's URL is\n * known, and it's the only place where on-demand daemon spawning happens.\n */\n\nconst RUNTIME_BASE = 'http://127.0.0.1:4321'\n\nexport interface RuntimeClientOptions {\n /** Override base URL (mostly for tests). */\n baseUrl?: string\n /** Auto-start the daemon if it isn't running. Default: true. */\n autoStart?: boolean\n}\n\nexport class RuntimeRequestError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: z.infer<typeof RuntimeError>['code'],\n public readonly details?: unknown,\n ) {\n super(message)\n this.name = 'RuntimeRequestError'\n }\n}\n\nasync function request<S extends z.ZodTypeAny>(\n baseUrl: string,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n responseSchema: S | null,\n): Promise<S extends z.ZodTypeAny ? z.output<S> : void> {\n const init: RequestInit = {\n method,\n headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n }\n let res: Response\n try {\n res = await fetch(`${baseUrl}${path}`, init)\n } catch (err) {\n throw new RuntimeRequestError(\n `Cannot reach Storyboard Runtime at ${baseUrl} — is the daemon running? (${(err as Error).message})`,\n 0,\n 'INTERNAL',\n )\n }\n const text = await res.text()\n let parsed: unknown\n try { parsed = text ? JSON.parse(text) : {} }\n catch { parsed = { error: text, code: 'INTERNAL' } }\n\n if (!res.ok) {\n const err = RuntimeError.safeParse(parsed)\n if (err.success) {\n throw new RuntimeRequestError(err.data.error, res.status, err.data.code, err.data.details)\n }\n throw new RuntimeRequestError(`HTTP ${res.status}`, res.status, 'INTERNAL', parsed)\n }\n if (responseSchema === null) return undefined as never\n return responseSchema.parse(parsed) as never\n}\n\n/**\n * Spawn the daemon as a detached child. Resolves once the health endpoint\n * answers (or rejects after a short timeout).\n */\nasync function spawnDaemon(baseUrl: string): Promise<void> {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin/storyboard-runtime.js lives next to dist/, two levels up from\n // dist/runtime/client/index.js (the published path).\n const binPath = resolve(here, '..', '..', '..', 'bin', 'storyboard-runtime.js')\n const child = spawn(process.execPath, [binPath], {\n detached: true,\n stdio: 'ignore',\n env: process.env,\n })\n child.unref()\n\n // Poll /health until the daemon is up.\n const deadline = Date.now() + 5000\n while (Date.now() < deadline) {\n try {\n const r = await fetch(`${baseUrl}/health`)\n if (r.ok) return\n } catch { /* not up yet */ }\n await new Promise(r => setTimeout(r, 100))\n }\n throw new Error(\n `Storyboard Runtime did not become ready within 5s ` +\n `(tried to spawn ${binPath})`,\n )\n}\n\n/**\n * Read the @dfosco/storyboard package.json version that this client is\n * shipping with. Used to detect mismatches against a long-lived daemon\n * that may have been spawned by a previous install.\n */\nfunction readClientVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n }\n }\n } catch { /* ignore */ }\n return '0.0.0'\n}\n\nconst CLIENT_VERSION = readClientVersion()\n\n/**\n * Send SIGTERM to the daemon PID and clear its lock/pid files so\n * spawnDaemon() can start a fresh one.\n */\nfunction killExistingDaemon(): void {\n try {\n const pidPath = resolve(process.env.HOME || '', '.storyboard', 'runtime.pid')\n if (!existsSync(pidPath)) return\n const pid = Number(readFileSync(pidPath, 'utf8').trim())\n if (Number.isFinite(pid) && pid > 0) {\n try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }\n }\n } catch { /* ignore */ }\n}\n\nexport class RuntimeClient {\n readonly baseUrl: string\n readonly autoStart: boolean\n\n constructor(opts: RuntimeClientOptions = {}) {\n this.baseUrl = opts.baseUrl ?? RUNTIME_BASE\n this.autoStart = opts.autoStart !== false\n }\n\n async health(): Promise<Health> {\n try {\n const result = await request(this.baseUrl, 'GET', '/health', undefined, Health)\n // Auto-respawn on version mismatch — a long-lived daemon from a\n // previous install otherwise keeps serving stale code after upgrade.\n // Skip when client reports 0.0.0 (dev/source layout where package\n // version isn't meaningful).\n if (\n this.autoStart &&\n CLIENT_VERSION !== '0.0.0' &&\n result.version !== '0.0.0' &&\n result.version !== CLIENT_VERSION\n ) {\n killExistingDaemon()\n // Give the OS a moment to release port 4321\n await new Promise(r => setTimeout(r, 250))\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n return result\n } catch (err) {\n if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n throw err\n }\n }\n\n async acquire(input: z.input<typeof AcquireRequest>): Promise<AcquireResponse> {\n // Force a health check first — this is the sole codepath that detects\n // a stale daemon (different version, crashed mid-restart, etc.) and\n // respawns it. Without this, `sb run` would happily POST against an\n // outdated daemon and inherit all of its bugs.\n if (this.autoStart) {\n try { await this.health() } catch { /* health() will throw on hard failure; let acquire surface it */ }\n }\n const body = AcquireRequest.parse(input)\n return request(this.baseUrl, 'POST', '/devserver/acquire', body, AcquireResponse)\n }\n\n async release(input: z.input<typeof ReleaseRequest>): Promise<void> {\n const body = ReleaseRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/release', body, null)\n }\n\n async proxyState(): Promise<ProxyState> {\n return request(this.baseUrl, 'GET', '/proxy/state', undefined, ProxyState)\n }\n\n async proxyUpsert(input: z.input<typeof ProxyUpsertRequest>): Promise<ProxyState> {\n const body = ProxyUpsertRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/upsert', body, ProxyState)\n }\n\n async proxyRemove(input: z.input<typeof ProxyRemoveRequest>): Promise<ProxyState> {\n const body = ProxyRemoveRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/remove', body, ProxyState)\n }\n\n async poolStatus(): Promise<PoolStatus> {\n return request(this.baseUrl, 'GET', '/pool/status', undefined, PoolStatus)\n }\n}\n\n/** Default singleton client for casual callers. */\nexport const runtime = new RuntimeClient()\n","import http from 'node:http'\nimport { z } from 'zod'\nimport {\n AcquireRequest,\n AcquireResponse,\n DevDomain,\n Health,\n PoolStatus,\n Port,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n WorktreeName,\n} from '../schema/index.js'\nimport { ProxyController } from '../proxy/index.js'\nimport { CaddyUnreachableError } from '../proxy/caddy.js'\nimport {\n DevServerOrchestrator,\n ForbiddenDefaultDomainError,\n LeaseNotFoundError,\n DevServerSpawnError,\n SlotCwdConflictError,\n} from '../devserver/index.js'\nimport { PortPool } from '../devserver/port-pool.js'\nimport { RUNTIME_PORT, RUNTIME_HOST, RUNTIME_VERSION } from './constants.js'\n\n/**\n * The runtime's HTTP API.\n *\n * Every request is parsed through a zod schema *before* any handler runs;\n * malformed input never reaches the orchestrator. Every response is also\n * shape-checked in development to prevent accidental contract drift.\n */\n\nconst startedAt = Date.now()\n\n/**\n * Process-wide singletons. The runtime is one-per-machine, so module-level\n * controllers are fine. Tests inject their own via createRuntimeServer({ ... }).\n */\nlet proxyController = new ProxyController()\nlet _ports = new PortPool()\nlet orchestrator: DevServerOrchestrator = new DevServerOrchestrator({\n proxy: proxyController,\n ports: _ports,\n})\n\nfunction sendJson(\n res: http.ServerResponse,\n status: number,\n body: unknown,\n schema?: z.ZodTypeAny,\n): void {\n if (schema && process.env.NODE_ENV !== 'production') {\n schema.parse(body)\n }\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction sendError(\n res: http.ServerResponse,\n status: number,\n code: z.infer<typeof RuntimeError>['code'],\n message: string,\n details?: unknown,\n): void {\n const body = RuntimeError.parse({ error: message, code, details })\n sendJson(res, status, body, RuntimeError)\n}\n\nasync function readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n if (chunks.length === 0) return {}\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n } catch {\n throw new Error('Malformed JSON body')\n }\n}\n\nasync function parseBody<S extends z.ZodTypeAny>(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n schema: S,\n): Promise<z.output<S> | null> {\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (err) {\n sendError(res, 400, 'BAD_REQUEST', (err as Error).message)\n return null\n }\n const result = schema.safeParse(raw)\n if (!result.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed', result.error.flatten())\n return null\n }\n return result.data\n}\n\ntype Route = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> | void\n\nconst routes = new Map<string, Route>()\n\nroutes.set('GET /health', (_req, res) => {\n const body: Health = Health.parse({\n ok: true,\n version: RUNTIME_VERSION,\n uptimeSeconds: (Date.now() - startedAt) / 1000,\n port: RUNTIME_PORT,\n })\n sendJson(res, 200, body, Health)\n})\n\nroutes.set('POST /devserver/acquire', async (req, res) => {\n const body = await parseBody(req, res, AcquireRequest)\n if (!body) return\n try {\n const result = await orchestrator.acquire(body)\n sendJson(res, 200, result, AcquireResponse)\n } catch (err) {\n if (err instanceof ForbiddenDefaultDomainError) {\n sendError(res, 403, 'FORBIDDEN_DEFAULT_DOMAIN', err.message)\n return\n }\n if (err instanceof SlotCwdConflictError) {\n sendError(res, 409, 'CONFLICT', err.message)\n return\n }\n if (err instanceof DevServerSpawnError) {\n sendError(res, 500, 'INTERNAL', err.message, { stderr: err.stderr })\n return\n }\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /devserver/release', async (req, res) => {\n const body = await parseBody(req, res, ReleaseRequest)\n if (!body) return\n try {\n orchestrator.release(body.leaseId)\n sendJson(res, 200, { ok: true })\n } catch (err) {\n if (err instanceof LeaseNotFoundError) {\n sendError(res, 404, 'NOT_FOUND', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('GET /devserver/list', (_req, res) => {\n sendJson(res, 200, { devServers: orchestrator.list() })\n})\n\nroutes.set('GET /proxy/state', async (_req, res) => {\n try {\n const body = await proxyController.stateForApi()\n sendJson(res, 200, body, ProxyState)\n } catch (err) {\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /proxy/upsert', async (req, res) => {\n const body = await parseBody(req, res, ProxyUpsertRequest)\n if (!body) return\n // Re-validate with branded types — the API request schema is intentionally\n // loose (string/number) so bad input gets a useful 400 rather than the\n // brand regex's cryptic message.\n const dev = DevDomain.safeParse(body.devDomain)\n const wt = WorktreeName.safeParse(body.worktree)\n const port = Port.safeParse(body.port)\n if (!dev.success || !wt.success || !port.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed', {\n devDomain: dev.success ? null : dev.error.flatten(),\n worktree: wt.success ? null : wt.error.flatten(),\n port: port.success ? null : port.error.flatten(),\n })\n return\n }\n if (dev.data === ('storyboard' as unknown as typeof dev.data)) {\n sendError(res, 403, 'FORBIDDEN_DEFAULT_DOMAIN',\n 'Refusing to bind a route under the default devDomain \"storyboard\". ' +\n 'Set a unique devDomain in storyboard.config.json.')\n return\n }\n try {\n const route = await proxyController.upsert(dev.data, wt.data, port.data)\n sendJson(res, 200, ProxyState.parse({ routes: [route], caddyReachable: true }), ProxyState)\n } catch (err) {\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /proxy/remove', async (req, res) => {\n const body = await parseBody(req, res, ProxyRemoveRequest)\n if (!body) return\n const dev = DevDomain.safeParse(body.devDomain)\n const wt = WorktreeName.safeParse(body.worktree)\n if (!dev.success || !wt.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed')\n return\n }\n try {\n await proxyController.removeWorktree(dev.data, wt.data)\n sendJson(res, 200, ProxyState.parse({ routes: [], caddyReachable: true }), ProxyState)\n } catch (err) {\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('GET /pool/status', (_req, res) => {\n // Prefer the orchestrator's view (reflects bound count); fall back to the\n // raw-pool snapshot when no hot pool is configured.\n const live = orchestrator.poolStatus()\n const body = PoolStatus.parse(live ?? { warm: 0, bound: 0, capacity: 0 })\n sendJson(res, 200, body, PoolStatus)\n})\n\nexport function createRuntimeServer(opts: {\n proxyController?: ProxyController\n orchestrator?: DevServerOrchestrator\n} = {}): http.Server {\n if (opts.proxyController) proxyController = opts.proxyController\n if (opts.orchestrator) orchestrator = opts.orchestrator\n // Reconcile Caddy on startup: delete any @id-tagged routes left over from\n // a previous daemon process. Without this, a recycled port can be hit by\n // a stale host route and serve the wrong repo (see \"Wrong domain 421\").\n void proxyController.reconcileFromCaddy().catch(() => undefined)\n // Expose orchestrator shutdown for the daemon's signal handler.\n ;(globalThis as { __storyboardOrchestratorShutdown?: () => void }).__storyboardOrchestratorShutdown = () => orchestrator.shutdown()\n const server = http.createServer(async (req, res) => {\n try {\n const url = new URL(req.url ?? '/', `http://${RUNTIME_HOST}:${RUNTIME_PORT}`)\n const key = `${req.method ?? 'GET'} ${url.pathname}`\n const handler = routes.get(key)\n if (!handler) {\n sendError(res, 404, 'NOT_FOUND', `No route for ${key}`)\n return\n }\n await handler(req, res)\n } catch (err) {\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n })\n return server\n}\n","import { z } from 'zod'\nimport { DevDomain, Port, WorktreeName } from '../schema/index.js'\n\n/**\n * Typed Caddy admin API client. The runtime is the SOLE writer to\n * `http://localhost:2019` — this module is the only place in the codebase\n * that issues PATCH/POST/DELETE against Caddy. All cross-repo route races\n * (the doubled-URL bug class) are eliminated by funneling writes here.\n *\n * Reads are also funneled through this module so the runtime's view of\n * Caddy state matches what's actually live.\n */\n\nexport const CADDY_ADMIN_DEFAULT = 'http://localhost:2019'\n\nexport const CaddyRoute = z\n .object({\n '@id': z.string().optional(),\n match: z.array(z.object({ host: z.array(z.string()).optional() }).passthrough()).optional(),\n handle: z.array(z.unknown()).optional(),\n })\n .passthrough()\nexport type CaddyRoute = z.infer<typeof CaddyRoute>\n\nexport interface CaddyAdminOptions {\n baseUrl?: string\n timeoutMs?: number\n}\n\nexport class CaddyUnreachableError extends Error {\n constructor(baseUrl: string, cause?: unknown) {\n super(`Caddy admin API not reachable at ${baseUrl}`)\n this.name = 'CaddyUnreachableError'\n if (cause) (this as Error & { cause?: unknown }).cause = cause\n }\n}\n\nexport class CaddyAdminClient {\n readonly baseUrl: string\n readonly timeoutMs: number\n\n constructor(opts: CaddyAdminOptions = {}) {\n this.baseUrl = opts.baseUrl ?? CADDY_ADMIN_DEFAULT\n this.timeoutMs = opts.timeoutMs ?? 5000\n }\n\n private async fetch(path: string, init: RequestInit = {}): Promise<Response> {\n const controller = new AbortController()\n const t = setTimeout(() => controller.abort(), this.timeoutMs)\n try {\n // Caddy's admin API rejects requests with 403 when the Origin header\n // doesn't match an allowed origin. Node's undici fetch sends an empty\n // Origin by default, which Caddy refuses. Set it explicitly to the\n // admin URL.\n const headers = new Headers(init.headers)\n if (!headers.has('origin')) headers.set('origin', this.baseUrl)\n return await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal: controller.signal })\n } catch (err) {\n throw new CaddyUnreachableError(this.baseUrl, err)\n } finally {\n clearTimeout(t)\n }\n }\n\n async ping(): Promise<boolean> {\n try {\n const r = await this.fetch('/config/')\n return r.ok\n } catch {\n return false\n }\n }\n\n async listRoutes(): Promise<CaddyRoute[]> {\n const r = await this.fetch('/config/apps/http/servers/srv0/routes')\n if (r.status === 404) return []\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n const raw = (await r.json()) as unknown\n return z.array(CaddyRoute).parse(raw ?? [])\n }\n\n async patchById(id: string, route: CaddyRoute): Promise<boolean> {\n const r = await this.fetch(`/id/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(route),\n })\n return r.ok\n }\n\n async appendRoute(route: CaddyRoute): Promise<void> {\n const r = await this.fetch('/config/apps/http/servers/srv0/routes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(route),\n })\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n }\n\n async deleteRouteAt(idx: number): Promise<void> {\n const r = await this.fetch(`/config/apps/http/servers/srv0/routes/${idx}`, {\n method: 'DELETE',\n })\n if (!r.ok && r.status !== 404) throw new CaddyUnreachableError(this.baseUrl)\n }\n\n async bootstrap(): Promise<void> {\n const config = {\n apps: { http: { servers: { srv0: { listen: [':80'], routes: [] } } } },\n }\n const r = await this.fetch('/load', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(config),\n })\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n }\n}\n\n/**\n * Build a Caddy route for a single devDomain, with one subroute per worktree.\n * The `@id` is the devDomain itself so writes are PATCHable in place.\n */\nexport function buildRouteFor(\n devDomain: DevDomain,\n upstreams: Record<string, Port>,\n mainPort?: Port,\n): CaddyRoute {\n const host = `${devDomain}.localhost`\n const subroutes: unknown[] = []\n\n for (const [worktree, port] of Object.entries(upstreams)) {\n if (worktree === 'main') continue\n subroutes.push({\n match: [{ path: [`/branch--${worktree}/*`] }],\n handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${port}` }] }],\n })\n }\n\n const fallbackPort = mainPort ?? upstreams.main ?? Object.values(upstreams)[0]\n if (fallbackPort) {\n subroutes.push({\n handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${fallbackPort}` }] }],\n })\n }\n\n return CaddyRoute.parse({\n '@id': devDomain,\n match: [{ host: [host] }],\n handle: [{ handler: 'subroute', routes: subroutes }],\n })\n}\n\nvoid WorktreeName\n","import { DevDomain, Port, ProxyRoute, ProxyState, WorktreeName } from '../schema/index.js'\nimport { buildRouteFor, CaddyAdminClient, CaddyUnreachableError, CaddyRoute } from './caddy.js'\n\n/**\n * ProxyController — the runtime's authoritative model of the Caddy proxy.\n *\n * Two invariants the rest of the system depends on:\n *\n * 1. **Sole writer.** Only this controller calls Caddy admin write methods\n * (PATCH/POST/DELETE). The legacy per-repo `caddy reload --config` path\n * that destructively replaced the entire config is no longer reachable\n * from any code under the runtime.\n *\n * 2. **Serialized writes.** Every mutation is queued through `withLock()`.\n * Two concurrent acquires for different devDomains can no longer race\n * each other into producing a Caddy config where one repo's routes\n * silently shadowed another's.\n *\n * The in-memory `routes` map is the desired state. After every write we\n * reconcile against Caddy's actual state and re-PATCH if drift is detected.\n */\n\nexport interface ProxyControllerOptions {\n caddy?: CaddyAdminClient\n}\n\nexport class ProxyController {\n private readonly caddy: CaddyAdminClient\n private readonly routes = new Map<DevDomain, ProxyRoute>()\n private writeChain: Promise<unknown> = Promise.resolve()\n\n constructor(opts: ProxyControllerOptions = {}) {\n this.caddy = opts.caddy ?? new CaddyAdminClient()\n }\n\n private withLock<T>(fn: () => Promise<T>): Promise<T> {\n const next = this.writeChain.then(fn, fn)\n this.writeChain = next.catch(() => undefined)\n return next\n }\n\n async caddyReachable(): Promise<boolean> {\n return this.caddy.ping()\n }\n\n async stateForApi(): Promise<ProxyState> {\n return ProxyState.parse({\n routes: Array.from(this.routes.values()),\n caddyReachable: await this.caddy.ping(),\n })\n }\n\n async upsert(devDomain: DevDomain, worktree: WorktreeName, port: Port): Promise<ProxyRoute> {\n return this.withLock(async () => {\n // Evict this port from every other devDomain. A port can be reassigned\n // when its previous owner exits, but the previous devDomain's Caddy\n // route still references the port number — so requests to host A would\n // silently land on host B's Vite (which then 421s with \"Wrong domain\").\n // Strip the port from sibling routes before binding it here.\n for (const [otherDomain, otherRoute] of this.routes) {\n if (otherDomain === devDomain) continue\n let mutated = false\n const nextUpstreams: Record<string, Port> = {}\n for (const [w, p] of Object.entries(otherRoute.upstreams)) {\n if (Number(p) === Number(port)) { mutated = true; continue }\n nextUpstreams[w] = p\n }\n if (!mutated) continue\n if (Object.keys(nextUpstreams).length === 0) {\n this.routes.delete(otherDomain)\n await this.removeFromCaddy(otherDomain)\n } else {\n const cleaned = ProxyRoute.parse({\n devDomain: otherDomain,\n host: `${otherDomain}.localhost`,\n upstreams: nextUpstreams,\n })\n this.routes.set(otherDomain, cleaned)\n await this.syncOne(cleaned)\n }\n }\n\n const existing = this.routes.get(devDomain)\n const upstreams: Record<string, Port> = { ...(existing?.upstreams ?? {}) }\n upstreams[worktree] = port\n const next = ProxyRoute.parse({\n devDomain,\n host: `${devDomain}.localhost`,\n upstreams,\n })\n this.routes.set(devDomain, next)\n await this.syncOne(next)\n return next\n })\n }\n\n async removeWorktree(devDomain: DevDomain, worktree: WorktreeName): Promise<void> {\n return this.withLock(async () => {\n const existing = this.routes.get(devDomain)\n if (!existing) return\n const upstreams: Record<string, Port> = { ...existing.upstreams }\n delete upstreams[worktree]\n if (Object.keys(upstreams).length === 0) {\n this.routes.delete(devDomain)\n await this.removeFromCaddy(devDomain)\n } else {\n const next = ProxyRoute.parse({\n devDomain,\n host: `${devDomain}.localhost`,\n upstreams,\n })\n this.routes.set(devDomain, next)\n await this.syncOne(next)\n }\n })\n }\n\n private async syncOne(route: ProxyRoute): Promise<void> {\n if (!(await this.caddy.ping())) {\n throw new CaddyUnreachableError(this.caddy.baseUrl)\n }\n const caddyRoute = buildRouteFor(route.devDomain, route.upstreams as Record<string, Port>)\n const patched = await this.caddy.patchById(route.devDomain, caddyRoute)\n if (!patched) {\n await this.caddy.appendRoute(caddyRoute)\n }\n await this.cleanupDuplicates(route.devDomain, `${route.devDomain}.localhost`)\n }\n\n private async removeFromCaddy(devDomain: DevDomain): Promise<void> {\n if (!(await this.caddy.ping())) throw new CaddyUnreachableError(this.caddy.baseUrl)\n const all = await this.caddy.listRoutes()\n const indices: number[] = []\n for (let i = 0; i < all.length; i++) {\n if (all[i]!['@id'] === devDomain) indices.push(i)\n }\n for (const i of indices.sort((a, b) => b - a)) {\n await this.caddy.deleteRouteAt(i)\n }\n }\n\n /**\n * Stale-route cleanup. The legacy `caddy reload` path used to leave\n * non-`@id` routes for the same host that would shadow our admin-API\n * route. We sweep them on every successful upsert.\n */\n private async cleanupDuplicates(keepId: DevDomain, host: string): Promise<void> {\n const all = await this.caddy.listRoutes()\n const stale: number[] = []\n for (let i = 0; i < all.length; i++) {\n const r = all[i]!\n if (r['@id'] === keepId) continue\n if (r['@id']) continue\n const hosts = r.match?.[0]?.host ?? []\n if (hosts.includes(host)) stale.push(i)\n }\n for (const i of stale.sort((a, b) => b - a)) {\n try { await this.caddy.deleteRouteAt(i) } catch { /* best-effort */ }\n }\n }\n\n async readLiveCaddyRoutes(): Promise<CaddyRoute[]> {\n return this.caddy.listRoutes()\n }\n\n /**\n * Reconcile Caddy with this controller's in-memory desired state.\n *\n * Called once on daemon startup. The runtime is the sole writer to Caddy,\n * so any `@id`-tagged route the controller doesn't know about is a leftover\n * from a previous daemon process. Leaving them in place causes the bug\n * where a recycled port hits a stale host (e.g. `storyboard.localhost` →\n * port 1240, which now belongs to a different repo). We delete them.\n *\n * Does NOT touch routes without `@id` — those may be hand-installed Caddy\n * rules outside our ownership; legacy duplicate cleanup happens per-upsert.\n */\n async reconcileFromCaddy(): Promise<void> {\n return this.withLock(async () => {\n if (!(await this.caddy.ping())) return\n const all = await this.caddy.listRoutes()\n const owned = new Set<string>(this.routes.keys())\n const stale: number[] = []\n for (let i = 0; i < all.length; i++) {\n const id = all[i]!['@id']\n if (id && !owned.has(id)) stale.push(i)\n }\n for (const i of stale.sort((a, b) => b - a)) {\n try { await this.caddy.deleteRouteAt(i) } catch { /* best-effort */ }\n }\n })\n }\n}\n","import { createServer } from 'node:net'\nimport { DEVSERVER_PORT_MAX, DEVSERVER_PORT_MIN } from '../server/constants.js'\nimport { Port } from '../schema/index.js'\n\n/**\n * PortPool — leases TCP ports from the runtime's reserved range.\n *\n * The runtime is the sole authority for port allocation. Vite child processes\n * never pick their own port; they're told which port to bind to. This is the\n * structural fix for hypothesis H4 (independent per-repo port registries with\n * no cross-repo awareness): leases live in this single in-memory pool, and\n * collisions are impossible because the pool refuses to hand out a port\n * that's already leased.\n */\n\nexport class PortExhaustedError extends Error {\n constructor() {\n super(`No free ports in range ${DEVSERVER_PORT_MIN}-${DEVSERVER_PORT_MAX}`)\n this.name = 'PortExhaustedError'\n }\n}\n\nexport class PortPool {\n private readonly leased = new Set<number>()\n\n async acquire(): Promise<Port> {\n for (let p = DEVSERVER_PORT_MIN; p <= DEVSERVER_PORT_MAX; p++) {\n if (this.leased.has(p)) continue\n if (await isPortFree(p)) {\n this.leased.add(p)\n return Port.parse(p)\n }\n }\n throw new PortExhaustedError()\n }\n\n release(port: Port | number): void {\n this.leased.delete(Number(port))\n }\n\n isLeased(port: Port | number): boolean {\n return this.leased.has(Number(port))\n }\n\n size(): number {\n return this.leased.size\n }\n}\n\nfunction isPortFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = createServer()\n probe.once('error', () => resolve(false))\n probe.once('listening', () => {\n probe.close(() => resolve(true))\n })\n probe.listen(port, '127.0.0.1')\n })\n}\n","import { homedir } from 'node:os'\nimport { join, dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Runtime constants — single source of truth for ports, paths, and version.\n *\n * The runtime port (`4321`) is intentionally fixed: a single global daemon\n * needs a well-known address so the CLI client can find it without\n * configuration. Port 4321 is RFC-allowed and avoids collisions with\n * Vite (1234), Caddy admin (2019), and the per-repo legacy server (4100-4199).\n */\nexport const RUNTIME_PORT = 4321 as const\nexport const RUNTIME_HOST = '127.0.0.1' as const\n\n/** Where the daemon writes its pidfile and lockfile. */\nexport const RUNTIME_HOME = join(homedir(), '.storyboard')\nexport const PIDFILE = join(RUNTIME_HOME, 'runtime.pid')\nexport const LOCKFILE = join(RUNTIME_HOME, 'runtime.lock')\nexport const STATEFILE = join(RUNTIME_HOME, 'runtime.state.json')\n\n/**\n * Range of TCP ports the runtime leases to Vite devservers. Picked to sit\n * above Vite's default (1234) and below ephemeral-port territory.\n */\nexport const DEVSERVER_PORT_MIN = 1240 as const\nexport const DEVSERVER_PORT_MAX = 1399 as const\n\n/** Caddy admin API endpoint. The runtime is the SOLE writer to this URL. */\nexport const CADDY_ADMIN = 'http://localhost:2019' as const\n\n// Runtime version is read from the package.json at startup so it tracks\n// the published @dfosco/storyboard version. Falls back to \"0.0.0\" if the\n// file can't be located (e.g. dev script runs from a non-standard layout).\nfunction readPackageVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n // Bundled to dist/runtime/server/main.js → ../../../package.json\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string' && pkg.version) return pkg.version\n }\n }\n } catch { /* fallthrough */ }\n return '0.0.0'\n}\n\nexport const RUNTIME_VERSION = readPackageVersion()\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { resolve as resolvePath } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport {\n AcquireRequest,\n AcquireResponse,\n assertTransition,\n DevServer,\n DevServerSlot,\n DevServerStatus,\n DEFAULT_DEV_DOMAIN,\n Port,\n slotKey,\n} from '../schema/index.js'\nimport { ProxyController } from '../proxy/index.js'\nimport { PortPool } from './port-pool.js'\n\n/**\n * DevServerOrchestrator — owns the lifecycle of every Vite dev process.\n *\n * Invariants enforced here:\n *\n * 1. **Slot uniqueness.** At most one DevServer per `(devDomain, worktree)`.\n * Acquire on an existing slot returns the same DevServer with a fresh\n * lease. Per-slot mutex prevents concurrent double-spawn.\n *\n * 2. **Default-domain refusal (H3).** `acquire()` rejects requests whose\n * devDomain is the literal `\"storyboard\"` unless `allowDefaultDomain`\n * is true. The CLI never passes `true`; only CI/scripts do.\n *\n * 3. **FSM.** Every status change goes through `assertTransition()`.\n * Illegal transitions throw — no devserver can be `ready` without\n * having gone through `spawning` first.\n *\n * 4. **Lease enforcement.** Release/renew require a valid lease ID. A stale\n * `sb dev` cannot kill a devserver claimed by a newer session.\n */\n\ninterface DevServerInternal {\n id: string\n child: ChildProcess\n pid: number\n port: Port\n status: DevServerStatus\n slot: DevServerSlot\n cwd: string\n spawnedAt: string\n updatedAt: string\n stderrTail: string[]\n readyPromise: Promise<void>\n}\n\ninterface LeaseInternal {\n id: string\n devServerId: string\n slot: DevServerSlot\n url: string\n expiresAt: number\n}\n\nexport class SlotCwdConflictError extends Error {\n constructor(slot: DevServerSlot, existingCwd: string, requestedCwd: string) {\n super(\n `Slot ${slotKey(slot)} is already bound to ${existingCwd}; refusing to rebind to ${requestedCwd}. ` +\n `Two different repositories cannot share devDomain \"${slot.devDomain}\". ` +\n `Set a unique devDomain in storyboard.config.json for one of them.`,\n )\n this.name = 'SlotCwdConflictError'\n }\n}\n\nexport class ForbiddenDefaultDomainError extends Error {\n constructor() {\n super(\n `Refusing to acquire under default devDomain \"${DEFAULT_DEV_DOMAIN}\". ` +\n `Set a unique devDomain in storyboard.config.json or pass allowDefaultDomain=true.`,\n )\n this.name = 'ForbiddenDefaultDomainError'\n }\n}\n\nexport class LeaseNotFoundError extends Error {\n constructor(id: string) {\n super(`No lease ${id}`)\n this.name = 'LeaseNotFoundError'\n }\n}\n\nexport class DevServerSpawnError extends Error {\n readonly stderr: string\n constructor(slot: DevServerSlot, exitCode: number | null, stderr: string) {\n super(`Vite for ${slotKey(slot)} exited (code ${exitCode ?? 'unknown'}) before becoming ready`)\n this.name = 'DevServerSpawnError'\n this.stderr = stderr\n }\n}\n\nexport interface DevServerOrchestratorOptions {\n proxy: ProxyController\n ports?: PortPool\n /** Override Vite spawn for tests — return a child you've prepared. */\n spawnVite?: (cwd: string, port: Port, basePath: string, devDomain: string) => ChildProcess\n readyTimeoutMs?: number\n}\n\nconst DEFAULT_READY_TIMEOUT_MS = 30_000\nconst STDERR_TAIL_MAX = 50\n\nexport class DevServerOrchestrator {\n private readonly proxy: ProxyController\n private readonly ports: PortPool\n private readonly readyTimeoutMs: number\n private readonly spawnViteFn: NonNullable<DevServerOrchestratorOptions['spawnVite']>\n\n private readonly bySlot = new Map<string, DevServerInternal>()\n private readonly byId = new Map<string, DevServerInternal>()\n private readonly leases = new Map<string, LeaseInternal>()\n private readonly slotLocks = new Map<string, Promise<unknown>>()\n\n constructor(opts: DevServerOrchestratorOptions) {\n this.proxy = opts.proxy\n this.ports = opts.ports ?? new PortPool()\n this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS\n this.spawnViteFn = opts.spawnVite ?? defaultSpawnVite\n }\n\n async acquire(input: AcquireRequest): Promise<AcquireResponse> {\n if (input.slot.devDomain === DEFAULT_DEV_DOMAIN && !input.allowDefaultDomain) {\n throw new ForbiddenDefaultDomainError()\n }\n if (!existsSync(input.targetCwd)) {\n throw new Error(`targetCwd does not exist: ${input.targetCwd}`)\n }\n\n const key = slotKey(input.slot)\n const prior = this.slotLocks.get(key) ?? Promise.resolve()\n const next = prior.then(() => this.acquireLocked(input))\n this.slotLocks.set(key, next.catch(() => undefined))\n return next\n }\n\n private async acquireLocked(input: AcquireRequest): Promise<AcquireResponse> {\n const key = slotKey(input.slot)\n const existing = this.bySlot.get(key)\n\n if (existing && existing.status === 'ready') {\n // M5 (per-devDomain origin): the slot is one shared origin to the\n // browser. Allowing two different working directories to claim the\n // same slot means cookies/SW/storage written by repo A would leak\n // into repo B. Refuse the rebind with a clear error pointing at the\n // user's storyboard.config.json.\n if (existing.cwd !== input.targetCwd) {\n throw new SlotCwdConflictError(input.slot, existing.cwd, input.targetCwd)\n }\n return this.toResponse(existing, this.mintLease(existing))\n }\n if (existing && existing.status !== 'stopped') {\n try { await existing.readyPromise } catch { /* fall through */ }\n const refreshed = this.bySlot.get(key)\n if (refreshed?.status === 'ready') {\n if (refreshed.cwd !== input.targetCwd) {\n throw new SlotCwdConflictError(input.slot, refreshed.cwd, input.targetCwd)\n }\n return this.toResponse(refreshed, this.mintLease(refreshed))\n }\n }\n\n const port = await this.ports.acquire()\n const id = randomUUID()\n const basePath = input.slot.worktree === 'main' ? '/' : `/branch--${input.slot.worktree}/`\n const child = this.spawnViteFn(input.targetCwd, port, basePath, input.slot.devDomain)\n\n const stderrTail: string[] = []\n let resolveReady: () => void = () => undefined\n let rejectReady: (err: Error) => void = () => undefined\n const readyPromise = new Promise<void>((res, rej) => { resolveReady = res; rejectReady = rej })\n\n const internal: DevServerInternal = {\n id,\n child,\n pid: child.pid ?? 0,\n port,\n status: 'spawning',\n slot: input.slot,\n cwd: input.targetCwd,\n spawnedAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n stderrTail,\n readyPromise,\n }\n this.bySlot.set(key, internal)\n this.byId.set(id, internal)\n\n child.stdout?.on('data', (buf: Buffer) => {\n const text = buf.toString()\n if (internal.status === 'spawning' && /ready in|ready /.test(text)) {\n this.transition(internal, 'ready')\n this.proxy.upsert(internal.slot.devDomain, internal.slot.worktree, internal.port)\n .then(() => resolveReady())\n .catch((err: Error) => resolveReady()) // proxy failure ≠ vite failure; surface via state instead\n .finally(() => undefined)\n // Ensure we don't leave readyPromise pending on proxy failure.\n // Use void to silence floating-promise lint where applicable.\n void rejectReady\n }\n })\n child.stderr?.on('data', (buf: Buffer) => {\n stderrTail.push(buf.toString())\n if (stderrTail.length > STDERR_TAIL_MAX) stderrTail.shift()\n })\n child.on('exit', (code) => {\n if (internal.status === 'stopped') return // already torn down\n const wasReady = internal.status === 'ready'\n this.transition(internal, 'stopped')\n this.bySlot.delete(key)\n this.byId.delete(id)\n // Return the port to the pool.\n this.ports.release(internal.port)\n this.proxy.removeWorktree(internal.slot.devDomain, internal.slot.worktree).catch(() => undefined)\n if (!wasReady) {\n rejectReady(new DevServerSpawnError(internal.slot, code, stderrTail.join('')))\n }\n })\n\n const timeout = new Promise<never>((_res, rej) => {\n setTimeout(() => rej(new Error(`Vite ready timeout after ${this.readyTimeoutMs}ms`)), this.readyTimeoutMs).unref()\n })\n try {\n await Promise.race([readyPromise, timeout])\n } catch (err) {\n try { child.kill('SIGTERM') } catch { /* already dead */ }\n throw err\n }\n\n return this.toResponse(internal, this.mintLease(internal))\n }\n\n release(leaseId: string): void {\n const lease = this.leases.get(leaseId)\n if (!lease) throw new LeaseNotFoundError(leaseId)\n this.leases.delete(leaseId)\n const ds = this.byId.get(lease.devServerId)\n if (!ds) return\n\n const stillReferenced = Array.from(this.leases.values()).some(l => l.devServerId === ds.id)\n if (stillReferenced) return\n\n if (ds.status === 'ready') {\n // SIGTERM on next tick to avoid racing the FSM transition.\n setImmediate(() => {\n try { ds.child.kill('SIGTERM') } catch { /* already dead */ }\n })\n }\n }\n\n list(): DevServer[] {\n return Array.from(this.byId.values()).map(toDevServer)\n }\n\n shutdown(): void {\n for (const ds of this.byId.values()) {\n try { ds.child.kill('SIGTERM') } catch { /* already dead */ }\n }\n }\n\n /** Returns null — kept for the /pool/status endpoint backward-compat. */\n poolStatus(): null { return null }\n\n // Sentinel used as Lease.expiresAt — leases never expire; they live as long\n // as the devserver they reference, which lives as long as the owning CLI.\n private static readonly NEVER_EXPIRES = '9999-12-31T23:59:59.999Z'\n\n private mintLease(ds: DevServerInternal): LeaseInternal {\n const id = randomUUID()\n const url = `http://${ds.slot.devDomain}.localhost${ds.slot.worktree === 'main' ? '/' : `/branch--${ds.slot.worktree}/`}`\n const lease: LeaseInternal = {\n id,\n devServerId: ds.id,\n slot: ds.slot,\n url,\n expiresAt: Number.MAX_SAFE_INTEGER,\n }\n this.leases.set(id, lease)\n return lease\n }\n\n private toResponse(ds: DevServerInternal, lease: LeaseInternal): AcquireResponse {\n return AcquireResponse.parse({\n lease: {\n id: lease.id,\n devServerId: lease.devServerId,\n slot: lease.slot,\n url: lease.url,\n expiresAt: DevServerOrchestrator.NEVER_EXPIRES,\n },\n devServer: toDevServer(ds),\n })\n }\n\n private transition(ds: DevServerInternal, to: DevServerStatus): void {\n assertTransition(ds.status, to)\n ds.status = to\n ds.updatedAt = new Date().toISOString()\n }\n}\n\nfunction toDevServer(ds: DevServerInternal): DevServer {\n return DevServer.parse({\n id: ds.id,\n pid: ds.pid,\n port: ds.port,\n status: ds.status,\n slot: ds.slot,\n cwd: ds.cwd,\n spawnedAt: ds.spawnedAt,\n updatedAt: ds.updatedAt,\n })\n}\n\nfunction defaultSpawnVite(cwd: string, port: Port, basePath: string, devDomain: string): ChildProcess {\n const localVite = resolvePath(cwd, 'node_modules', '.bin', 'vite')\n const useLocal = existsSync(localVite)\n // dist/devserver/orchestrator.js → ../vite-plugin/wrapper.js\n const wrapperPath = resolvePath(import.meta.dirname ?? '', '..', 'vite-plugin', 'wrapper.js')\n const args = ['--port', String(port)]\n if (existsSync(wrapperPath)) {\n args.push('--config', wrapperPath)\n }\n const branchMatch = basePath.match(/^\\/branch--([^/]+)\\/$/)\n const branch = branchMatch ? branchMatch[1]! : 'main'\n const env = {\n ...process.env,\n VITE_BASE_PATH: basePath,\n STORYBOARD_RUNTIME_BRANCH: branch,\n STORYBOARD_RUNTIME_DOMAIN: devDomain,\n }\n return useLocal\n ? spawn(localVite, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })\n : spawn('npx', ['vite', ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })\n}\n","import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { LOCKFILE, PIDFILE, RUNTIME_HOME } from './constants.js'\n\n/**\n * Lockfile-based singleton enforcement.\n *\n * On daemon start we attempt an exclusive create of `~/.storyboard/runtime.lock`.\n * Two daemons can never coexist — without this guarantee the runtime's whole\n * raison d'être (single source of truth for proxy + devservers) collapses.\n *\n * If the lockfile exists but its PID is dead, we treat it as stale and reclaim\n * it. This handles crashes / `kill -9` cleanly without operator intervention.\n */\n\nexport class RuntimeAlreadyRunningError extends Error {\n constructor(public readonly pid: number) {\n super(`Storyboard Runtime is already running (pid ${pid})`)\n this.name = 'RuntimeAlreadyRunningError'\n }\n}\n\nfunction ensureRuntimeHome(): void {\n if (!existsSync(RUNTIME_HOME)) mkdirSync(RUNTIME_HOME, { recursive: true })\n}\n\n/** Returns true if the OS reports a process with `pid` is alive. */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Acquire the runtime lock. Throws RuntimeAlreadyRunningError if another\n * live daemon holds it. On success, writes `process.pid` to the pidfile and\n * returns a release function the caller MUST invoke on shutdown.\n */\nexport function acquireRuntimeLock(): () => void {\n ensureRuntimeHome()\n\n if (existsSync(LOCKFILE)) {\n const raw = readFileSync(LOCKFILE, 'utf8').trim()\n const pid = Number(raw)\n if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {\n throw new RuntimeAlreadyRunningError(pid)\n }\n // Stale lock — the previous daemon crashed. Reclaim it.\n try { unlinkSync(LOCKFILE) } catch { /* race: another claimant */ }\n }\n\n // O_EXCL — atomic create-or-fail. Wins over racing daemons.\n let fd: number\n try {\n fd = openSync(LOCKFILE, 'wx')\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n // Another daemon raced us to the lock.\n const raw = readFileSync(LOCKFILE, 'utf8').trim()\n const pid = Number(raw) || 0\n throw new RuntimeAlreadyRunningError(pid)\n }\n throw err\n }\n writeFileSync(fd, String(process.pid))\n closeSync(fd)\n writeFileSync(PIDFILE, String(process.pid))\n\n let released = false\n return function release(): void {\n if (released) return\n released = true\n try { unlinkSync(LOCKFILE) } catch { /* already gone */ }\n try { unlinkSync(PIDFILE) } catch { /* already gone */ }\n }\n}\n\n/** Returns the live daemon's PID, or null if no daemon is running. */\nexport function readLivePid(): number | null {\n if (!existsSync(PIDFILE)) return null\n try {\n const pid = Number(readFileSync(PIDFILE, 'utf8').trim())\n if (!Number.isFinite(pid) || pid <= 0) return null\n return isProcessAlive(pid) ? pid : null\n } catch {\n return null\n }\n}\n\nvoid dirname // tree-shaking hint: keep node:path import even if unused above\n","import { createRuntimeServer } from './http.js'\nimport { acquireRuntimeLock, RuntimeAlreadyRunningError } from './lock.js'\nimport { RUNTIME_HOST, RUNTIME_PORT } from './constants.js'\n\n/**\n * Daemon entrypoint. Run via `node bin/runtime.js` (typically forked &\n * detached by the CLI on first call).\n *\n * Lifecycle:\n * 1. Acquire singleton lock (`~/.storyboard/runtime.lock`). Refuses to\n * start if another live daemon already holds it.\n * 2. Bind the HTTP server on `127.0.0.1:4321`.\n * 3. On SIGINT/SIGTERM, drain in-flight requests, release the lock, exit 0.\n */\nexport async function startDaemon(): Promise<void> {\n let release: () => void\n try {\n release = acquireRuntimeLock()\n } catch (err) {\n if (err instanceof RuntimeAlreadyRunningError) {\n // eslint-disable-next-line no-console\n console.error(`[storyboard-runtime] already running (pid ${err.pid}). Exiting.`)\n process.exit(0)\n }\n throw err\n }\n\n const server = createRuntimeServer()\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(RUNTIME_PORT, RUNTIME_HOST, () => resolve())\n })\n\n // eslint-disable-next-line no-console\n console.log(`[storyboard-runtime] listening on http://${RUNTIME_HOST}:${RUNTIME_PORT}`)\n\n let shuttingDown = false\n function shutdown(signal: NodeJS.Signals): void {\n if (shuttingDown) return\n shuttingDown = true\n // eslint-disable-next-line no-console\n console.log(`[storyboard-runtime] received ${signal}, shutting down…`)\n // Kill all spawned Vite children before closing the HTTP server.\n try { (globalThis as { __storyboardOrchestratorShutdown?: () => void }).__storyboardOrchestratorShutdown?.() } catch { /* */ }\n server.close(() => {\n release()\n process.exit(0)\n })\n setTimeout(() => {\n release()\n process.exit(0)\n }, 5000).unref()\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","dirname","resolve","existsSync","readFileSync","fileURLToPath","dirname","fileURLToPath","resolve","existsSync","readFileSync","resolve","spawn","existsSync","existsSync","spawn","existsSync","readFileSync","dirname","existsSync","readFileSync","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/schema/identity.ts","../../src/runtime/schema/devserver.ts","../../src/runtime/schema/api.ts","../../src/runtime/client/index.ts","../../src/runtime/server/http.ts","../../src/runtime/proxy/caddy.ts","../../src/runtime/proxy/controller.ts","../../src/runtime/devserver/port-pool.ts","../../src/runtime/server/constants.ts","../../src/runtime/devserver/orchestrator.ts","../../src/runtime/server/lock.ts","../../src/runtime/server/main.ts"],"sourcesContent":["import { z } from 'zod'\n\n/**\n * The legacy/default devDomain. Acquire requests using this value are rejected\n * unless `allowDefaultDomain` is set — see DevServerOrchestrator for details.\n */\nexport const DEFAULT_DEV_DOMAIN = 'storyboard'\n\n/**\n * A devDomain identifies a Storyboard repo on this machine.\n *\n * The literal default value `\"storyboard\"` is intentionally *not* allowed by\n * `acquire` (see schema/acquire.ts) — every checkout MUST set its own\n * `devDomain` in `storyboard.config.json`. This is the structural fix for H3\n * in the server-state RCA: two repos can never share a host space.\n *\n * Allowed: lowercase letters, digits, hyphens. Must start with a letter.\n * 1–32 chars. The runtime constructs the public host as `${devDomain}.localhost`.\n */\nexport const DevDomain = z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z][a-z0-9-]*$/, 'devDomain must match /^[a-z][a-z0-9-]*$/')\n .brand<'DevDomain'>()\nexport type DevDomain = z.infer<typeof DevDomain>\n\n/**\n * A worktree name. `\"main\"` is reserved for the repo root.\n *\n * Names are URL-safe by construction so we never have to escape them when\n * building branch URLs (`/branch--<name>/...`).\n */\nexport const WorktreeName = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9][a-z0-9._-]*$/i, 'worktree name must be URL-safe')\n .brand<'WorktreeName'>()\nexport type WorktreeName = z.infer<typeof WorktreeName>\n\n/**\n * A TCP port the runtime has leased to a devserver. The runtime is the sole\n * authority for port allocation; clients never pick their own port.\n */\nexport const Port = z.number().int().min(1024).max(65535).brand<'Port'>()\nexport type Port = z.infer<typeof Port>\n\n/**\n * The composite key `(devDomain, worktree)` uniquely identifies a devserver.\n *\n * The runtime guarantees at most one devserver per slot — illegal collisions\n * (e.g. two repos trying to claim `(storyboard, main)`) are rejected with\n * `409 CONFLICT` rather than silently overwriting routes.\n */\nexport const DevServerSlot = z.object({\n devDomain: DevDomain,\n worktree: WorktreeName,\n})\nexport type DevServerSlot = z.infer<typeof DevServerSlot>\n\n/**\n * Convert a slot to its canonical string key, used for map lookups and\n * logging. Format: `${devDomain}::${worktree}`.\n */\nexport function slotKey(slot: DevServerSlot): string {\n return `${slot.devDomain}::${slot.worktree}`\n}\n","import { z } from 'zod'\nimport { DevDomain, DevServerSlot, Port, WorktreeName } from './identity.js'\n\n/**\n * DevServer lifecycle FSM.\n *\n * ```\n * spawning → ready → stopped\n * └────────────────┘ (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'stopped', // process exited, slot freed, port returned to the pool\n])\nexport type DevServerStatus = z.infer<typeof DevServerStatus>\n\n/** Allowed FSM transitions. Centralised so misuse is a one-line review catch. */\nexport const ALLOWED_TRANSITIONS: Record<DevServerStatus, readonly DevServerStatus[]> = {\n spawning: ['ready', 'stopped'],\n ready: ['stopped'],\n stopped: [],\n} as const\n\nexport class IllegalTransitionError extends Error {\n constructor(from: DevServerStatus, to: DevServerStatus) {\n super(`Illegal devserver transition: ${from} → ${to}`)\n this.name = 'IllegalTransitionError'\n }\n}\n\nexport function assertTransition(from: DevServerStatus, to: DevServerStatus): void {\n if (!ALLOWED_TRANSITIONS[from].includes(to)) {\n throw new IllegalTransitionError(from, to)\n }\n}\n\n/**\n * A devserver record as exposed by the runtime API. Always bound to a slot\n * after acquire (no pre-spawned pool members), so `slot` and `cwd` are required.\n */\nexport const DevServer = z.object({\n id: z.string().uuid(),\n pid: z.number().int().positive(),\n port: Port,\n status: DevServerStatus,\n slot: DevServerSlot,\n /** Absolute path of the worktree directory. */\n cwd: z.string(),\n /** ISO timestamp; immutable after spawn. */\n spawnedAt: z.string().datetime(),\n /** ISO timestamp of last status change. */\n updatedAt: z.string().datetime(),\n})\nexport type DevServer = z.infer<typeof DevServer>\n\n/**\n * A lease handed to a CLI client when it acquires a devserver.\n *\n * Leases are the *only* way a client controls a devserver — the runtime\n * refuses commands without a valid leaseId. They live as long as the\n * acquiring CLI process; expiry is a far-future sentinel.\n */\nexport const Lease = z.object({\n id: z.string().uuid(),\n devServerId: z.string().uuid(),\n slot: DevServerSlot,\n /** Public proxy URL the client should print to the user. Authoritative. */\n url: z.string().url(),\n /** Far-future sentinel — leases don't actually expire. */\n expiresAt: z.string().datetime(),\n})\nexport type Lease = z.infer<typeof Lease>\n\n/**\n * A Caddy proxy route owned by the runtime. The `@id` is always the devDomain;\n * this lets the runtime PATCH a single route in place without touching others.\n *\n * `upstreams` is keyed by plain string (validated elsewhere as WorktreeName)\n * to avoid `Partial<Record<branded, …>>` shenanigans at the value-spread sites.\n */\nexport const ProxyRoute = z.object({\n devDomain: DevDomain,\n host: z.string(),\n /** worktree name → upstream port. `main` is the host's catch-all. */\n upstreams: z.record(z.string(), Port),\n})\nexport type ProxyRoute = z.infer<typeof ProxyRoute>\n","import { z } from 'zod'\nimport { DevServerSlot, Port } from './identity.js'\nimport { DevServer, Lease, ProxyRoute } from './devserver.js'\n\n/**\n * `POST /devserver/acquire` — request a devserver for a `(devDomain, worktree)` slot.\n *\n * If a devserver already exists for the slot, the runtime returns its existing\n * lease. Otherwise it spawns a new Vite process. The slot is locked for the\n * duration of the call.\n */\nexport const AcquireRequest = z.object({\n slot: DevServerSlot,\n /** Absolute path of the worktree directory; the runtime spawns Vite with `cwd: targetCwd`. */\n targetCwd: z.string().min(1),\n /**\n * Escape hatch for the deprecated default devDomain `\"storyboard\"`. CI and\n * one-off scripts may pass true; the CLI never does.\n */\n allowDefaultDomain: z.boolean().default(false),\n})\nexport type AcquireRequest = z.infer<typeof AcquireRequest>\n\nexport const AcquireResponse = z.object({\n lease: Lease,\n devServer: DevServer,\n})\nexport type AcquireResponse = z.infer<typeof AcquireResponse>\n\n/** `POST /devserver/release` — relinquish the lease and stop the devserver. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `GET /proxy/state` — current routing table the runtime believes Caddy holds. */\nexport const ProxyState = z.object({\n routes: z.array(ProxyRoute),\n caddyReachable: z.boolean(),\n})\nexport type ProxyState = z.infer<typeof ProxyState>\n\n/** `GET /pool/status` — kept for backward compat; always reports zeros. */\nexport const PoolStatus = z.object({\n warm: z.number().int().nonnegative(),\n bound: z.number().int().nonnegative(),\n capacity: z.number().int().nonnegative(),\n})\nexport type PoolStatus = z.infer<typeof PoolStatus>\n\n/** `POST /proxy/upsert` — bind (devDomain, worktree) → port in the proxy. */\nexport const ProxyUpsertRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n port: z.number(),\n})\nexport type ProxyUpsertRequest = z.infer<typeof ProxyUpsertRequest>\n\n/** `POST /proxy/remove` — drop a worktree's route from the proxy. */\nexport const ProxyRemoveRequest = z.object({\n devDomain: z.string(),\n worktree: z.string(),\n})\nexport type ProxyRemoveRequest = z.infer<typeof ProxyRemoveRequest>\n\n/** `GET /health` — daemon liveness probe. */\nexport const Health = z.object({\n ok: z.literal(true),\n version: z.string(),\n uptimeSeconds: z.number().nonnegative(),\n port: Port,\n})\nexport type Health = z.infer<typeof Health>\n\n/** Runtime error envelope. All non-2xx responses share this shape. */\nexport const RuntimeError = z.object({\n error: z.string(),\n code: z.enum([\n 'NOT_IMPLEMENTED',\n 'BAD_REQUEST',\n 'CONFLICT',\n 'NOT_FOUND',\n 'FORBIDDEN_DEFAULT_DOMAIN',\n 'INTERNAL',\n 'CADDY_UNREACHABLE',\n 'PORT_EXHAUSTED',\n 'TIMEOUT',\n ]),\n details: z.unknown().optional(),\n})\nexport type RuntimeError = z.infer<typeof RuntimeError>\n","import { spawn } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport {\n AcquireRequest,\n AcquireResponse,\n Health,\n PoolStatus,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n} from '../schema/index.js'\nimport type { z } from 'zod'\n\n/**\n * Typed JS/TS client for the Storyboard Runtime daemon.\n *\n * Consumers should always go through this client rather than hand-rolling\n * `fetch` calls — the client is the only place where the daemon's URL is\n * known, and it's the only place where on-demand daemon spawning happens.\n */\n\nconst RUNTIME_BASE = 'http://127.0.0.1:4321'\n\nexport interface RuntimeClientOptions {\n /** Override base URL (mostly for tests). */\n baseUrl?: string\n /** Auto-start the daemon if it isn't running. Default: true. */\n autoStart?: boolean\n}\n\nexport class RuntimeRequestError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: z.infer<typeof RuntimeError>['code'],\n public readonly details?: unknown,\n ) {\n super(message)\n this.name = 'RuntimeRequestError'\n }\n}\n\nasync function request<S extends z.ZodTypeAny>(\n baseUrl: string,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n responseSchema: S | null,\n): Promise<S extends z.ZodTypeAny ? z.output<S> : void> {\n const init: RequestInit = {\n method,\n headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n }\n let res: Response\n try {\n res = await fetch(`${baseUrl}${path}`, init)\n } catch (err) {\n throw new RuntimeRequestError(\n `Cannot reach Storyboard Runtime at ${baseUrl} — is the daemon running? (${(err as Error).message})`,\n 0,\n 'INTERNAL',\n )\n }\n const text = await res.text()\n let parsed: unknown\n try { parsed = text ? JSON.parse(text) : {} }\n catch { parsed = { error: text, code: 'INTERNAL' } }\n\n if (!res.ok) {\n const err = RuntimeError.safeParse(parsed)\n if (err.success) {\n throw new RuntimeRequestError(err.data.error, res.status, err.data.code, err.data.details)\n }\n throw new RuntimeRequestError(`HTTP ${res.status}`, res.status, 'INTERNAL', parsed)\n }\n if (responseSchema === null) return undefined as never\n return responseSchema.parse(parsed) as never\n}\n\n/**\n * Spawn the daemon as a detached child. Resolves once the health endpoint\n * answers (or rejects after a short timeout).\n */\nasync function spawnDaemon(baseUrl: string): Promise<void> {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin/storyboard-runtime.js lives next to dist/, two levels up from\n // dist/runtime/client/index.js (the published path).\n const binPath = resolve(here, '..', '..', '..', 'bin', 'storyboard-runtime.js')\n const child = spawn(process.execPath, [binPath], {\n detached: true,\n stdio: 'ignore',\n env: process.env,\n })\n child.unref()\n\n // Poll /health until the daemon is up.\n const deadline = Date.now() + 5000\n while (Date.now() < deadline) {\n try {\n const r = await fetch(`${baseUrl}/health`)\n if (r.ok) return\n } catch { /* not up yet */ }\n await new Promise(r => setTimeout(r, 100))\n }\n throw new Error(\n `Storyboard Runtime did not become ready within 5s ` +\n `(tried to spawn ${binPath})`,\n )\n}\n\n/**\n * Read the @dfosco/storyboard package.json version that this client is\n * shipping with. Used to detect mismatches against a long-lived daemon\n * that may have been spawned by a previous install.\n */\nfunction readClientVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n }\n }\n } catch { /* ignore */ }\n return '0.0.0'\n}\n\nconst CLIENT_VERSION = readClientVersion()\n\n/**\n * Send SIGTERM to the daemon PID and clear its lock/pid files so\n * spawnDaemon() can start a fresh one.\n */\nfunction killExistingDaemon(): void {\n try {\n const pidPath = resolve(process.env.HOME || '', '.storyboard', 'runtime.pid')\n if (!existsSync(pidPath)) return\n const pid = Number(readFileSync(pidPath, 'utf8').trim())\n if (Number.isFinite(pid) && pid > 0) {\n try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }\n }\n } catch { /* ignore */ }\n}\n\nexport class RuntimeClient {\n readonly baseUrl: string\n readonly autoStart: boolean\n\n constructor(opts: RuntimeClientOptions = {}) {\n this.baseUrl = opts.baseUrl ?? RUNTIME_BASE\n this.autoStart = opts.autoStart !== false\n }\n\n async health(): Promise<Health> {\n try {\n const result = await request(this.baseUrl, 'GET', '/health', undefined, Health)\n // Auto-respawn on version mismatch — a long-lived daemon from a\n // previous install otherwise keeps serving stale code after upgrade.\n // Skip when client reports 0.0.0 (dev/source layout where package\n // version isn't meaningful).\n if (\n this.autoStart &&\n CLIENT_VERSION !== '0.0.0' &&\n result.version !== '0.0.0' &&\n result.version !== CLIENT_VERSION\n ) {\n // Don't kill a daemon that's actively hosting other dev servers —\n // doing so would SIGTERM every Vite child including ones owned by\n // unrelated repos. Multi-repo coexistence is the entire point of\n // running a shared daemon. Warn and reuse instead; the user can\n // manually `sb reset` when they're ready to upgrade.\n let activeCount = 0\n try {\n const listRes = await fetch(`${this.baseUrl}/devserver/list`)\n if (listRes.ok) {\n const data = await listRes.json() as { devServers?: unknown[] }\n activeCount = Array.isArray(data.devServers) ? data.devServers.length : 0\n }\n } catch { /* fall through; treat as zero */ }\n if (activeCount > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `[storyboard] daemon version ${result.version} differs from client ${CLIENT_VERSION}, ` +\n `but ${activeCount} dev server(s) are active. Reusing the existing daemon. ` +\n `Run \\`sb reset\\` to restart it once those are stopped.`,\n )\n return result\n }\n killExistingDaemon()\n // Give the OS a moment to release port 4321\n await new Promise(r => setTimeout(r, 250))\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n return result\n } catch (err) {\n if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {\n await spawnDaemon(this.baseUrl)\n return request(this.baseUrl, 'GET', '/health', undefined, Health)\n }\n throw err\n }\n }\n\n async acquire(input: z.input<typeof AcquireRequest>): Promise<AcquireResponse> {\n // Force a health check first — this is the sole codepath that detects\n // a stale daemon (different version, crashed mid-restart, etc.) and\n // respawns it. Without this, `sb run` would happily POST against an\n // outdated daemon and inherit all of its bugs.\n if (this.autoStart) {\n try { await this.health() } catch { /* health() will throw on hard failure; let acquire surface it */ }\n }\n const body = AcquireRequest.parse(input)\n return request(this.baseUrl, 'POST', '/devserver/acquire', body, AcquireResponse)\n }\n\n async release(input: z.input<typeof ReleaseRequest>): Promise<void> {\n const body = ReleaseRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/release', body, null)\n }\n\n async proxyState(): Promise<ProxyState> {\n return request(this.baseUrl, 'GET', '/proxy/state', undefined, ProxyState)\n }\n\n async proxyUpsert(input: z.input<typeof ProxyUpsertRequest>): Promise<ProxyState> {\n const body = ProxyUpsertRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/upsert', body, ProxyState)\n }\n\n async proxyRemove(input: z.input<typeof ProxyRemoveRequest>): Promise<ProxyState> {\n const body = ProxyRemoveRequest.parse(input)\n return request(this.baseUrl, 'POST', '/proxy/remove', body, ProxyState)\n }\n\n async poolStatus(): Promise<PoolStatus> {\n return request(this.baseUrl, 'GET', '/pool/status', undefined, PoolStatus)\n }\n}\n\n/** Default singleton client for casual callers. */\nexport const runtime = new RuntimeClient()\n","import http from 'node:http'\nimport { z } from 'zod'\nimport {\n AcquireRequest,\n AcquireResponse,\n DevDomain,\n Health,\n PoolStatus,\n Port,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RuntimeError,\n WorktreeName,\n} from '../schema/index.js'\nimport { ProxyController } from '../proxy/index.js'\nimport { CaddyUnreachableError } from '../proxy/caddy.js'\nimport {\n DevServerOrchestrator,\n ForbiddenDefaultDomainError,\n LeaseNotFoundError,\n DevServerSpawnError,\n SlotCwdConflictError,\n} from '../devserver/index.js'\nimport { PortPool } from '../devserver/port-pool.js'\nimport { RUNTIME_PORT, RUNTIME_HOST, RUNTIME_VERSION } from './constants.js'\n\n/**\n * The runtime's HTTP API.\n *\n * Every request is parsed through a zod schema *before* any handler runs;\n * malformed input never reaches the orchestrator. Every response is also\n * shape-checked in development to prevent accidental contract drift.\n */\n\nconst startedAt = Date.now()\n\n/**\n * Process-wide singletons. The runtime is one-per-machine, so module-level\n * controllers are fine. Tests inject their own via createRuntimeServer({ ... }).\n */\nlet proxyController = new ProxyController()\nlet _ports = new PortPool()\nlet orchestrator: DevServerOrchestrator = new DevServerOrchestrator({\n proxy: proxyController,\n ports: _ports,\n})\n\nfunction sendJson(\n res: http.ServerResponse,\n status: number,\n body: unknown,\n schema?: z.ZodTypeAny,\n): void {\n if (schema && process.env.NODE_ENV !== 'production') {\n schema.parse(body)\n }\n res.writeHead(status, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n}\n\nfunction sendError(\n res: http.ServerResponse,\n status: number,\n code: z.infer<typeof RuntimeError>['code'],\n message: string,\n details?: unknown,\n): void {\n const body = RuntimeError.parse({ error: message, code, details })\n sendJson(res, status, body, RuntimeError)\n}\n\nasync function readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n if (chunks.length === 0) return {}\n try {\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n } catch {\n throw new Error('Malformed JSON body')\n }\n}\n\nasync function parseBody<S extends z.ZodTypeAny>(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n schema: S,\n): Promise<z.output<S> | null> {\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (err) {\n sendError(res, 400, 'BAD_REQUEST', (err as Error).message)\n return null\n }\n const result = schema.safeParse(raw)\n if (!result.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed', result.error.flatten())\n return null\n }\n return result.data\n}\n\ntype Route = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> | void\n\nconst routes = new Map<string, Route>()\n\nroutes.set('GET /health', (_req, res) => {\n const body: Health = Health.parse({\n ok: true,\n version: RUNTIME_VERSION,\n uptimeSeconds: (Date.now() - startedAt) / 1000,\n port: RUNTIME_PORT,\n })\n sendJson(res, 200, body, Health)\n})\n\nroutes.set('POST /devserver/acquire', async (req, res) => {\n const body = await parseBody(req, res, AcquireRequest)\n if (!body) return\n try {\n const result = await orchestrator.acquire(body)\n sendJson(res, 200, result, AcquireResponse)\n } catch (err) {\n if (err instanceof ForbiddenDefaultDomainError) {\n sendError(res, 403, 'FORBIDDEN_DEFAULT_DOMAIN', err.message)\n return\n }\n if (err instanceof SlotCwdConflictError) {\n sendError(res, 409, 'CONFLICT', err.message)\n return\n }\n if (err instanceof DevServerSpawnError) {\n sendError(res, 500, 'INTERNAL', err.message, { stderr: err.stderr })\n return\n }\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /devserver/release', async (req, res) => {\n const body = await parseBody(req, res, ReleaseRequest)\n if (!body) return\n try {\n orchestrator.release(body.leaseId)\n sendJson(res, 200, { ok: true })\n } catch (err) {\n if (err instanceof LeaseNotFoundError) {\n sendError(res, 404, 'NOT_FOUND', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('GET /devserver/list', (_req, res) => {\n sendJson(res, 200, { devServers: orchestrator.list() })\n})\n\nroutes.set('GET /proxy/state', async (_req, res) => {\n try {\n const body = await proxyController.stateForApi()\n sendJson(res, 200, body, ProxyState)\n } catch (err) {\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /proxy/upsert', async (req, res) => {\n const body = await parseBody(req, res, ProxyUpsertRequest)\n if (!body) return\n // Re-validate with branded types — the API request schema is intentionally\n // loose (string/number) so bad input gets a useful 400 rather than the\n // brand regex's cryptic message.\n const dev = DevDomain.safeParse(body.devDomain)\n const wt = WorktreeName.safeParse(body.worktree)\n const port = Port.safeParse(body.port)\n if (!dev.success || !wt.success || !port.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed', {\n devDomain: dev.success ? null : dev.error.flatten(),\n worktree: wt.success ? null : wt.error.flatten(),\n port: port.success ? null : port.error.flatten(),\n })\n return\n }\n if (dev.data === ('storyboard' as unknown as typeof dev.data)) {\n sendError(res, 403, 'FORBIDDEN_DEFAULT_DOMAIN',\n 'Refusing to bind a route under the default devDomain \"storyboard\". ' +\n 'Set a unique devDomain in storyboard.config.json.')\n return\n }\n try {\n const route = await proxyController.upsert(dev.data, wt.data, port.data)\n sendJson(res, 200, ProxyState.parse({ routes: [route], caddyReachable: true }), ProxyState)\n } catch (err) {\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('POST /proxy/remove', async (req, res) => {\n const body = await parseBody(req, res, ProxyRemoveRequest)\n if (!body) return\n const dev = DevDomain.safeParse(body.devDomain)\n const wt = WorktreeName.safeParse(body.worktree)\n if (!dev.success || !wt.success) {\n sendError(res, 400, 'BAD_REQUEST', 'Validation failed')\n return\n }\n try {\n await proxyController.removeWorktree(dev.data, wt.data)\n sendJson(res, 200, ProxyState.parse({ routes: [], caddyReachable: true }), ProxyState)\n } catch (err) {\n if (err instanceof CaddyUnreachableError) {\n sendError(res, 503, 'CADDY_UNREACHABLE', err.message)\n return\n }\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n})\n\nroutes.set('GET /pool/status', (_req, res) => {\n // Prefer the orchestrator's view (reflects bound count); fall back to the\n // raw-pool snapshot when no hot pool is configured.\n const live = orchestrator.poolStatus()\n const body = PoolStatus.parse(live ?? { warm: 0, bound: 0, capacity: 0 })\n sendJson(res, 200, body, PoolStatus)\n})\n\nexport function createRuntimeServer(opts: {\n proxyController?: ProxyController\n orchestrator?: DevServerOrchestrator\n} = {}): http.Server {\n if (opts.proxyController) proxyController = opts.proxyController\n if (opts.orchestrator) orchestrator = opts.orchestrator\n // Reconcile Caddy on startup: delete any @id-tagged routes left over from\n // a previous daemon process. Without this, a recycled port can be hit by\n // a stale host route and serve the wrong repo (see \"Wrong domain 421\").\n void proxyController.reconcileFromCaddy().catch(() => undefined)\n // Expose orchestrator shutdown for the daemon's signal handler.\n ;(globalThis as { __storyboardOrchestratorShutdown?: () => void }).__storyboardOrchestratorShutdown = () => orchestrator.shutdown()\n const server = http.createServer(async (req, res) => {\n try {\n const url = new URL(req.url ?? '/', `http://${RUNTIME_HOST}:${RUNTIME_PORT}`)\n const key = `${req.method ?? 'GET'} ${url.pathname}`\n const handler = routes.get(key)\n if (!handler) {\n sendError(res, 404, 'NOT_FOUND', `No route for ${key}`)\n return\n }\n await handler(req, res)\n } catch (err) {\n sendError(res, 500, 'INTERNAL', (err as Error).message)\n }\n })\n return server\n}\n","import { z } from 'zod'\nimport { DevDomain, Port, WorktreeName } from '../schema/index.js'\n\n/**\n * Typed Caddy admin API client. The runtime is the SOLE writer to\n * `http://localhost:2019` — this module is the only place in the codebase\n * that issues PATCH/POST/DELETE against Caddy. All cross-repo route races\n * (the doubled-URL bug class) are eliminated by funneling writes here.\n *\n * Reads are also funneled through this module so the runtime's view of\n * Caddy state matches what's actually live.\n */\n\nexport const CADDY_ADMIN_DEFAULT = 'http://localhost:2019'\n\nexport const CaddyRoute = z\n .object({\n '@id': z.string().optional(),\n match: z.array(z.object({ host: z.array(z.string()).optional() }).passthrough()).optional(),\n handle: z.array(z.unknown()).optional(),\n })\n .passthrough()\nexport type CaddyRoute = z.infer<typeof CaddyRoute>\n\nexport interface CaddyAdminOptions {\n baseUrl?: string\n timeoutMs?: number\n}\n\nexport class CaddyUnreachableError extends Error {\n constructor(baseUrl: string, cause?: unknown) {\n super(`Caddy admin API not reachable at ${baseUrl}`)\n this.name = 'CaddyUnreachableError'\n if (cause) (this as Error & { cause?: unknown }).cause = cause\n }\n}\n\nexport class CaddyAdminClient {\n readonly baseUrl: string\n readonly timeoutMs: number\n\n constructor(opts: CaddyAdminOptions = {}) {\n this.baseUrl = opts.baseUrl ?? CADDY_ADMIN_DEFAULT\n this.timeoutMs = opts.timeoutMs ?? 5000\n }\n\n private async fetch(path: string, init: RequestInit = {}): Promise<Response> {\n const controller = new AbortController()\n const t = setTimeout(() => controller.abort(), this.timeoutMs)\n try {\n // Caddy's admin API rejects requests with 403 when the Origin header\n // doesn't match an allowed origin. Node's undici fetch sends an empty\n // Origin by default, which Caddy refuses. Set it explicitly to the\n // admin URL.\n const headers = new Headers(init.headers)\n if (!headers.has('origin')) headers.set('origin', this.baseUrl)\n return await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal: controller.signal })\n } catch (err) {\n throw new CaddyUnreachableError(this.baseUrl, err)\n } finally {\n clearTimeout(t)\n }\n }\n\n async ping(): Promise<boolean> {\n try {\n const r = await this.fetch('/config/')\n return r.ok\n } catch {\n return false\n }\n }\n\n async listRoutes(): Promise<CaddyRoute[]> {\n const r = await this.fetch('/config/apps/http/servers/srv0/routes')\n if (r.status === 404) return []\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n const raw = (await r.json()) as unknown\n return z.array(CaddyRoute).parse(raw ?? [])\n }\n\n async patchById(id: string, route: CaddyRoute): Promise<boolean> {\n const r = await this.fetch(`/id/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(route),\n })\n return r.ok\n }\n\n async appendRoute(route: CaddyRoute): Promise<void> {\n const r = await this.fetch('/config/apps/http/servers/srv0/routes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(route),\n })\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n }\n\n async deleteRouteAt(idx: number): Promise<void> {\n const r = await this.fetch(`/config/apps/http/servers/srv0/routes/${idx}`, {\n method: 'DELETE',\n })\n if (!r.ok && r.status !== 404) throw new CaddyUnreachableError(this.baseUrl)\n }\n\n async bootstrap(): Promise<void> {\n const config = {\n apps: { http: { servers: { srv0: { listen: [':80'], routes: [] } } } },\n }\n const r = await this.fetch('/load', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(config),\n })\n if (!r.ok) throw new CaddyUnreachableError(this.baseUrl)\n }\n}\n\n/**\n * Build a Caddy route for a single devDomain, with one subroute per worktree.\n * The `@id` is the devDomain itself so writes are PATCHable in place.\n */\nexport function buildRouteFor(\n devDomain: DevDomain,\n upstreams: Record<string, Port>,\n mainPort?: Port,\n): CaddyRoute {\n const host = `${devDomain}.localhost`\n const subroutes: unknown[] = []\n\n for (const [worktree, port] of Object.entries(upstreams)) {\n if (worktree === 'main') continue\n subroutes.push({\n match: [{ path: [`/branch--${worktree}/*`] }],\n handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${port}` }] }],\n })\n }\n\n const fallbackPort = mainPort ?? upstreams.main ?? Object.values(upstreams)[0]\n if (fallbackPort) {\n subroutes.push({\n handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${fallbackPort}` }] }],\n })\n }\n\n return CaddyRoute.parse({\n '@id': devDomain,\n match: [{ host: [host] }],\n handle: [{ handler: 'subroute', routes: subroutes }],\n })\n}\n\nvoid WorktreeName\n","import { DevDomain, Port, ProxyRoute, ProxyState, WorktreeName } from '../schema/index.js'\nimport { buildRouteFor, CaddyAdminClient, CaddyUnreachableError, CaddyRoute } from './caddy.js'\n\n/**\n * ProxyController — the runtime's authoritative model of the Caddy proxy.\n *\n * Two invariants the rest of the system depends on:\n *\n * 1. **Sole writer.** Only this controller calls Caddy admin write methods\n * (PATCH/POST/DELETE). The legacy per-repo `caddy reload --config` path\n * that destructively replaced the entire config is no longer reachable\n * from any code under the runtime.\n *\n * 2. **Serialized writes.** Every mutation is queued through `withLock()`.\n * Two concurrent acquires for different devDomains can no longer race\n * each other into producing a Caddy config where one repo's routes\n * silently shadowed another's.\n *\n * The in-memory `routes` map is the desired state. After every write we\n * reconcile against Caddy's actual state and re-PATCH if drift is detected.\n */\n\nexport interface ProxyControllerOptions {\n caddy?: CaddyAdminClient\n}\n\nexport class ProxyController {\n private readonly caddy: CaddyAdminClient\n private readonly routes = new Map<DevDomain, ProxyRoute>()\n private writeChain: Promise<unknown> = Promise.resolve()\n\n constructor(opts: ProxyControllerOptions = {}) {\n this.caddy = opts.caddy ?? new CaddyAdminClient()\n }\n\n private withLock<T>(fn: () => Promise<T>): Promise<T> {\n const next = this.writeChain.then(fn, fn)\n this.writeChain = next.catch(() => undefined)\n return next\n }\n\n async caddyReachable(): Promise<boolean> {\n return this.caddy.ping()\n }\n\n async stateForApi(): Promise<ProxyState> {\n return ProxyState.parse({\n routes: Array.from(this.routes.values()),\n caddyReachable: await this.caddy.ping(),\n })\n }\n\n async upsert(devDomain: DevDomain, worktree: WorktreeName, port: Port): Promise<ProxyRoute> {\n return this.withLock(async () => {\n // Evict this port from every other devDomain. A port can be reassigned\n // when its previous owner exits, but the previous devDomain's Caddy\n // route still references the port number — so requests to host A would\n // silently land on host B's Vite (which then 421s with \"Wrong domain\").\n // Strip the port from sibling routes before binding it here.\n for (const [otherDomain, otherRoute] of this.routes) {\n if (otherDomain === devDomain) continue\n let mutated = false\n const nextUpstreams: Record<string, Port> = {}\n for (const [w, p] of Object.entries(otherRoute.upstreams)) {\n if (Number(p) === Number(port)) { mutated = true; continue }\n nextUpstreams[w] = p\n }\n if (!mutated) continue\n if (Object.keys(nextUpstreams).length === 0) {\n this.routes.delete(otherDomain)\n await this.removeFromCaddy(otherDomain)\n } else {\n const cleaned = ProxyRoute.parse({\n devDomain: otherDomain,\n host: `${otherDomain}.localhost`,\n upstreams: nextUpstreams,\n })\n this.routes.set(otherDomain, cleaned)\n await this.syncOne(cleaned)\n }\n }\n\n const existing = this.routes.get(devDomain)\n const upstreams: Record<string, Port> = { ...(existing?.upstreams ?? {}) }\n upstreams[worktree] = port\n const next = ProxyRoute.parse({\n devDomain,\n host: `${devDomain}.localhost`,\n upstreams,\n })\n this.routes.set(devDomain, next)\n await this.syncOne(next)\n return next\n })\n }\n\n async removeWorktree(devDomain: DevDomain, worktree: WorktreeName): Promise<void> {\n return this.withLock(async () => {\n const existing = this.routes.get(devDomain)\n if (!existing) return\n const upstreams: Record<string, Port> = { ...existing.upstreams }\n delete upstreams[worktree]\n if (Object.keys(upstreams).length === 0) {\n this.routes.delete(devDomain)\n await this.removeFromCaddy(devDomain)\n } else {\n const next = ProxyRoute.parse({\n devDomain,\n host: `${devDomain}.localhost`,\n upstreams,\n })\n this.routes.set(devDomain, next)\n await this.syncOne(next)\n }\n })\n }\n\n private async syncOne(route: ProxyRoute): Promise<void> {\n if (!(await this.caddy.ping())) {\n throw new CaddyUnreachableError(this.caddy.baseUrl)\n }\n const caddyRoute = buildRouteFor(route.devDomain, route.upstreams as Record<string, Port>)\n const patched = await this.caddy.patchById(route.devDomain, caddyRoute)\n if (!patched) {\n await this.caddy.appendRoute(caddyRoute)\n }\n await this.cleanupDuplicates(route.devDomain, `${route.devDomain}.localhost`)\n }\n\n private async removeFromCaddy(devDomain: DevDomain): Promise<void> {\n if (!(await this.caddy.ping())) throw new CaddyUnreachableError(this.caddy.baseUrl)\n const all = await this.caddy.listRoutes()\n const indices: number[] = []\n for (let i = 0; i < all.length; i++) {\n if (all[i]!['@id'] === devDomain) indices.push(i)\n }\n for (const i of indices.sort((a, b) => b - a)) {\n await this.caddy.deleteRouteAt(i)\n }\n }\n\n /**\n * Stale-route cleanup. The legacy `caddy reload` path used to leave\n * non-`@id` routes for the same host that would shadow our admin-API\n * route. We sweep them on every successful upsert.\n */\n private async cleanupDuplicates(keepId: DevDomain, host: string): Promise<void> {\n const all = await this.caddy.listRoutes()\n const stale: number[] = []\n for (let i = 0; i < all.length; i++) {\n const r = all[i]!\n if (r['@id'] === keepId) continue\n if (r['@id']) continue\n const hosts = r.match?.[0]?.host ?? []\n if (hosts.includes(host)) stale.push(i)\n }\n for (const i of stale.sort((a, b) => b - a)) {\n try { await this.caddy.deleteRouteAt(i) } catch { /* best-effort */ }\n }\n }\n\n async readLiveCaddyRoutes(): Promise<CaddyRoute[]> {\n return this.caddy.listRoutes()\n }\n\n /**\n * Reconcile Caddy with this controller's in-memory desired state.\n *\n * Called once on daemon startup. The runtime is the sole writer to Caddy,\n * so any `@id`-tagged route the controller doesn't know about is a leftover\n * from a previous daemon process. Leaving them in place causes the bug\n * where a recycled port hits a stale host (e.g. `storyboard.localhost` →\n * port 1240, which now belongs to a different repo). We delete them.\n *\n * Does NOT touch routes without `@id` — those may be hand-installed Caddy\n * rules outside our ownership; legacy duplicate cleanup happens per-upsert.\n */\n async reconcileFromCaddy(): Promise<void> {\n return this.withLock(async () => {\n if (!(await this.caddy.ping())) return\n const all = await this.caddy.listRoutes()\n const owned = new Set<string>(this.routes.keys())\n const stale: number[] = []\n for (let i = 0; i < all.length; i++) {\n const id = all[i]!['@id']\n if (id && !owned.has(id)) stale.push(i)\n }\n for (const i of stale.sort((a, b) => b - a)) {\n try { await this.caddy.deleteRouteAt(i) } catch { /* best-effort */ }\n }\n })\n }\n}\n","import { createServer } from 'node:net'\nimport { DEVSERVER_PORT_MAX, DEVSERVER_PORT_MIN } from '../server/constants.js'\nimport { Port } from '../schema/index.js'\n\n/**\n * PortPool — leases TCP ports from the runtime's reserved range.\n *\n * The runtime is the sole authority for port allocation. Vite child processes\n * never pick their own port; they're told which port to bind to. This is the\n * structural fix for hypothesis H4 (independent per-repo port registries with\n * no cross-repo awareness): leases live in this single in-memory pool, and\n * collisions are impossible because the pool refuses to hand out a port\n * that's already leased.\n */\n\nexport class PortExhaustedError extends Error {\n constructor() {\n super(`No free ports in range ${DEVSERVER_PORT_MIN}-${DEVSERVER_PORT_MAX}`)\n this.name = 'PortExhaustedError'\n }\n}\n\nexport class PortPool {\n private readonly leased = new Set<number>()\n\n async acquire(): Promise<Port> {\n for (let p = DEVSERVER_PORT_MIN; p <= DEVSERVER_PORT_MAX; p++) {\n if (this.leased.has(p)) continue\n if (await isPortFree(p)) {\n this.leased.add(p)\n return Port.parse(p)\n }\n }\n throw new PortExhaustedError()\n }\n\n release(port: Port | number): void {\n this.leased.delete(Number(port))\n }\n\n isLeased(port: Port | number): boolean {\n return this.leased.has(Number(port))\n }\n\n size(): number {\n return this.leased.size\n }\n}\n\nfunction isPortFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = createServer()\n probe.once('error', () => resolve(false))\n probe.once('listening', () => {\n probe.close(() => resolve(true))\n })\n probe.listen(port, '127.0.0.1')\n })\n}\n","import { homedir } from 'node:os'\nimport { join, dirname, resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Runtime constants — single source of truth for ports, paths, and version.\n *\n * The runtime port (`4321`) is intentionally fixed: a single global daemon\n * needs a well-known address so the CLI client can find it without\n * configuration. Port 4321 is RFC-allowed and avoids collisions with\n * Vite (1234), Caddy admin (2019), and the per-repo legacy server (4100-4199).\n */\nexport const RUNTIME_PORT = 4321 as const\nexport const RUNTIME_HOST = '127.0.0.1' as const\n\n/** Where the daemon writes its pidfile and lockfile. */\nexport const RUNTIME_HOME = join(homedir(), '.storyboard')\nexport const PIDFILE = join(RUNTIME_HOME, 'runtime.pid')\nexport const LOCKFILE = join(RUNTIME_HOME, 'runtime.lock')\nexport const STATEFILE = join(RUNTIME_HOME, 'runtime.state.json')\n\n/**\n * Range of TCP ports the runtime leases to Vite devservers. Picked to sit\n * above Vite's default (1234) and below ephemeral-port territory.\n */\nexport const DEVSERVER_PORT_MIN = 1240 as const\nexport const DEVSERVER_PORT_MAX = 1399 as const\n\n/** Caddy admin API endpoint. The runtime is the SOLE writer to this URL. */\nexport const CADDY_ADMIN = 'http://localhost:2019' as const\n\n// Runtime version is read from the package.json at startup so it tracks\n// the published @dfosco/storyboard version. Falls back to \"0.0.0\" if the\n// file can't be located (e.g. dev script runs from a non-standard layout).\nfunction readPackageVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n // Bundled to dist/runtime/server/main.js → ../../../package.json\n const candidates = [\n resolve(here, '..', '..', '..', 'package.json'),\n resolve(here, '..', '..', 'package.json'),\n ]\n for (const p of candidates) {\n if (existsSync(p)) {\n const pkg = JSON.parse(readFileSync(p, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string' && pkg.version) return pkg.version\n }\n }\n } catch { /* fallthrough */ }\n return '0.0.0'\n}\n\nexport const RUNTIME_VERSION = readPackageVersion()\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { resolve as resolvePath } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport {\n AcquireRequest,\n AcquireResponse,\n assertTransition,\n DevServer,\n DevServerSlot,\n DevServerStatus,\n DEFAULT_DEV_DOMAIN,\n Port,\n slotKey,\n} from '../schema/index.js'\nimport { ProxyController } from '../proxy/index.js'\nimport { PortPool } from './port-pool.js'\n\n/**\n * DevServerOrchestrator — owns the lifecycle of every Vite dev process.\n *\n * Invariants enforced here:\n *\n * 1. **Slot uniqueness.** At most one DevServer per `(devDomain, worktree)`.\n * Acquire on an existing slot returns the same DevServer with a fresh\n * lease. Per-slot mutex prevents concurrent double-spawn.\n *\n * 2. **Default-domain refusal (H3).** `acquire()` rejects requests whose\n * devDomain is the literal `\"storyboard\"` unless `allowDefaultDomain`\n * is true. The CLI never passes `true`; only CI/scripts do.\n *\n * 3. **FSM.** Every status change goes through `assertTransition()`.\n * Illegal transitions throw — no devserver can be `ready` without\n * having gone through `spawning` first.\n *\n * 4. **Lease enforcement.** Release/renew require a valid lease ID. A stale\n * `sb dev` cannot kill a devserver claimed by a newer session.\n */\n\ninterface DevServerInternal {\n id: string\n child: ChildProcess\n pid: number\n port: Port\n status: DevServerStatus\n slot: DevServerSlot\n cwd: string\n spawnedAt: string\n updatedAt: string\n stderrTail: string[]\n readyPromise: Promise<void>\n}\n\ninterface LeaseInternal {\n id: string\n devServerId: string\n slot: DevServerSlot\n url: string\n expiresAt: number\n}\n\nexport class SlotCwdConflictError extends Error {\n constructor(slot: DevServerSlot, existingCwd: string, requestedCwd: string) {\n super(\n `Slot ${slotKey(slot)} is already bound to ${existingCwd}; refusing to rebind to ${requestedCwd}. ` +\n `Two different repositories cannot share devDomain \"${slot.devDomain}\". ` +\n `Set a unique devDomain in storyboard.config.json for one of them.`,\n )\n this.name = 'SlotCwdConflictError'\n }\n}\n\nexport class ForbiddenDefaultDomainError extends Error {\n constructor() {\n super(\n `Refusing to acquire under default devDomain \"${DEFAULT_DEV_DOMAIN}\". ` +\n `Set a unique devDomain in storyboard.config.json or pass allowDefaultDomain=true.`,\n )\n this.name = 'ForbiddenDefaultDomainError'\n }\n}\n\nexport class LeaseNotFoundError extends Error {\n constructor(id: string) {\n super(`No lease ${id}`)\n this.name = 'LeaseNotFoundError'\n }\n}\n\nexport class DevServerSpawnError extends Error {\n readonly stderr: string\n constructor(slot: DevServerSlot, exitCode: number | null, stderr: string) {\n super(`Vite for ${slotKey(slot)} exited (code ${exitCode ?? 'unknown'}) before becoming ready`)\n this.name = 'DevServerSpawnError'\n this.stderr = stderr\n }\n}\n\nexport interface DevServerOrchestratorOptions {\n proxy: ProxyController\n ports?: PortPool\n /** Override Vite spawn for tests — return a child you've prepared. */\n spawnVite?: (cwd: string, port: Port, basePath: string, devDomain: string) => ChildProcess\n readyTimeoutMs?: number\n}\n\nconst DEFAULT_READY_TIMEOUT_MS = 30_000\nconst STDERR_TAIL_MAX = 50\n\nexport class DevServerOrchestrator {\n private readonly proxy: ProxyController\n private readonly ports: PortPool\n private readonly readyTimeoutMs: number\n private readonly spawnViteFn: NonNullable<DevServerOrchestratorOptions['spawnVite']>\n\n private readonly bySlot = new Map<string, DevServerInternal>()\n private readonly byId = new Map<string, DevServerInternal>()\n private readonly leases = new Map<string, LeaseInternal>()\n private readonly slotLocks = new Map<string, Promise<unknown>>()\n\n constructor(opts: DevServerOrchestratorOptions) {\n this.proxy = opts.proxy\n this.ports = opts.ports ?? new PortPool()\n this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS\n this.spawnViteFn = opts.spawnVite ?? defaultSpawnVite\n }\n\n async acquire(input: AcquireRequest): Promise<AcquireResponse> {\n if (input.slot.devDomain === DEFAULT_DEV_DOMAIN && !input.allowDefaultDomain) {\n throw new ForbiddenDefaultDomainError()\n }\n if (!existsSync(input.targetCwd)) {\n throw new Error(`targetCwd does not exist: ${input.targetCwd}`)\n }\n\n const key = slotKey(input.slot)\n const prior = this.slotLocks.get(key) ?? Promise.resolve()\n const next = prior.then(() => this.acquireLocked(input))\n this.slotLocks.set(key, next.catch(() => undefined))\n return next\n }\n\n private async acquireLocked(input: AcquireRequest): Promise<AcquireResponse> {\n const key = slotKey(input.slot)\n const existing = this.bySlot.get(key)\n\n if (existing && existing.status === 'ready') {\n // M5 (per-devDomain origin): the slot is one shared origin to the\n // browser. Allowing two different working directories to claim the\n // same slot means cookies/SW/storage written by repo A would leak\n // into repo B. Refuse the rebind with a clear error pointing at the\n // user's storyboard.config.json.\n if (existing.cwd !== input.targetCwd) {\n throw new SlotCwdConflictError(input.slot, existing.cwd, input.targetCwd)\n }\n return this.toResponse(existing, this.mintLease(existing))\n }\n if (existing && existing.status !== 'stopped') {\n try { await existing.readyPromise } catch { /* fall through */ }\n const refreshed = this.bySlot.get(key)\n if (refreshed?.status === 'ready') {\n if (refreshed.cwd !== input.targetCwd) {\n throw new SlotCwdConflictError(input.slot, refreshed.cwd, input.targetCwd)\n }\n return this.toResponse(refreshed, this.mintLease(refreshed))\n }\n }\n\n const port = await this.ports.acquire()\n const id = randomUUID()\n const basePath = input.slot.worktree === 'main' ? '/' : `/branch--${input.slot.worktree}/`\n const child = this.spawnViteFn(input.targetCwd, port, basePath, input.slot.devDomain)\n\n const stderrTail: string[] = []\n let resolveReady: () => void = () => undefined\n let rejectReady: (err: Error) => void = () => undefined\n const readyPromise = new Promise<void>((res, rej) => { resolveReady = res; rejectReady = rej })\n\n const internal: DevServerInternal = {\n id,\n child,\n pid: child.pid ?? 0,\n port,\n status: 'spawning',\n slot: input.slot,\n cwd: input.targetCwd,\n spawnedAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n stderrTail,\n readyPromise,\n }\n this.bySlot.set(key, internal)\n this.byId.set(id, internal)\n\n child.stdout?.on('data', (buf: Buffer) => {\n const text = buf.toString()\n if (internal.status === 'spawning' && /ready in|ready /.test(text)) {\n this.transition(internal, 'ready')\n this.proxy.upsert(internal.slot.devDomain, internal.slot.worktree, internal.port)\n .then(() => resolveReady())\n .catch((err: Error) => resolveReady()) // proxy failure ≠ vite failure; surface via state instead\n .finally(() => undefined)\n // Ensure we don't leave readyPromise pending on proxy failure.\n // Use void to silence floating-promise lint where applicable.\n void rejectReady\n }\n })\n child.stderr?.on('data', (buf: Buffer) => {\n stderrTail.push(buf.toString())\n if (stderrTail.length > STDERR_TAIL_MAX) stderrTail.shift()\n })\n child.on('exit', (code) => {\n if (internal.status === 'stopped') return // already torn down\n const wasReady = internal.status === 'ready'\n this.transition(internal, 'stopped')\n this.bySlot.delete(key)\n this.byId.delete(id)\n // Return the port to the pool.\n this.ports.release(internal.port)\n this.proxy.removeWorktree(internal.slot.devDomain, internal.slot.worktree).catch(() => undefined)\n if (!wasReady) {\n rejectReady(new DevServerSpawnError(internal.slot, code, stderrTail.join('')))\n }\n })\n\n const timeout = new Promise<never>((_res, rej) => {\n setTimeout(() => rej(new Error(`Vite ready timeout after ${this.readyTimeoutMs}ms`)), this.readyTimeoutMs).unref()\n })\n try {\n await Promise.race([readyPromise, timeout])\n } catch (err) {\n try { child.kill('SIGTERM') } catch { /* already dead */ }\n throw err\n }\n\n return this.toResponse(internal, this.mintLease(internal))\n }\n\n release(leaseId: string): void {\n const lease = this.leases.get(leaseId)\n if (!lease) throw new LeaseNotFoundError(leaseId)\n this.leases.delete(leaseId)\n const ds = this.byId.get(lease.devServerId)\n if (!ds) return\n\n const stillReferenced = Array.from(this.leases.values()).some(l => l.devServerId === ds.id)\n if (stillReferenced) return\n\n if (ds.status === 'ready') {\n // SIGTERM on next tick to avoid racing the FSM transition.\n setImmediate(() => {\n try { ds.child.kill('SIGTERM') } catch { /* already dead */ }\n })\n }\n }\n\n list(): DevServer[] {\n return Array.from(this.byId.values()).map(toDevServer)\n }\n\n shutdown(): void {\n for (const ds of this.byId.values()) {\n try { ds.child.kill('SIGTERM') } catch { /* already dead */ }\n }\n }\n\n /** Returns null — kept for the /pool/status endpoint backward-compat. */\n poolStatus(): null { return null }\n\n // Sentinel used as Lease.expiresAt — leases never expire; they live as long\n // as the devserver they reference, which lives as long as the owning CLI.\n private static readonly NEVER_EXPIRES = '9999-12-31T23:59:59.999Z'\n\n private mintLease(ds: DevServerInternal): LeaseInternal {\n const id = randomUUID()\n const url = `http://${ds.slot.devDomain}.localhost${ds.slot.worktree === 'main' ? '/' : `/branch--${ds.slot.worktree}/`}`\n const lease: LeaseInternal = {\n id,\n devServerId: ds.id,\n slot: ds.slot,\n url,\n expiresAt: Number.MAX_SAFE_INTEGER,\n }\n this.leases.set(id, lease)\n return lease\n }\n\n private toResponse(ds: DevServerInternal, lease: LeaseInternal): AcquireResponse {\n return AcquireResponse.parse({\n lease: {\n id: lease.id,\n devServerId: lease.devServerId,\n slot: lease.slot,\n url: lease.url,\n expiresAt: DevServerOrchestrator.NEVER_EXPIRES,\n },\n devServer: toDevServer(ds),\n })\n }\n\n private transition(ds: DevServerInternal, to: DevServerStatus): void {\n assertTransition(ds.status, to)\n ds.status = to\n ds.updatedAt = new Date().toISOString()\n }\n}\n\nfunction toDevServer(ds: DevServerInternal): DevServer {\n return DevServer.parse({\n id: ds.id,\n pid: ds.pid,\n port: ds.port,\n status: ds.status,\n slot: ds.slot,\n cwd: ds.cwd,\n spawnedAt: ds.spawnedAt,\n updatedAt: ds.updatedAt,\n })\n}\n\nfunction defaultSpawnVite(cwd: string, port: Port, basePath: string, devDomain: string): ChildProcess {\n const localVite = resolvePath(cwd, 'node_modules', '.bin', 'vite')\n const useLocal = existsSync(localVite)\n // dist/devserver/orchestrator.js → ../vite-plugin/wrapper.js\n const wrapperPath = resolvePath(import.meta.dirname ?? '', '..', 'vite-plugin', 'wrapper.js')\n const args = ['--port', String(port)]\n if (existsSync(wrapperPath)) {\n args.push('--config', wrapperPath)\n }\n const branchMatch = basePath.match(/^\\/branch--([^/]+)\\/$/)\n const branch = branchMatch ? branchMatch[1]! : 'main'\n const env = {\n ...process.env,\n VITE_BASE_PATH: basePath,\n STORYBOARD_RUNTIME_BRANCH: branch,\n STORYBOARD_RUNTIME_DOMAIN: devDomain,\n }\n return useLocal\n ? spawn(localVite, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })\n : spawn('npx', ['vite', ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })\n}\n","import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { LOCKFILE, PIDFILE, RUNTIME_HOME } from './constants.js'\n\n/**\n * Lockfile-based singleton enforcement.\n *\n * On daemon start we attempt an exclusive create of `~/.storyboard/runtime.lock`.\n * Two daemons can never coexist — without this guarantee the runtime's whole\n * raison d'être (single source of truth for proxy + devservers) collapses.\n *\n * If the lockfile exists but its PID is dead, we treat it as stale and reclaim\n * it. This handles crashes / `kill -9` cleanly without operator intervention.\n */\n\nexport class RuntimeAlreadyRunningError extends Error {\n constructor(public readonly pid: number) {\n super(`Storyboard Runtime is already running (pid ${pid})`)\n this.name = 'RuntimeAlreadyRunningError'\n }\n}\n\nfunction ensureRuntimeHome(): void {\n if (!existsSync(RUNTIME_HOME)) mkdirSync(RUNTIME_HOME, { recursive: true })\n}\n\n/** Returns true if the OS reports a process with `pid` is alive. */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Acquire the runtime lock. Throws RuntimeAlreadyRunningError if another\n * live daemon holds it. On success, writes `process.pid` to the pidfile and\n * returns a release function the caller MUST invoke on shutdown.\n */\nexport function acquireRuntimeLock(): () => void {\n ensureRuntimeHome()\n\n if (existsSync(LOCKFILE)) {\n const raw = readFileSync(LOCKFILE, 'utf8').trim()\n const pid = Number(raw)\n if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {\n throw new RuntimeAlreadyRunningError(pid)\n }\n // Stale lock — the previous daemon crashed. Reclaim it.\n try { unlinkSync(LOCKFILE) } catch { /* race: another claimant */ }\n }\n\n // O_EXCL — atomic create-or-fail. Wins over racing daemons.\n let fd: number\n try {\n fd = openSync(LOCKFILE, 'wx')\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') {\n // Another daemon raced us to the lock.\n const raw = readFileSync(LOCKFILE, 'utf8').trim()\n const pid = Number(raw) || 0\n throw new RuntimeAlreadyRunningError(pid)\n }\n throw err\n }\n writeFileSync(fd, String(process.pid))\n closeSync(fd)\n writeFileSync(PIDFILE, String(process.pid))\n\n let released = false\n return function release(): void {\n if (released) return\n released = true\n try { unlinkSync(LOCKFILE) } catch { /* already gone */ }\n try { unlinkSync(PIDFILE) } catch { /* already gone */ }\n }\n}\n\n/** Returns the live daemon's PID, or null if no daemon is running. */\nexport function readLivePid(): number | null {\n if (!existsSync(PIDFILE)) return null\n try {\n const pid = Number(readFileSync(PIDFILE, 'utf8').trim())\n if (!Number.isFinite(pid) || pid <= 0) return null\n return isProcessAlive(pid) ? pid : null\n } catch {\n return null\n }\n}\n\nvoid dirname // tree-shaking hint: keep node:path import even if unused above\n","import { createRuntimeServer } from './http.js'\nimport { acquireRuntimeLock, RuntimeAlreadyRunningError } from './lock.js'\nimport { RUNTIME_HOST, RUNTIME_PORT } from './constants.js'\n\n/**\n * Daemon entrypoint. Run via `node bin/runtime.js` (typically forked &\n * detached by the CLI on first call).\n *\n * Lifecycle:\n * 1. Acquire singleton lock (`~/.storyboard/runtime.lock`). Refuses to\n * start if another live daemon already holds it.\n * 2. Bind the HTTP server on `127.0.0.1:4321`.\n * 3. On SIGINT/SIGTERM, drain in-flight requests, release the lock, exit 0.\n */\nexport async function startDaemon(): Promise<void> {\n let release: () => void\n try {\n release = acquireRuntimeLock()\n } catch (err) {\n if (err instanceof RuntimeAlreadyRunningError) {\n // eslint-disable-next-line no-console\n console.error(`[storyboard-runtime] already running (pid ${err.pid}). Exiting.`)\n process.exit(0)\n }\n throw err\n }\n\n const server = createRuntimeServer()\n\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(RUNTIME_PORT, RUNTIME_HOST, () => resolve())\n })\n\n // eslint-disable-next-line no-console\n console.log(`[storyboard-runtime] listening on http://${RUNTIME_HOST}:${RUNTIME_PORT}`)\n\n let shuttingDown = false\n function shutdown(signal: NodeJS.Signals): void {\n if (shuttingDown) return\n shuttingDown = true\n // eslint-disable-next-line no-console\n console.log(`[storyboard-runtime] received ${signal}, shutting down…`)\n // Kill all spawned Vite children before closing the HTTP server.\n try { (globalThis as { __storyboardOrchestratorShutdown?: () => void }).__storyboardOrchestratorShutdown?.() } catch { /* */ }\n server.close(() => {\n release()\n process.exit(0)\n })\n setTimeout(() => {\n release()\n process.exit(0)\n }, 5000).unref()\n }\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],"mappings":";AAAA,SAAS,SAAS;AAMX,IAAM,qBAAqB;AAa3B,IAAM,YAAY,EACtB,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,qBAAqB,0CAA0C,EACrE,MAAmB;AASf,IAAM,eAAe,EACzB,OAAO,EACP,IAAI,CAAC,EACL,IAAI,EAAE,EACN,MAAM,2BAA2B,gCAAgC,EACjE,MAAsB;AAOlB,IAAM,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,KAAK,EAAE,MAAc;AAUjE,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,WAAW;AAAA,EACX,UAAU;AACZ,CAAC;AAOM,SAAS,QAAQ,MAA6B;AACnD,SAAO,GAAG,KAAK,SAAS,KAAK,KAAK,QAAQ;AAC5C;;;ACnEA,SAAS,KAAAA,UAAS;AAWX,IAAM,kBAAkBC,GAAE,KAAK;AAAA,EACpC;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAIM,IAAM,sBAA2E;AAAA,EACtF,UAAU,CAAC,SAAS,SAAS;AAAA,EAC7B,OAAO,CAAC,SAAS;AAAA,EACjB,SAAS,CAAC;AACZ;AAEO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAChD,YAAY,MAAuB,IAAqB;AACtD,UAAM,iCAAiC,IAAI,WAAM,EAAE,EAAE;AACrD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,iBAAiB,MAAuB,IAA2B;AACjF,MAAI,CAAC,oBAAoB,IAAI,EAAE,SAAS,EAAE,GAAG;AAC3C,UAAM,IAAI,uBAAuB,MAAM,EAAE;AAAA,EAC3C;AACF;AAMO,IAAM,YAAYA,GAAE,OAAO;AAAA,EAChC,IAAIA,GAAE,OAAO,EAAE,KAAK;AAAA,EACpB,KAAKA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAC/B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA;AAAA,EAEN,KAAKA,GAAE,OAAO;AAAA;AAAA,EAEd,WAAWA,GAAE,OAAO,EAAE,SAAS;AAAA;AAAA,EAE/B,WAAWA,GAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAUM,IAAM,QAAQA,GAAE,OAAO;AAAA,EAC5B,IAAIA,GAAE,OAAO,EAAE,KAAK;AAAA,EACpB,aAAaA,GAAE,OAAO,EAAE,KAAK;AAAA,EAC7B,MAAM;AAAA;AAAA,EAEN,KAAKA,GAAE,OAAO,EAAE,IAAI;AAAA;AAAA,EAEpB,WAAWA,GAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAUM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,WAAW;AAAA,EACX,MAAMA,GAAE,OAAO;AAAA;AAAA,EAEf,WAAWA,GAAE,OAAOA,GAAE,OAAO,GAAG,IAAI;AACtC,CAAC;;;ACvFD,SAAS,KAAAC,UAAS;AAWX,IAAM,iBAAiBC,GAAE,OAAO;AAAA,EACrC,MAAM;AAAA;AAAA,EAEN,WAAWA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3B,oBAAoBA,GAAE,QAAQ,EAAE,QAAQ,KAAK;AAC/C,CAAC;AAGM,IAAM,kBAAkBA,GAAE,OAAO;AAAA,EACtC,OAAO;AAAA,EACP,WAAW;AACb,CAAC;AAIM,IAAM,iBAAiBA,GAAE,OAAO;AAAA,EACrC,SAASA,GAAE,OAAO,EAAE,KAAK;AAC3B,CAAC;AAIM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,QAAQA,GAAE,MAAM,UAAU;AAAA,EAC1B,gBAAgBA,GAAE,QAAQ;AAC5B,CAAC;AAIM,IAAM,aAAaA,GAAE,OAAO;AAAA,EACjC,MAAMA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACnC,OAAOA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACpC,UAAUA,GAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AACzC,CAAC;AAIM,IAAM,qBAAqBA,GAAE,OAAO;AAAA,EACzC,WAAWA,GAAE,OAAO;AAAA,EACpB,UAAUA,GAAE,OAAO;AAAA,EACnB,MAAMA,GAAE,OAAO;AACjB,CAAC;AAIM,IAAM,qBAAqBA,GAAE,OAAO;AAAA,EACzC,WAAWA,GAAE,OAAO;AAAA,EACpB,UAAUA,GAAE,OAAO;AACrB,CAAC;AAIM,IAAM,SAASA,GAAE,OAAO;AAAA,EAC7B,IAAIA,GAAE,QAAQ,IAAI;AAAA,EAClB,SAASA,GAAE,OAAO;AAAA,EAClB,eAAeA,GAAE,OAAO,EAAE,YAAY;AAAA,EACtC,MAAM;AACR,CAAC;AAIM,IAAM,eAAeA,GAAE,OAAO;AAAA,EACnC,OAAOA,GAAE,OAAO;AAAA,EAChB,MAAMA,GAAE,KAAK;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAAA,EACD,SAASA,GAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;;;ACzFD,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,eAAe;AACjC,SAAS,YAAY,oBAAoB;AAsBzC,IAAM,eAAe;AASd,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAC7C,YACE,SACgB,QACA,MACA,SAChB;AACA,UAAM,OAAO;AAJG;AACA;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,QACb,SACA,QACA,MACA,MACA,gBACsD;AACtD,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,SAAS,SAAS,SAAY,EAAE,gBAAgB,mBAAmB,IAAI;AAAA,IACvE,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,EACpD;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,GAAG,OAAO,GAAG,IAAI,IAAI,IAAI;AAAA,EAC7C,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,sCAAsC,OAAO,mCAA+B,IAAc,OAAO;AAAA,MACjG;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI;AACJ,MAAI;AAAE,aAAS,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;AAAA,EAAE,QACtC;AAAE,aAAS,EAAE,OAAO,MAAM,MAAM,WAAW;AAAA,EAAE;AAEnD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,MAAM,aAAa,UAAU,MAAM;AACzC,QAAI,IAAI,SAAS;AACf,YAAM,IAAI,oBAAoB,IAAI,KAAK,OAAO,IAAI,QAAQ,IAAI,KAAK,MAAM,IAAI,KAAK,OAAO;AAAA,IAC3F;AACA,UAAM,IAAI,oBAAoB,QAAQ,IAAI,MAAM,IAAI,IAAI,QAAQ,YAAY,MAAM;AAAA,EACpF;AACA,MAAI,mBAAmB,KAAM,QAAO;AACpC,SAAO,eAAe,MAAM,MAAM;AACpC;AAMA,eAAe,YAAY,SAAgC;AACzD,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAGnD,QAAM,UAAU,QAAQ,MAAM,MAAM,MAAM,MAAM,OAAO,uBAAuB;AAC9E,QAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,OAAO,GAAG;AAAA,IAC/C,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK,QAAQ;AAAA,EACf,CAAC;AACD,QAAM,MAAM;AAGZ,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI;AACF,YAAM,IAAI,MAAM,MAAM,GAAG,OAAO,SAAS;AACzC,UAAI,EAAE,GAAI;AAAA,IACZ,QAAQ;AAAA,IAAmB;AAC3B,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAG,CAAC;AAAA,EAC3C;AACA,QAAM,IAAI;AAAA,IACR,qEACmB,OAAO;AAAA,EAC5B;AACF;AAOA,SAAS,oBAA4B;AACnC,MAAI;AACF,UAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,UAAM,aAAa;AAAA,MACjB,QAAQ,MAAM,MAAM,MAAM,MAAM,cAAc;AAAA,MAC9C,QAAQ,MAAM,MAAM,MAAM,cAAc;AAAA,IAC1C;AACA,eAAW,KAAK,YAAY;AAC1B,UAAI,WAAW,CAAC,GAAG;AACjB,cAAM,MAAM,KAAK,MAAM,aAAa,GAAG,MAAM,CAAC;AAC9C,YAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAAA,MAClD;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAe;AACvB,SAAO;AACT;AAEA,IAAM,iBAAiB,kBAAkB;AAMzC,SAAS,qBAA2B;AAClC,MAAI;AACF,UAAM,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,eAAe,aAAa;AAC5E,QAAI,CAAC,WAAW,OAAO,EAAG;AAC1B,UAAM,MAAM,OAAO,aAAa,SAAS,MAAM,EAAE,KAAK,CAAC;AACvD,QAAI,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AACnC,UAAI;AAAE,gBAAQ,KAAK,KAAK,SAAS;AAAA,MAAE,QAAQ;AAAA,MAAqB;AAAA,IAClE;AAAA,EACF,QAAQ;AAAA,EAAe;AACzB;AAEO,IAAM,gBAAN,MAAoB;AAAA,EAChB;AAAA,EACA;AAAA,EAET,YAAY,OAA6B,CAAC,GAAG;AAC3C,SAAK,UAAU,KAAK,WAAW;AAC/B,SAAK,YAAY,KAAK,cAAc;AAAA,EACtC;AAAA,EAEA,MAAM,SAA0B;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAK9E,UACE,KAAK,aACL,mBAAmB,WACnB,OAAO,YAAY,WACnB,OAAO,YAAY,gBACnB;AAMA,YAAI,cAAc;AAClB,YAAI;AACF,gBAAM,UAAU,MAAM,MAAM,GAAG,KAAK,OAAO,iBAAiB;AAC5D,cAAI,QAAQ,IAAI;AACd,kBAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,0BAAc,MAAM,QAAQ,KAAK,UAAU,IAAI,KAAK,WAAW,SAAS;AAAA,UAC1E;AAAA,QACF,QAAQ;AAAA,QAAoC;AAC5C,YAAI,cAAc,GAAG;AAEnB,kBAAQ;AAAA,YACN,+BAA+B,OAAO,OAAO,wBAAwB,cAAc,SAC5E,WAAW;AAAA,UAEpB;AACA,iBAAO;AAAA,QACT;AACA,2BAAmB;AAEnB,cAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAG,CAAC;AACzC,cAAM,YAAY,KAAK,OAAO;AAC9B,eAAO,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAAA,MAClE;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,UAAI,KAAK,aAAa,eAAe,uBAAuB,IAAI,WAAW,GAAG;AAC5E,cAAM,YAAY,KAAK,OAAO;AAC9B,eAAO,QAAQ,KAAK,SAAS,OAAO,WAAW,QAAW,MAAM;AAAA,MAClE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAAiE;AAK7E,QAAI,KAAK,WAAW;AAClB,UAAI;AAAE,cAAM,KAAK,OAAO;AAAA,MAAE,QAAQ;AAAA,MAAoE;AAAA,IACxG;AACA,UAAM,OAAO,eAAe,MAAM,KAAK;AACvC,WAAO,QAAQ,KAAK,SAAS,QAAQ,sBAAsB,MAAM,eAAe;AAAA,EAClF;AAAA,EAEA,MAAM,QAAQ,OAAsD;AAClE,UAAM,OAAO,eAAe,MAAM,KAAK;AACvC,UAAM,QAAQ,KAAK,SAAS,QAAQ,sBAAsB,MAAM,IAAI;AAAA,EACtE;AAAA,EAEA,MAAM,aAAkC;AACtC,WAAO,QAAQ,KAAK,SAAS,OAAO,gBAAgB,QAAW,UAAU;AAAA,EAC3E;AAAA,EAEA,MAAM,YAAY,OAAgE;AAChF,UAAM,OAAO,mBAAmB,MAAM,KAAK;AAC3C,WAAO,QAAQ,KAAK,SAAS,QAAQ,iBAAiB,MAAM,UAAU;AAAA,EACxE;AAAA,EAEA,MAAM,YAAY,OAAgE;AAChF,UAAM,OAAO,mBAAmB,MAAM,KAAK;AAC3C,WAAO,QAAQ,KAAK,SAAS,QAAQ,iBAAiB,MAAM,UAAU;AAAA,EACxE;AAAA,EAEA,MAAM,aAAkC;AACtC,WAAO,QAAQ,KAAK,SAAS,OAAO,gBAAgB,QAAW,UAAU;AAAA,EAC3E;AACF;AAGO,IAAM,UAAU,IAAI,cAAc;;;AC3PzC,OAAO,UAAU;;;ACAjB,SAAS,KAAAC,UAAS;AAaX,IAAM,sBAAsB;AAE5B,IAAM,aAAaC,GACvB,OAAO;AAAA,EACN,OAAOA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAOA,GAAE,MAAMA,GAAE,OAAO,EAAE,MAAMA,GAAE,MAAMA,GAAE,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,CAAC,EAAE,SAAS;AAAA,EAC1F,QAAQA,GAAE,MAAMA,GAAE,QAAQ,CAAC,EAAE,SAAS;AACxC,CAAC,EACA,YAAY;AAQR,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YAAY,SAAiB,OAAiB;AAC5C,UAAM,oCAAoC,OAAO,EAAE;AACnD,SAAK,OAAO;AACZ,QAAI,MAAO,CAAC,KAAqC,QAAQ;AAAA,EAC3D;AACF;AAEO,IAAM,mBAAN,MAAuB;AAAA,EACnB;AAAA,EACA;AAAA,EAET,YAAY,OAA0B,CAAC,GAAG;AACxC,SAAK,UAAU,KAAK,WAAW;AAC/B,SAAK,YAAY,KAAK,aAAa;AAAA,EACrC;AAAA,EAEA,MAAc,MAAM,MAAc,OAAoB,CAAC,GAAsB;AAC3E,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,SAAS;AAC7D,QAAI;AAKF,YAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,UAAI,CAAC,QAAQ,IAAI,QAAQ,EAAG,SAAQ,IAAI,UAAU,KAAK,OAAO;AAC9D,aAAO,MAAM,MAAM,GAAG,KAAK,OAAO,GAAG,IAAI,IAAI,EAAE,GAAG,MAAM,SAAS,QAAQ,WAAW,OAAO,CAAC;AAAA,IAC9F,SAAS,KAAK;AACZ,YAAM,IAAI,sBAAsB,KAAK,SAAS,GAAG;AAAA,IACnD,UAAE;AACA,mBAAa,CAAC;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,MAAM,OAAyB;AAC7B,QAAI;AACF,YAAM,IAAI,MAAM,KAAK,MAAM,UAAU;AACrC,aAAO,EAAE;AAAA,IACX,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,aAAoC;AACxC,UAAM,IAAI,MAAM,KAAK,MAAM,uCAAuC;AAClE,QAAI,EAAE,WAAW,IAAK,QAAO,CAAC;AAC9B,QAAI,CAAC,EAAE,GAAI,OAAM,IAAI,sBAAsB,KAAK,OAAO;AACvD,UAAM,MAAO,MAAM,EAAE,KAAK;AAC1B,WAAOA,GAAE,MAAM,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAAA,EAC5C;AAAA,EAEA,MAAM,UAAU,IAAY,OAAqC;AAC/D,UAAM,IAAI,MAAM,KAAK,MAAM,OAAO,mBAAmB,EAAE,CAAC,IAAI;AAAA,MAC1D,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,KAAK;AAAA,IAC5B,CAAC;AACD,WAAO,EAAE;AAAA,EACX;AAAA,EAEA,MAAM,YAAY,OAAkC;AAClD,UAAM,IAAI,MAAM,KAAK,MAAM,yCAAyC;AAAA,MAClE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,KAAK;AAAA,IAC5B,CAAC;AACD,QAAI,CAAC,EAAE,GAAI,OAAM,IAAI,sBAAsB,KAAK,OAAO;AAAA,EACzD;AAAA,EAEA,MAAM,cAAc,KAA4B;AAC9C,UAAM,IAAI,MAAM,KAAK,MAAM,yCAAyC,GAAG,IAAI;AAAA,MACzE,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,EAAE,MAAM,EAAE,WAAW,IAAK,OAAM,IAAI,sBAAsB,KAAK,OAAO;AAAA,EAC7E;AAAA,EAEA,MAAM,YAA2B;AAC/B,UAAM,SAAS;AAAA,MACb,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE;AAAA,IACvE;AACA,UAAM,IAAI,MAAM,KAAK,MAAM,SAAS;AAAA,MAClC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,MAAM;AAAA,IAC7B,CAAC;AACD,QAAI,CAAC,EAAE,GAAI,OAAM,IAAI,sBAAsB,KAAK,OAAO;AAAA,EACzD;AACF;AAMO,SAAS,cACd,WACA,WACA,UACY;AACZ,QAAM,OAAO,GAAG,SAAS;AACzB,QAAM,YAAuB,CAAC;AAE9B,aAAW,CAAC,UAAU,IAAI,KAAK,OAAO,QAAQ,SAAS,GAAG;AACxD,QAAI,aAAa,OAAQ;AACzB,cAAU,KAAK;AAAA,MACb,OAAO,CAAC,EAAE,MAAM,CAAC,YAAY,QAAQ,IAAI,EAAE,CAAC;AAAA,MAC5C,QAAQ,CAAC,EAAE,SAAS,iBAAiB,WAAW,CAAC,EAAE,MAAM,aAAa,IAAI,GAAG,CAAC,EAAE,CAAC;AAAA,IACnF,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY,UAAU,QAAQ,OAAO,OAAO,SAAS,EAAE,CAAC;AAC7E,MAAI,cAAc;AAChB,cAAU,KAAK;AAAA,MACb,QAAQ,CAAC,EAAE,SAAS,iBAAiB,WAAW,CAAC,EAAE,MAAM,aAAa,YAAY,GAAG,CAAC,EAAE,CAAC;AAAA,IAC3F,CAAC;AAAA,EACH;AAEA,SAAO,WAAW,MAAM;AAAA,IACtB,OAAO;AAAA,IACP,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAAA,IACxB,QAAQ,CAAC,EAAE,SAAS,YAAY,QAAQ,UAAU,CAAC;AAAA,EACrD,CAAC;AACH;;;AC7HO,IAAM,kBAAN,MAAsB;AAAA,EACV;AAAA,EACA,SAAS,oBAAI,IAA2B;AAAA,EACjD,aAA+B,QAAQ,QAAQ;AAAA,EAEvD,YAAY,OAA+B,CAAC,GAAG;AAC7C,SAAK,QAAQ,KAAK,SAAS,IAAI,iBAAiB;AAAA,EAClD;AAAA,EAEQ,SAAY,IAAkC;AACpD,UAAM,OAAO,KAAK,WAAW,KAAK,IAAI,EAAE;AACxC,SAAK,aAAa,KAAK,MAAM,MAAM,MAAS;AAC5C,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBAAmC;AACvC,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AAAA,EAEA,MAAM,cAAmC;AACvC,WAAO,WAAW,MAAM;AAAA,MACtB,QAAQ,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC;AAAA,MACvC,gBAAgB,MAAM,KAAK,MAAM,KAAK;AAAA,IACxC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,WAAsB,UAAwB,MAAiC;AAC1F,WAAO,KAAK,SAAS,YAAY;AAM/B,iBAAW,CAAC,aAAa,UAAU,KAAK,KAAK,QAAQ;AACnD,YAAI,gBAAgB,UAAW;AAC/B,YAAI,UAAU;AACd,cAAM,gBAAsC,CAAC;AAC7C,mBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,WAAW,SAAS,GAAG;AACzD,cAAI,OAAO,CAAC,MAAM,OAAO,IAAI,GAAG;AAAE,sBAAU;AAAM;AAAA,UAAS;AAC3D,wBAAc,CAAC,IAAI;AAAA,QACrB;AACA,YAAI,CAAC,QAAS;AACd,YAAI,OAAO,KAAK,aAAa,EAAE,WAAW,GAAG;AAC3C,eAAK,OAAO,OAAO,WAAW;AAC9B,gBAAM,KAAK,gBAAgB,WAAW;AAAA,QACxC,OAAO;AACL,gBAAM,UAAU,WAAW,MAAM;AAAA,YAC/B,WAAW;AAAA,YACX,MAAM,GAAG,WAAW;AAAA,YACpB,WAAW;AAAA,UACb,CAAC;AACD,eAAK,OAAO,IAAI,aAAa,OAAO;AACpC,gBAAM,KAAK,QAAQ,OAAO;AAAA,QAC5B;AAAA,MACF;AAEA,YAAM,WAAW,KAAK,OAAO,IAAI,SAAS;AAC1C,YAAM,YAAkC,EAAE,GAAI,UAAU,aAAa,CAAC,EAAG;AACzE,gBAAU,QAAQ,IAAI;AACtB,YAAM,OAAO,WAAW,MAAM;AAAA,QAC5B;AAAA,QACA,MAAM,GAAG,SAAS;AAAA,QAClB;AAAA,MACF,CAAC;AACD,WAAK,OAAO,IAAI,WAAW,IAAI;AAC/B,YAAM,KAAK,QAAQ,IAAI;AACvB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,eAAe,WAAsB,UAAuC;AAChF,WAAO,KAAK,SAAS,YAAY;AAC/B,YAAM,WAAW,KAAK,OAAO,IAAI,SAAS;AAC1C,UAAI,CAAC,SAAU;AACf,YAAM,YAAkC,EAAE,GAAG,SAAS,UAAU;AAChE,aAAO,UAAU,QAAQ;AACzB,UAAI,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AACvC,aAAK,OAAO,OAAO,SAAS;AAC5B,cAAM,KAAK,gBAAgB,SAAS;AAAA,MACtC,OAAO;AACL,cAAM,OAAO,WAAW,MAAM;AAAA,UAC5B;AAAA,UACA,MAAM,GAAG,SAAS;AAAA,UAClB;AAAA,QACF,CAAC;AACD,aAAK,OAAO,IAAI,WAAW,IAAI;AAC/B,cAAM,KAAK,QAAQ,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,QAAQ,OAAkC;AACtD,QAAI,CAAE,MAAM,KAAK,MAAM,KAAK,GAAI;AAC9B,YAAM,IAAI,sBAAsB,KAAK,MAAM,OAAO;AAAA,IACpD;AACA,UAAM,aAAa,cAAc,MAAM,WAAW,MAAM,SAAiC;AACzF,UAAM,UAAU,MAAM,KAAK,MAAM,UAAU,MAAM,WAAW,UAAU;AACtE,QAAI,CAAC,SAAS;AACZ,YAAM,KAAK,MAAM,YAAY,UAAU;AAAA,IACzC;AACA,UAAM,KAAK,kBAAkB,MAAM,WAAW,GAAG,MAAM,SAAS,YAAY;AAAA,EAC9E;AAAA,EAEA,MAAc,gBAAgB,WAAqC;AACjE,QAAI,CAAE,MAAM,KAAK,MAAM,KAAK,EAAI,OAAM,IAAI,sBAAsB,KAAK,MAAM,OAAO;AAClF,UAAM,MAAM,MAAM,KAAK,MAAM,WAAW;AACxC,UAAM,UAAoB,CAAC;AAC3B,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,UAAI,IAAI,CAAC,EAAG,KAAK,MAAM,UAAW,SAAQ,KAAK,CAAC;AAAA,IAClD;AACA,eAAW,KAAK,QAAQ,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG;AAC7C,YAAM,KAAK,MAAM,cAAc,CAAC;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,kBAAkB,QAAmB,MAA6B;AAC9E,UAAM,MAAM,MAAM,KAAK,MAAM,WAAW;AACxC,UAAM,QAAkB,CAAC;AACzB,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,YAAM,IAAI,IAAI,CAAC;AACf,UAAI,EAAE,KAAK,MAAM,OAAQ;AACzB,UAAI,EAAE,KAAK,EAAG;AACd,YAAM,QAAQ,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC;AACrC,UAAI,MAAM,SAAS,IAAI,EAAG,OAAM,KAAK,CAAC;AAAA,IACxC;AACA,eAAW,KAAK,MAAM,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG;AAC3C,UAAI;AAAE,cAAM,KAAK,MAAM,cAAc,CAAC;AAAA,MAAE,QAAQ;AAAA,MAAoB;AAAA,IACtE;AAAA,EACF;AAAA,EAEA,MAAM,sBAA6C;AACjD,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,qBAAoC;AACxC,WAAO,KAAK,SAAS,YAAY;AAC/B,UAAI,CAAE,MAAM,KAAK,MAAM,KAAK,EAAI;AAChC,YAAM,MAAM,MAAM,KAAK,MAAM,WAAW;AACxC,YAAM,QAAQ,IAAI,IAAY,KAAK,OAAO,KAAK,CAAC;AAChD,YAAM,QAAkB,CAAC;AACzB,eAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,cAAM,KAAK,IAAI,CAAC,EAAG,KAAK;AACxB,YAAI,MAAM,CAAC,MAAM,IAAI,EAAE,EAAG,OAAM,KAAK,CAAC;AAAA,MACxC;AACA,iBAAW,KAAK,MAAM,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG;AAC3C,YAAI;AAAE,gBAAM,KAAK,MAAM,cAAc,CAAC;AAAA,QAAE,QAAQ;AAAA,QAAoB;AAAA,MACtE;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;AChMA,SAAS,oBAAoB;;;ACA7B,SAAS,eAAe;AACxB,SAAS,MAAM,WAAAC,UAAS,WAAAC,gBAAe;AACvC,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,iBAAAC,sBAAqB;AAUvB,IAAM,eAAe;AACrB,IAAM,eAAe;AAGrB,IAAM,eAAe,KAAK,QAAQ,GAAG,aAAa;AAClD,IAAM,UAAU,KAAK,cAAc,aAAa;AAChD,IAAM,WAAW,KAAK,cAAc,cAAc;AAClD,IAAM,YAAY,KAAK,cAAc,oBAAoB;AAMzD,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAQlC,SAAS,qBAA6B;AACpC,MAAI;AACF,UAAM,OAAOC,SAAQC,eAAc,YAAY,GAAG,CAAC;AAEnD,UAAM,aAAa;AAAA,MACjBC,SAAQ,MAAM,MAAM,MAAM,MAAM,cAAc;AAAA,MAC9CA,SAAQ,MAAM,MAAM,MAAM,cAAc;AAAA,IAC1C;AACA,eAAW,KAAK,YAAY;AAC1B,UAAIC,YAAW,CAAC,GAAG;AACjB,cAAM,MAAM,KAAK,MAAMC,cAAa,GAAG,MAAM,CAAC;AAC9C,YAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAS,QAAO,IAAI;AAAA,MACjE;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAoB;AAC5B,SAAO;AACT;AAEO,IAAM,kBAAkB,mBAAmB;;;ADtC3C,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,cAAc;AACZ,UAAM,0BAA0B,kBAAkB,IAAI,kBAAkB,EAAE;AAC1E,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,WAAN,MAAe;AAAA,EACH,SAAS,oBAAI,IAAY;AAAA,EAE1C,MAAM,UAAyB;AAC7B,aAAS,IAAI,oBAAoB,KAAK,oBAAoB,KAAK;AAC7D,UAAI,KAAK,OAAO,IAAI,CAAC,EAAG;AACxB,UAAI,MAAM,WAAW,CAAC,GAAG;AACvB,aAAK,OAAO,IAAI,CAAC;AACjB,eAAO,KAAK,MAAM,CAAC;AAAA,MACrB;AAAA,IACF;AACA,UAAM,IAAI,mBAAmB;AAAA,EAC/B;AAAA,EAEA,QAAQ,MAA2B;AACjC,SAAK,OAAO,OAAO,OAAO,IAAI,CAAC;AAAA,EACjC;AAAA,EAEA,SAAS,MAA8B;AACrC,WAAO,KAAK,OAAO,IAAI,OAAO,IAAI,CAAC;AAAA,EACrC;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,OAAO;AAAA,EACrB;AACF;AAEA,SAAS,WAAW,MAAgC;AAClD,SAAO,IAAI,QAAQ,CAACC,aAAY;AAC9B,UAAM,QAAQ,aAAa;AAC3B,UAAM,KAAK,SAAS,MAAMA,SAAQ,KAAK,CAAC;AACxC,UAAM,KAAK,aAAa,MAAM;AAC5B,YAAM,MAAM,MAAMA,SAAQ,IAAI,CAAC;AAAA,IACjC,CAAC;AACD,UAAM,OAAO,MAAM,WAAW;AAAA,EAChC,CAAC;AACH;;;AE1DA,SAAS,SAAAC,cAAgC;AACzC,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,WAAW,mBAAmB;AACvC,SAAS,kBAAkB;AA0DpB,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,MAAqB,aAAqB,cAAsB;AAC1E;AAAA,MACE,QAAQ,QAAQ,IAAI,CAAC,wBAAwB,WAAW,2BAA2B,YAAY,wDACzC,KAAK,SAAS;AAAA,IAEtE;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,cAAc;AACZ;AAAA,MACE,gDAAgD,kBAAkB;AAAA,IAEpE;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,IAAY;AACtB,UAAM,YAAY,EAAE,EAAE;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACpC;AAAA,EACT,YAAY,MAAqB,UAAyB,QAAgB;AACxE,UAAM,YAAY,QAAQ,IAAI,CAAC,iBAAiB,YAAY,SAAS,yBAAyB;AAC9F,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAUA,IAAM,2BAA2B;AACjC,IAAM,kBAAkB;AAEjB,IAAM,wBAAN,MAAM,uBAAsB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,SAAS,oBAAI,IAA+B;AAAA,EAC5C,OAAO,oBAAI,IAA+B;AAAA,EAC1C,SAAS,oBAAI,IAA2B;AAAA,EACxC,YAAY,oBAAI,IAA8B;AAAA,EAE/D,YAAY,MAAoC;AAC9C,SAAK,QAAQ,KAAK;AAClB,SAAK,QAAQ,KAAK,SAAS,IAAI,SAAS;AACxC,SAAK,iBAAiB,KAAK,kBAAkB;AAC7C,SAAK,cAAc,KAAK,aAAa;AAAA,EACvC;AAAA,EAEA,MAAM,QAAQ,OAAiD;AAC7D,QAAI,MAAM,KAAK,cAAc,sBAAsB,CAAC,MAAM,oBAAoB;AAC5E,YAAM,IAAI,4BAA4B;AAAA,IACxC;AACA,QAAI,CAACC,YAAW,MAAM,SAAS,GAAG;AAChC,YAAM,IAAI,MAAM,6BAA6B,MAAM,SAAS,EAAE;AAAA,IAChE;AAEA,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,UAAM,QAAQ,KAAK,UAAU,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACzD,UAAM,OAAO,MAAM,KAAK,MAAM,KAAK,cAAc,KAAK,CAAC;AACvD,SAAK,UAAU,IAAI,KAAK,KAAK,MAAM,MAAM,MAAS,CAAC;AACnD,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,cAAc,OAAiD;AAC3E,UAAM,MAAM,QAAQ,MAAM,IAAI;AAC9B,UAAM,WAAW,KAAK,OAAO,IAAI,GAAG;AAEpC,QAAI,YAAY,SAAS,WAAW,SAAS;AAM3C,UAAI,SAAS,QAAQ,MAAM,WAAW;AACpC,cAAM,IAAI,qBAAqB,MAAM,MAAM,SAAS,KAAK,MAAM,SAAS;AAAA,MAC1E;AACA,aAAO,KAAK,WAAW,UAAU,KAAK,UAAU,QAAQ,CAAC;AAAA,IAC3D;AACA,QAAI,YAAY,SAAS,WAAW,WAAW;AAC7C,UAAI;AAAE,cAAM,SAAS;AAAA,MAAa,QAAQ;AAAA,MAAqB;AAC/D,YAAM,YAAY,KAAK,OAAO,IAAI,GAAG;AACrC,UAAI,WAAW,WAAW,SAAS;AACjC,YAAI,UAAU,QAAQ,MAAM,WAAW;AACrC,gBAAM,IAAI,qBAAqB,MAAM,MAAM,UAAU,KAAK,MAAM,SAAS;AAAA,QAC3E;AACA,eAAO,KAAK,WAAW,WAAW,KAAK,UAAU,SAAS,CAAC;AAAA,MAC7D;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,KAAK,MAAM,QAAQ;AACtC,UAAM,KAAK,WAAW;AACtB,UAAM,WAAW,MAAM,KAAK,aAAa,SAAS,MAAM,YAAY,MAAM,KAAK,QAAQ;AACvF,UAAM,QAAQ,KAAK,YAAY,MAAM,WAAW,MAAM,UAAU,MAAM,KAAK,SAAS;AAEpF,UAAM,aAAuB,CAAC;AAC9B,QAAI,eAA2B,MAAM;AACrC,QAAI,cAAoC,MAAM;AAC9C,UAAM,eAAe,IAAI,QAAc,CAAC,KAAK,QAAQ;AAAE,qBAAe;AAAK,oBAAc;AAAA,IAAI,CAAC;AAE9F,UAAM,WAA8B;AAAA,MAClC;AAAA,MACA;AAAA,MACA,KAAK,MAAM,OAAO;AAAA,MAClB;AAAA,MACA,QAAQ;AAAA,MACR,MAAM,MAAM;AAAA,MACZ,KAAK,MAAM;AAAA,MACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AACA,SAAK,OAAO,IAAI,KAAK,QAAQ;AAC7B,SAAK,KAAK,IAAI,IAAI,QAAQ;AAE1B,UAAM,QAAQ,GAAG,QAAQ,CAAC,QAAgB;AACxC,YAAM,OAAO,IAAI,SAAS;AAC1B,UAAI,SAAS,WAAW,cAAc,kBAAkB,KAAK,IAAI,GAAG;AAClE,aAAK,WAAW,UAAU,OAAO;AACjC,aAAK,MAAM,OAAO,SAAS,KAAK,WAAW,SAAS,KAAK,UAAU,SAAS,IAAI,EAC7E,KAAK,MAAM,aAAa,CAAC,EACzB,MAAM,CAAC,QAAe,aAAa,CAAC,EACpC,QAAQ,MAAM,MAAS;AAG1B,aAAK;AAAA,MACP;AAAA,IACF,CAAC;AACD,UAAM,QAAQ,GAAG,QAAQ,CAAC,QAAgB;AACxC,iBAAW,KAAK,IAAI,SAAS,CAAC;AAC9B,UAAI,WAAW,SAAS,gBAAiB,YAAW,MAAM;AAAA,IAC5D,CAAC;AACD,UAAM,GAAG,QAAQ,CAAC,SAAS;AACzB,UAAI,SAAS,WAAW,UAAW;AACnC,YAAM,WAAW,SAAS,WAAW;AACrC,WAAK,WAAW,UAAU,SAAS;AACnC,WAAK,OAAO,OAAO,GAAG;AACtB,WAAK,KAAK,OAAO,EAAE;AAEnB,WAAK,MAAM,QAAQ,SAAS,IAAI;AAChC,WAAK,MAAM,eAAe,SAAS,KAAK,WAAW,SAAS,KAAK,QAAQ,EAAE,MAAM,MAAM,MAAS;AAChG,UAAI,CAAC,UAAU;AACb,oBAAY,IAAI,oBAAoB,SAAS,MAAM,MAAM,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,MAC/E;AAAA,IACF,CAAC;AAED,UAAM,UAAU,IAAI,QAAe,CAAC,MAAM,QAAQ;AAChD,iBAAW,MAAM,IAAI,IAAI,MAAM,4BAA4B,KAAK,cAAc,IAAI,CAAC,GAAG,KAAK,cAAc,EAAE,MAAM;AAAA,IACnH,CAAC;AACD,QAAI;AACF,YAAM,QAAQ,KAAK,CAAC,cAAc,OAAO,CAAC;AAAA,IAC5C,SAAS,KAAK;AACZ,UAAI;AAAE,cAAM,KAAK,SAAS;AAAA,MAAE,QAAQ;AAAA,MAAqB;AACzD,YAAM;AAAA,IACR;AAEA,WAAO,KAAK,WAAW,UAAU,KAAK,UAAU,QAAQ,CAAC;AAAA,EAC3D;AAAA,EAEA,QAAQ,SAAuB;AAC7B,UAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;AACrC,QAAI,CAAC,MAAO,OAAM,IAAI,mBAAmB,OAAO;AAChD,SAAK,OAAO,OAAO,OAAO;AAC1B,UAAM,KAAK,KAAK,KAAK,IAAI,MAAM,WAAW;AAC1C,QAAI,CAAC,GAAI;AAET,UAAM,kBAAkB,MAAM,KAAK,KAAK,OAAO,OAAO,CAAC,EAAE,KAAK,OAAK,EAAE,gBAAgB,GAAG,EAAE;AAC1F,QAAI,gBAAiB;AAErB,QAAI,GAAG,WAAW,SAAS;AAEzB,mBAAa,MAAM;AACjB,YAAI;AAAE,aAAG,MAAM,KAAK,SAAS;AAAA,QAAE,QAAQ;AAAA,QAAqB;AAAA,MAC9D,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,OAAoB;AAClB,WAAO,MAAM,KAAK,KAAK,KAAK,OAAO,CAAC,EAAE,IAAI,WAAW;AAAA,EACvD;AAAA,EAEA,WAAiB;AACf,eAAW,MAAM,KAAK,KAAK,OAAO,GAAG;AACnC,UAAI;AAAE,WAAG,MAAM,KAAK,SAAS;AAAA,MAAE,QAAQ;AAAA,MAAqB;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AAAE,WAAO;AAAA,EAAK;AAAA;AAAA;AAAA,EAIjC,OAAwB,gBAAgB;AAAA,EAEhC,UAAU,IAAsC;AACtD,UAAM,KAAK,WAAW;AACtB,UAAM,MAAM,UAAU,GAAG,KAAK,SAAS,aAAa,GAAG,KAAK,aAAa,SAAS,MAAM,YAAY,GAAG,KAAK,QAAQ,GAAG;AACvH,UAAM,QAAuB;AAAA,MAC3B;AAAA,MACA,aAAa,GAAG;AAAA,MAChB,MAAM,GAAG;AAAA,MACT;AAAA,MACA,WAAW,OAAO;AAAA,IACpB;AACA,SAAK,OAAO,IAAI,IAAI,KAAK;AACzB,WAAO;AAAA,EACT;AAAA,EAEQ,WAAW,IAAuB,OAAuC;AAC/E,WAAO,gBAAgB,MAAM;AAAA,MAC3B,OAAO;AAAA,QACL,IAAI,MAAM;AAAA,QACV,aAAa,MAAM;AAAA,QACnB,MAAM,MAAM;AAAA,QACZ,KAAK,MAAM;AAAA,QACX,WAAW,uBAAsB;AAAA,MACnC;AAAA,MACA,WAAW,YAAY,EAAE;AAAA,IAC3B,CAAC;AAAA,EACH;AAAA,EAEQ,WAAW,IAAuB,IAA2B;AACnE,qBAAiB,GAAG,QAAQ,EAAE;AAC9B,OAAG,SAAS;AACZ,OAAG,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EACxC;AACF;AAEA,SAAS,YAAY,IAAkC;AACrD,SAAO,UAAU,MAAM;AAAA,IACrB,IAAI,GAAG;AAAA,IACP,KAAK,GAAG;AAAA,IACR,MAAM,GAAG;AAAA,IACT,QAAQ,GAAG;AAAA,IACX,MAAM,GAAG;AAAA,IACT,KAAK,GAAG;AAAA,IACR,WAAW,GAAG;AAAA,IACd,WAAW,GAAG;AAAA,EAChB,CAAC;AACH;AAEA,SAAS,iBAAiB,KAAa,MAAY,UAAkB,WAAiC;AACpG,QAAM,YAAY,YAAY,KAAK,gBAAgB,QAAQ,MAAM;AACjE,QAAM,WAAWA,YAAW,SAAS;AAErC,QAAM,cAAc,YAAY,YAAY,WAAW,IAAI,MAAM,eAAe,YAAY;AAC5F,QAAM,OAAO,CAAC,UAAU,OAAO,IAAI,CAAC;AACpC,MAAIA,YAAW,WAAW,GAAG;AAC3B,SAAK,KAAK,YAAY,WAAW;AAAA,EACnC;AACA,QAAM,cAAc,SAAS,MAAM,uBAAuB;AAC1D,QAAM,SAAS,cAAc,YAAY,CAAC,IAAK;AAC/C,QAAM,MAAM;AAAA,IACV,GAAG,QAAQ;AAAA,IACX,gBAAgB;AAAA,IAChB,2BAA2B;AAAA,IAC3B,2BAA2B;AAAA,EAC7B;AACA,SAAO,WACHC,OAAM,WAAW,MAAM,EAAE,KAAK,KAAK,OAAO,CAAC,UAAU,QAAQ,MAAM,EAAE,CAAC,IACtEA,OAAM,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,EAAE,KAAK,KAAK,OAAO,CAAC,UAAU,QAAQ,MAAM,EAAE,CAAC;AACrF;;;ALhTA,IAAM,YAAY,KAAK,IAAI;AAM3B,IAAI,kBAAkB,IAAI,gBAAgB;AAC1C,IAAI,SAAS,IAAI,SAAS;AAC1B,IAAI,eAAsC,IAAI,sBAAsB;AAAA,EAClE,OAAO;AAAA,EACP,OAAO;AACT,CAAC;AAED,SAAS,SACP,KACA,QACA,MACA,QACM;AACN,MAAI,UAAU,QAAQ,IAAI,aAAa,cAAc;AACnD,WAAO,MAAM,IAAI;AAAA,EACnB;AACA,MAAI,UAAU,QAAQ,EAAE,gBAAgB,mBAAmB,CAAC;AAC5D,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,UACP,KACA,QACA,MACA,SACA,SACM;AACN,QAAM,OAAO,aAAa,MAAM,EAAE,OAAO,SAAS,MAAM,QAAQ,CAAC;AACjE,WAAS,KAAK,QAAQ,MAAM,YAAY;AAC1C;AAEA,eAAe,aAAa,KAA6C;AACvE,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,IAAK,QAAO,KAAK,KAAe;AAC1D,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,MAAI;AACF,WAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC;AAAA,EAC1D,QAAQ;AACN,UAAM,IAAI,MAAM,qBAAqB;AAAA,EACvC;AACF;AAEA,eAAe,UACb,KACA,KACA,QAC6B;AAC7B,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,aAAa,GAAG;AAAA,EAC9B,SAAS,KAAK;AACZ,cAAU,KAAK,KAAK,eAAgB,IAAc,OAAO;AACzD,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,UAAU,GAAG;AACnC,MAAI,CAAC,OAAO,SAAS;AACnB,cAAU,KAAK,KAAK,eAAe,qBAAqB,OAAO,MAAM,QAAQ,CAAC;AAC9E,WAAO;AAAA,EACT;AACA,SAAO,OAAO;AAChB;AAIA,IAAM,SAAS,oBAAI,IAAmB;AAEtC,OAAO,IAAI,eAAe,CAAC,MAAM,QAAQ;AACvC,QAAM,OAAe,OAAO,MAAM;AAAA,IAChC,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,gBAAgB,KAAK,IAAI,IAAI,aAAa;AAAA,IAC1C,MAAM;AAAA,EACR,CAAC;AACD,WAAS,KAAK,KAAK,MAAM,MAAM;AACjC,CAAC;AAED,OAAO,IAAI,2BAA2B,OAAO,KAAK,QAAQ;AACxD,QAAM,OAAO,MAAM,UAAU,KAAK,KAAK,cAAc;AACrD,MAAI,CAAC,KAAM;AACX,MAAI;AACF,UAAM,SAAS,MAAM,aAAa,QAAQ,IAAI;AAC9C,aAAS,KAAK,KAAK,QAAQ,eAAe;AAAA,EAC5C,SAAS,KAAK;AACZ,QAAI,eAAe,6BAA6B;AAC9C,gBAAU,KAAK,KAAK,4BAA4B,IAAI,OAAO;AAC3D;AAAA,IACF;AACA,QAAI,eAAe,sBAAsB;AACvC,gBAAU,KAAK,KAAK,YAAY,IAAI,OAAO;AAC3C;AAAA,IACF;AACA,QAAI,eAAe,qBAAqB;AACtC,gBAAU,KAAK,KAAK,YAAY,IAAI,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnE;AAAA,IACF;AACA,QAAI,eAAe,uBAAuB;AACxC,gBAAU,KAAK,KAAK,qBAAqB,IAAI,OAAO;AACpD;AAAA,IACF;AACA,cAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,EACxD;AACF,CAAC;AAED,OAAO,IAAI,2BAA2B,OAAO,KAAK,QAAQ;AACxD,QAAM,OAAO,MAAM,UAAU,KAAK,KAAK,cAAc;AACrD,MAAI,CAAC,KAAM;AACX,MAAI;AACF,iBAAa,QAAQ,KAAK,OAAO;AACjC,aAAS,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACjC,SAAS,KAAK;AACZ,QAAI,eAAe,oBAAoB;AACrC,gBAAU,KAAK,KAAK,aAAa,IAAI,OAAO;AAC5C;AAAA,IACF;AACA,cAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,EACxD;AACF,CAAC;AAED,OAAO,IAAI,uBAAuB,CAAC,MAAM,QAAQ;AAC/C,WAAS,KAAK,KAAK,EAAE,YAAY,aAAa,KAAK,EAAE,CAAC;AACxD,CAAC;AAED,OAAO,IAAI,oBAAoB,OAAO,MAAM,QAAQ;AAClD,MAAI;AACF,UAAM,OAAO,MAAM,gBAAgB,YAAY;AAC/C,aAAS,KAAK,KAAK,MAAM,UAAU;AAAA,EACrC,SAAS,KAAK;AACZ,cAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,EACxD;AACF,CAAC;AAED,OAAO,IAAI,sBAAsB,OAAO,KAAK,QAAQ;AACnD,QAAM,OAAO,MAAM,UAAU,KAAK,KAAK,kBAAkB;AACzD,MAAI,CAAC,KAAM;AAIX,QAAM,MAAM,UAAU,UAAU,KAAK,SAAS;AAC9C,QAAM,KAAK,aAAa,UAAU,KAAK,QAAQ;AAC/C,QAAM,OAAO,KAAK,UAAU,KAAK,IAAI;AACrC,MAAI,CAAC,IAAI,WAAW,CAAC,GAAG,WAAW,CAAC,KAAK,SAAS;AAChD,cAAU,KAAK,KAAK,eAAe,qBAAqB;AAAA,MACtD,WAAW,IAAI,UAAU,OAAO,IAAI,MAAM,QAAQ;AAAA,MAClD,UAAU,GAAG,UAAU,OAAO,GAAG,MAAM,QAAQ;AAAA,MAC/C,MAAM,KAAK,UAAU,OAAO,KAAK,MAAM,QAAQ;AAAA,IACjD,CAAC;AACD;AAAA,EACF;AACA,MAAI,IAAI,SAAU,cAA6C;AAC7D;AAAA,MAAU;AAAA,MAAK;AAAA,MAAK;AAAA,MAClB;AAAA,IACmD;AACrD;AAAA,EACF;AACA,MAAI;AACF,UAAM,QAAQ,MAAM,gBAAgB,OAAO,IAAI,MAAM,GAAG,MAAM,KAAK,IAAI;AACvE,aAAS,KAAK,KAAK,WAAW,MAAM,EAAE,QAAQ,CAAC,KAAK,GAAG,gBAAgB,KAAK,CAAC,GAAG,UAAU;AAAA,EAC5F,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AACxC,gBAAU,KAAK,KAAK,qBAAqB,IAAI,OAAO;AACpD;AAAA,IACF;AACA,cAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,EACxD;AACF,CAAC;AAED,OAAO,IAAI,sBAAsB,OAAO,KAAK,QAAQ;AACnD,QAAM,OAAO,MAAM,UAAU,KAAK,KAAK,kBAAkB;AACzD,MAAI,CAAC,KAAM;AACX,QAAM,MAAM,UAAU,UAAU,KAAK,SAAS;AAC9C,QAAM,KAAK,aAAa,UAAU,KAAK,QAAQ;AAC/C,MAAI,CAAC,IAAI,WAAW,CAAC,GAAG,SAAS;AAC/B,cAAU,KAAK,KAAK,eAAe,mBAAmB;AACtD;AAAA,EACF;AACA,MAAI;AACF,UAAM,gBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI;AACtD,aAAS,KAAK,KAAK,WAAW,MAAM,EAAE,QAAQ,CAAC,GAAG,gBAAgB,KAAK,CAAC,GAAG,UAAU;AAAA,EACvF,SAAS,KAAK;AACZ,QAAI,eAAe,uBAAuB;AACxC,gBAAU,KAAK,KAAK,qBAAqB,IAAI,OAAO;AACpD;AAAA,IACF;AACA,cAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,EACxD;AACF,CAAC;AAED,OAAO,IAAI,oBAAoB,CAAC,MAAM,QAAQ;AAG5C,QAAM,OAAO,aAAa,WAAW;AACrC,QAAM,OAAO,WAAW,MAAM,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,EAAE,CAAC;AACxE,WAAS,KAAK,KAAK,MAAM,UAAU;AACrC,CAAC;AAEM,SAAS,oBAAoB,OAGhC,CAAC,GAAgB;AACnB,MAAI,KAAK,gBAAiB,mBAAkB,KAAK;AACjD,MAAI,KAAK,aAAc,gBAAe,KAAK;AAI3C,OAAK,gBAAgB,mBAAmB,EAAE,MAAM,MAAM,MAAS;AAE9D,EAAC,WAAiE,mCAAmC,MAAM,aAAa,SAAS;AAClI,QAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,YAAY,IAAI,YAAY,EAAE;AAC5E,YAAM,MAAM,GAAG,IAAI,UAAU,KAAK,IAAI,IAAI,QAAQ;AAClD,YAAM,UAAU,OAAO,IAAI,GAAG;AAC9B,UAAI,CAAC,SAAS;AACZ,kBAAU,KAAK,KAAK,aAAa,gBAAgB,GAAG,EAAE;AACtD;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,GAAG;AAAA,IACxB,SAAS,KAAK;AACZ,gBAAU,KAAK,KAAK,YAAa,IAAc,OAAO;AAAA,IACxD;AAAA,EACF,CAAC;AACD,SAAO;AACT;;;AMxQA,SAAS,cAAAC,aAAY,WAAW,gBAAAC,eAAc,YAAY,eAAe,UAAU,iBAAiB;AACpG,SAAS,WAAAC,gBAAe;AAcjB,IAAM,6BAAN,cAAyC,MAAM;AAAA,EACpD,YAA4B,KAAa;AACvC,UAAM,8CAA8C,GAAG,GAAG;AADhC;AAE1B,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,oBAA0B;AACjC,MAAI,CAACC,YAAW,YAAY,EAAG,WAAU,cAAc,EAAE,WAAW,KAAK,CAAC;AAC5E;AAGA,SAAS,eAAe,KAAsB;AAC5C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,qBAAiC;AAC/C,oBAAkB;AAElB,MAAIA,YAAW,QAAQ,GAAG;AACxB,UAAM,MAAMC,cAAa,UAAU,MAAM,EAAE,KAAK;AAChD,UAAM,MAAM,OAAO,GAAG;AACtB,QAAI,OAAO,SAAS,GAAG,KAAK,MAAM,KAAK,eAAe,GAAG,GAAG;AAC1D,YAAM,IAAI,2BAA2B,GAAG;AAAA,IAC1C;AAEA,QAAI;AAAE,iBAAW,QAAQ;AAAA,IAAE,QAAQ;AAAA,IAA+B;AAAA,EACpE;AAGA,MAAI;AACJ,MAAI;AACF,SAAK,SAAS,UAAU,IAAI;AAAA,EAC9B,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AAEpD,YAAM,MAAMA,cAAa,UAAU,MAAM,EAAE,KAAK;AAChD,YAAM,MAAM,OAAO,GAAG,KAAK;AAC3B,YAAM,IAAI,2BAA2B,GAAG;AAAA,IAC1C;AACA,UAAM;AAAA,EACR;AACA,gBAAc,IAAI,OAAO,QAAQ,GAAG,CAAC;AACrC,YAAU,EAAE;AACZ,gBAAc,SAAS,OAAO,QAAQ,GAAG,CAAC;AAE1C,MAAI,WAAW;AACf,SAAO,SAAS,UAAgB;AAC9B,QAAI,SAAU;AACd,eAAW;AACX,QAAI;AAAE,iBAAW,QAAQ;AAAA,IAAE,QAAQ;AAAA,IAAqB;AACxD,QAAI;AAAE,iBAAW,OAAO;AAAA,IAAE,QAAQ;AAAA,IAAqB;AAAA,EACzD;AACF;;;AChEA,eAAsB,cAA6B;AACjD,MAAI;AACJ,MAAI;AACF,cAAU,mBAAmB;AAAA,EAC/B,SAAS,KAAK;AACZ,QAAI,eAAe,4BAA4B;AAE7C,cAAQ,MAAM,6CAA6C,IAAI,GAAG,aAAa;AAC/E,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAM;AAAA,EACR;AAEA,QAAM,SAAS,oBAAoB;AAEnC,QAAM,IAAI,QAAc,CAACC,UAAS,WAAW;AAC3C,WAAO,KAAK,SAAS,MAAM;AAC3B,WAAO,OAAO,cAAc,cAAc,MAAMA,SAAQ,CAAC;AAAA,EAC3D,CAAC;AAGD,UAAQ,IAAI,4CAA4C,YAAY,IAAI,YAAY,EAAE;AAEtF,MAAI,eAAe;AACnB,WAAS,SAAS,QAA8B;AAC9C,QAAI,aAAc;AAClB,mBAAe;AAEf,YAAQ,IAAI,iCAAiC,MAAM,uBAAkB;AAErE,QAAI;AAAE,MAAC,WAAiE,mCAAmC;AAAA,IAAE,QAAQ;AAAA,IAAQ;AAC7H,WAAO,MAAM,MAAM;AACjB,cAAQ;AACR,cAAQ,KAAK,CAAC;AAAA,IAChB,CAAC;AACD,eAAW,MAAM;AACf,cAAQ;AACR,cAAQ,KAAK,CAAC;AAAA,IAChB,GAAG,GAAI,EAAE,MAAM;AAAA,EACjB;AACA,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAChC;","names":["z","z","z","z","z","z","dirname","resolve","existsSync","readFileSync","fileURLToPath","dirname","fileURLToPath","resolve","existsSync","readFileSync","resolve","spawn","existsSync","existsSync","spawn","existsSync","readFileSync","dirname","existsSync","readFileSync","resolve"]}
|
package/package.json
CHANGED
|
@@ -164,6 +164,8 @@ export default function ArtifactForm({
|
|
|
164
164
|
setErrors({})
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
168
|
+
|
|
167
169
|
const visibleFields = useMemo(() => {
|
|
168
170
|
if (!schema) return []
|
|
169
171
|
if (compact) return schema.fields.filter(f => f.required)
|
|
@@ -182,6 +184,18 @@ export default function ArtifactForm({
|
|
|
182
184
|
return fields
|
|
183
185
|
}, [schema, compact, values])
|
|
184
186
|
|
|
187
|
+
// Split into basic/advanced. A field with no `tier` defaults to 'basic'
|
|
188
|
+
// (matches the old behaviour for any schema not yet annotated).
|
|
189
|
+
const { basicFields, advancedFields } = useMemo(() => {
|
|
190
|
+
const basic = []
|
|
191
|
+
const advanced = []
|
|
192
|
+
for (const f of visibleFields) {
|
|
193
|
+
if (f.tier === 'advanced') advanced.push(f)
|
|
194
|
+
else basic.push(f)
|
|
195
|
+
}
|
|
196
|
+
return { basicFields: basic, advancedFields: advanced }
|
|
197
|
+
}, [visibleFields])
|
|
198
|
+
|
|
185
199
|
if (!schema) {
|
|
186
200
|
return (
|
|
187
201
|
<Flash variant="warning">
|
|
@@ -234,7 +248,7 @@ export default function ArtifactForm({
|
|
|
234
248
|
)}
|
|
235
249
|
|
|
236
250
|
<div className={styles.fields}>
|
|
237
|
-
{
|
|
251
|
+
{basicFields.map(field => {
|
|
238
252
|
const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
|
|
239
253
|
return (
|
|
240
254
|
<FormControl key={field.name} required={field.required}>
|
|
@@ -257,6 +271,43 @@ export default function ArtifactForm({
|
|
|
257
271
|
</FormControl>
|
|
258
272
|
)
|
|
259
273
|
})}
|
|
274
|
+
|
|
275
|
+
{advancedFields.length > 0 && (
|
|
276
|
+
<>
|
|
277
|
+
<Button
|
|
278
|
+
type="button"
|
|
279
|
+
variant="invisible"
|
|
280
|
+
size="small"
|
|
281
|
+
onClick={() => setShowAdvanced(s => !s)}
|
|
282
|
+
className={styles.advancedToggle}
|
|
283
|
+
>
|
|
284
|
+
{showAdvanced ? '− Hide advanced fields' : '+ Advanced fields'}
|
|
285
|
+
</Button>
|
|
286
|
+
{showAdvanced && advancedFields.map(field => {
|
|
287
|
+
const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
|
|
288
|
+
return (
|
|
289
|
+
<FormControl key={field.name} required={field.required}>
|
|
290
|
+
<FormControl.Label>{field.label}</FormControl.Label>
|
|
291
|
+
<FieldRenderer
|
|
292
|
+
field={field}
|
|
293
|
+
value={values[field.name]}
|
|
294
|
+
error={errors[field.name]}
|
|
295
|
+
onChange={val => handleChange(field.name, val)}
|
|
296
|
+
options={options}
|
|
297
|
+
/>
|
|
298
|
+
{errors[field.name] && (
|
|
299
|
+
<FormControl.Validation variant="error">
|
|
300
|
+
{errors[field.name]}
|
|
301
|
+
</FormControl.Validation>
|
|
302
|
+
)}
|
|
303
|
+
{field.patternHint && !errors[field.name] && (
|
|
304
|
+
<FormControl.Caption>{field.patternHint}</FormControl.Caption>
|
|
305
|
+
)}
|
|
306
|
+
</FormControl>
|
|
307
|
+
)
|
|
308
|
+
})}
|
|
309
|
+
</>
|
|
310
|
+
)}
|
|
260
311
|
</div>
|
|
261
312
|
|
|
262
313
|
{errors._form && (
|
|
@@ -14,17 +14,17 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
14
14
|
icon: '📐',
|
|
15
15
|
description: 'Interactive prototype with pages and flows. Add a URL to make it external.',
|
|
16
16
|
fields: [
|
|
17
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'my-app', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64 },
|
|
18
|
-
{ name: 'title', label: 'Title', type: 'text', placeholder: 'My App' },
|
|
19
|
-
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'What this prototype demonstrates…' },
|
|
20
|
-
{ name: '
|
|
21
|
-
{ name: '
|
|
22
|
-
{ name: '
|
|
23
|
-
{ name: '
|
|
24
|
-
{ name: '
|
|
25
|
-
{ name: '
|
|
26
|
-
{ name: '
|
|
27
|
-
{ name: 'flow', label: 'Create default flow', type: 'checkbox', checkboxLabel: 'Generate a default.flow.json' },
|
|
17
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'my-app', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64, tier: 'basic' },
|
|
18
|
+
{ name: 'title', label: 'Title', type: 'text', placeholder: 'My App', tier: 'basic' },
|
|
19
|
+
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'What this prototype demonstrates…', tier: 'basic' },
|
|
20
|
+
{ name: 'url', label: 'External URL', type: 'url', placeholder: 'https://figma.com/… (makes it external)', tier: 'basic' },
|
|
21
|
+
{ name: 'partial', label: 'Template / Recipe', type: 'select', placeholder: 'Blank prototype', dynamic: 'partials', tier: 'basic' },
|
|
22
|
+
{ name: 'author', label: 'Author', type: 'text', placeholder: 'dfosco (or comma-separated)', tier: 'advanced' },
|
|
23
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'main (optional .folder grouping)', tier: 'advanced' },
|
|
24
|
+
{ name: 'icon', label: 'Icon', type: 'text', placeholder: 'rocket', tier: 'advanced' },
|
|
25
|
+
{ name: 'tags', label: 'Tags', type: 'text', placeholder: 'design, exploration (comma-separated)', tier: 'advanced' },
|
|
26
|
+
{ name: 'team', label: 'Team', type: 'text', placeholder: 'design-systems', tier: 'advanced' },
|
|
27
|
+
{ name: 'flow', label: 'Create default flow', type: 'checkbox', checkboxLabel: 'Generate a default.flow.json', tier: 'advanced' },
|
|
28
28
|
],
|
|
29
29
|
operations: ['create', 'edit', 'delete', 'duplicate'],
|
|
30
30
|
mutuallyExclusive: [['url', 'flow'], ['url', 'partial']],
|
|
@@ -35,10 +35,10 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
35
35
|
icon: '🎨',
|
|
36
36
|
description: 'Freeform spatial canvas for exploration and planning',
|
|
37
37
|
fields: [
|
|
38
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'design-system', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64 },
|
|
39
|
-
{ name: 'title', label: 'Title', type: 'text', placeholder: 'Design Exploration' },
|
|
40
|
-
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'Purpose of this canvas…' },
|
|
41
|
-
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'storyboarding (optional grouping)' },
|
|
38
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'design-system', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64, tier: 'basic' },
|
|
39
|
+
{ name: 'title', label: 'Title', type: 'text', placeholder: 'Design Exploration', tier: 'basic' },
|
|
40
|
+
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'Purpose of this canvas…', tier: 'basic' },
|
|
41
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'storyboarding (optional grouping)', tier: 'advanced' },
|
|
42
42
|
],
|
|
43
43
|
operations: ['create', 'edit', 'delete', 'duplicate'],
|
|
44
44
|
},
|
|
@@ -48,8 +48,8 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
48
48
|
icon: '🧩',
|
|
49
49
|
description: 'Reusable UI component with story file',
|
|
50
50
|
fields: [
|
|
51
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'LoginForm', pattern: '^[A-Z][A-Za-z0-9]+$', patternHint: 'PascalCase (e.g. LoginForm)', maxLength: 64 },
|
|
52
|
-
{ name: 'directory', label: 'Directory', type: 'text', placeholder: 'src/components (default)' },
|
|
51
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'LoginForm', pattern: '^[A-Z][A-Za-z0-9]+$', patternHint: 'PascalCase (e.g. LoginForm)', maxLength: 64, tier: 'basic' },
|
|
52
|
+
{ name: 'directory', label: 'Directory', type: 'text', placeholder: 'src/components (default)', tier: 'advanced' },
|
|
53
53
|
],
|
|
54
54
|
operations: ['create', 'delete'],
|
|
55
55
|
},
|
|
@@ -59,14 +59,14 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
59
59
|
icon: '🔀',
|
|
60
60
|
description: 'Page data context — composes objects via $ref and $global',
|
|
61
61
|
fields: [
|
|
62
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'default', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64 },
|
|
63
|
-
{ name: 'prototype', label: 'Prototype', type: 'select', required: true, options: [], dynamic: 'prototypes' },
|
|
64
|
-
{ name: 'title', label: 'Title', type: 'text', placeholder: 'Settings Flow' },
|
|
65
|
-
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'Data context for…' },
|
|
66
|
-
{ name: 'globals', label: '$global objects', type: 'text', placeholder: 'navigation, sidebar (comma-separated)' },
|
|
67
|
-
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional subfolder' },
|
|
68
|
-
{ name: 'copyFrom', label: 'Copy from', type: 'text', placeholder: 'Existing flow name to duplicate' },
|
|
69
|
-
{ name: 'startingPage', label: 'Starting page', type: 'text', placeholder: 'Route to open with this flow' },
|
|
62
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'default', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64, tier: 'basic' },
|
|
63
|
+
{ name: 'prototype', label: 'Prototype', type: 'select', required: true, options: [], dynamic: 'prototypes', tier: 'basic' },
|
|
64
|
+
{ name: 'title', label: 'Title', type: 'text', placeholder: 'Settings Flow', tier: 'basic' },
|
|
65
|
+
{ name: 'description', label: 'Description', type: 'textarea', placeholder: 'Data context for…', tier: 'basic' },
|
|
66
|
+
{ name: 'globals', label: '$global objects', type: 'text', placeholder: 'navigation, sidebar (comma-separated)', tier: 'advanced' },
|
|
67
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional subfolder', tier: 'advanced' },
|
|
68
|
+
{ name: 'copyFrom', label: 'Copy from', type: 'text', placeholder: 'Existing flow name to duplicate', tier: 'advanced' },
|
|
69
|
+
{ name: 'startingPage', label: 'Starting page', type: 'text', placeholder: 'Route to open with this flow', tier: 'advanced' },
|
|
70
70
|
],
|
|
71
71
|
operations: ['create', 'edit', 'delete', 'duplicate'],
|
|
72
72
|
},
|
|
@@ -76,9 +76,9 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
76
76
|
icon: '📦',
|
|
77
77
|
description: 'Reusable data fragment — freeform JSON',
|
|
78
78
|
fields: [
|
|
79
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'jane-doe', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64 },
|
|
80
|
-
{ name: '
|
|
81
|
-
{ name: '
|
|
79
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'jane-doe', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64, tier: 'basic' },
|
|
80
|
+
{ name: 'body', label: 'JSON Body', type: 'code', placeholder: '{\n "name": "Jane Doe",\n "role": "admin"\n}', language: 'json', tier: 'basic' },
|
|
81
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional folder (or inside prototype for scoping)', tier: 'advanced' },
|
|
82
82
|
],
|
|
83
83
|
operations: ['create', 'edit', 'delete', 'duplicate'],
|
|
84
84
|
},
|
|
@@ -88,9 +88,9 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
88
88
|
icon: '📋',
|
|
89
89
|
description: 'Collection of entries — array of objects with unique id',
|
|
90
90
|
fields: [
|
|
91
|
-
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'posts', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64 },
|
|
92
|
-
{ name: '
|
|
93
|
-
{ name: '
|
|
91
|
+
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'posts', pattern: NAME_PATTERN, patternHint: NAME_HINT, maxLength: 64, tier: 'basic' },
|
|
92
|
+
{ name: 'body', label: 'Entries (JSON array)', type: 'code', placeholder: '[\n { "id": "first", "title": "First Entry" }\n]', language: 'json', tier: 'basic' },
|
|
93
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional folder', tier: 'advanced' },
|
|
94
94
|
],
|
|
95
95
|
operations: ['create', 'edit', 'delete'],
|
|
96
96
|
},
|
|
@@ -100,10 +100,10 @@ export const ARTIFACT_SCHEMAS = {
|
|
|
100
100
|
icon: '📄',
|
|
101
101
|
description: 'A page inside an existing prototype',
|
|
102
102
|
fields: [
|
|
103
|
-
{ name: 'prototype', label: 'Prototype', type: 'select', required: true, options: [], dynamic: 'prototypes' },
|
|
104
|
-
{ name: 'path', label: 'Path', type: 'text', required: true, placeholder: 'settings/general', pattern: '^[a-z0-9][a-z0-9-/]*$', patternHint: 'Lowercase path with slashes (e.g. settings/general)' },
|
|
105
|
-
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional subfolder within prototype' },
|
|
106
|
-
{ name: 'template', label: 'Template', type: 'text', placeholder: 'Template page to copy from' },
|
|
103
|
+
{ name: 'prototype', label: 'Prototype', type: 'select', required: true, options: [], dynamic: 'prototypes', tier: 'basic' },
|
|
104
|
+
{ name: 'path', label: 'Path', type: 'text', required: true, placeholder: 'settings/general', pattern: '^[a-z0-9][a-z0-9-/]*$', patternHint: 'Lowercase path with slashes (e.g. settings/general)', tier: 'basic' },
|
|
105
|
+
{ name: 'folder', label: 'Folder', type: 'text', placeholder: 'Optional subfolder within prototype', tier: 'advanced' },
|
|
106
|
+
{ name: 'template', label: 'Template', type: 'text', placeholder: 'Template page to copy from', tier: 'advanced' },
|
|
107
107
|
],
|
|
108
108
|
operations: ['create', 'delete'],
|
|
109
109
|
},
|
|
@@ -174,6 +174,28 @@ export class RuntimeClient {
|
|
|
174
174
|
result.version !== '0.0.0' &&
|
|
175
175
|
result.version !== CLIENT_VERSION
|
|
176
176
|
) {
|
|
177
|
+
// Don't kill a daemon that's actively hosting other dev servers —
|
|
178
|
+
// doing so would SIGTERM every Vite child including ones owned by
|
|
179
|
+
// unrelated repos. Multi-repo coexistence is the entire point of
|
|
180
|
+
// running a shared daemon. Warn and reuse instead; the user can
|
|
181
|
+
// manually `sb reset` when they're ready to upgrade.
|
|
182
|
+
let activeCount = 0
|
|
183
|
+
try {
|
|
184
|
+
const listRes = await fetch(`${this.baseUrl}/devserver/list`)
|
|
185
|
+
if (listRes.ok) {
|
|
186
|
+
const data = await listRes.json() as { devServers?: unknown[] }
|
|
187
|
+
activeCount = Array.isArray(data.devServers) ? data.devServers.length : 0
|
|
188
|
+
}
|
|
189
|
+
} catch { /* fall through; treat as zero */ }
|
|
190
|
+
if (activeCount > 0) {
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.warn(
|
|
193
|
+
`[storyboard] daemon version ${result.version} differs from client ${CLIENT_VERSION}, ` +
|
|
194
|
+
`but ${activeCount} dev server(s) are active. Reusing the existing daemon. ` +
|
|
195
|
+
`Run \`sb reset\` to restart it once those are stopped.`,
|
|
196
|
+
)
|
|
197
|
+
return result
|
|
198
|
+
}
|
|
177
199
|
killExistingDaemon()
|
|
178
200
|
// Give the OS a moment to release port 4321
|
|
179
201
|
await new Promise(r => setTimeout(r, 250))
|