@dfosco/storyboard 0.5.0-alpha.26 → 0.5.0-alpha.28

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.
@@ -2,6 +2,7 @@
2
2
  import { spawn } from "child_process";
3
3
  import { fileURLToPath } from "url";
4
4
  import { dirname, resolve } from "path";
5
+ import { existsSync, readFileSync } from "fs";
5
6
 
6
7
  // src/runtime/schema/identity.ts
7
8
  import { z } from "zod";
@@ -187,6 +188,38 @@ async function spawnDaemon(baseUrl) {
187
188
  `Storyboard Runtime did not become ready within 5s (tried to spawn ${binPath})`
188
189
  );
189
190
  }
191
+ function readClientVersion() {
192
+ try {
193
+ const here = dirname(fileURLToPath(import.meta.url));
194
+ const candidates = [
195
+ resolve(here, "..", "..", "..", "package.json"),
196
+ resolve(here, "..", "..", "package.json")
197
+ ];
198
+ for (const p of candidates) {
199
+ if (existsSync(p)) {
200
+ const pkg = JSON.parse(readFileSync(p, "utf8"));
201
+ if (typeof pkg.version === "string") return pkg.version;
202
+ }
203
+ }
204
+ } catch {
205
+ }
206
+ return "0.0.0";
207
+ }
208
+ var CLIENT_VERSION = readClientVersion();
209
+ function killExistingDaemon() {
210
+ try {
211
+ const pidPath = resolve(process.env.HOME || "", ".storyboard", "runtime.pid");
212
+ if (!existsSync(pidPath)) return;
213
+ const pid = Number(readFileSync(pidPath, "utf8").trim());
214
+ if (Number.isFinite(pid) && pid > 0) {
215
+ try {
216
+ process.kill(pid, "SIGTERM");
217
+ } catch {
218
+ }
219
+ }
220
+ } catch {
221
+ }
222
+ }
190
223
  var RuntimeClient = class {
191
224
  baseUrl;
192
225
  autoStart;
@@ -196,7 +229,14 @@ var RuntimeClient = class {
196
229
  }
197
230
  async health() {
198
231
  try {
199
- return await request(this.baseUrl, "GET", "/health", void 0, Health);
232
+ const result = await request(this.baseUrl, "GET", "/health", void 0, Health);
233
+ if (this.autoStart && CLIENT_VERSION !== "0.0.0" && result.version !== "0.0.0" && result.version !== CLIENT_VERSION) {
234
+ killExistingDaemon();
235
+ await new Promise((r) => setTimeout(r, 250));
236
+ await spawnDaemon(this.baseUrl);
237
+ return request(this.baseUrl, "GET", "/health", void 0, Health);
238
+ }
239
+ return result;
200
240
  } catch (err) {
201
241
  if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {
202
242
  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 {\n AcquireRequest,\n AcquireResponse,\n Health,\n PoolStatus,\n ProxyRemoveRequest,\n ProxyState,\n ProxyUpsertRequest,\n ReleaseRequest,\n RenewRequest,\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\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 return await request(this.baseUrl, 'GET', '/health', undefined, Health)\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 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 renew(input: z.input<typeof RenewRequest>): Promise<void> {\n const body = RenewRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/renew', 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 * Transitions are enforced in code; illegal transitions throw. This is the\n * structural fix for the per-repo server's \"best-effort\" state — a devserver\n * that thinks it's `ready` but whose port is dead cannot exist here.\n *\n * ```\n * idle → spawning → ready → draining → stopped\n * │ │ │\n * └───────────┴────────┴──────→ stopped (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'idle', // pre-warmed in the hot pool, no project bound yet\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'draining', // releasing — finishing in-flight requests before kill\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 idle: ['spawning', 'stopped'],\n spawning: ['ready', 'stopped'],\n ready: ['draining', 'stopped'],\n draining: ['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.\n *\n * `slot` is `null` for hot-pool members that haven't been acquired yet.\n * Once bound, `slot.devDomain + slot.worktree` is unique across the whole\n * runtime; the orchestrator rejects duplicate binds.\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.nullable(),\n /** Absolute path of the worktree directory once bound; null while in the pool. */\n cwd: z.string().nullable(),\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 short-lived 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. This means a stale `sb dev`\n * process can't kill a devserver belonging to a newer session.\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 /** Renew before this timestamp or the lease expires and the devserver drains. */\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 (renewed). Otherwise it either rents a hot-pool member or spawns a new\n * Vite process. The slot is locked for the 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 /** Lease TTL in seconds. Defaults to 5 min; CLI clients renew on each command. */\n ttlSeconds: z.number().int().min(30).max(60 * 60).default(300),\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 trigger draining. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `POST /devserver/renew` — extend the lease without changing devserver state. */\nexport const RenewRequest = z.object({\n leaseId: z.string().uuid(),\n ttlSeconds: z.number().int().min(30).max(60 * 60).default(300),\n})\nexport type RenewRequest = z.infer<typeof RenewRequest>\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` — hot-pool inventory. */\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 RenewRequest,\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 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 renew(input: z.input<typeof RenewRequest>): Promise<void> {\n const body = RenewRequest.parse(input)\n await request(this.baseUrl, 'POST', '/devserver/renew', 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 * Transitions are enforced in code; illegal transitions throw. This is the\n * structural fix for the per-repo server's \"best-effort\" state — a devserver\n * that thinks it's `ready` but whose port is dead cannot exist here.\n *\n * ```\n * idle → spawning → ready → draining → stopped\n * │ │ │\n * └───────────┴────────┴──────→ stopped (on crash)\n * ```\n */\nexport const DevServerStatus = z.enum([\n 'idle', // pre-warmed in the hot pool, no project bound yet\n 'spawning', // process started, waiting for `ready in …` from Vite stdout\n 'ready', // bound to a slot, accepting traffic via Caddy\n 'draining', // releasing — finishing in-flight requests before kill\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 idle: ['spawning', 'stopped'],\n spawning: ['ready', 'stopped'],\n ready: ['draining', 'stopped'],\n draining: ['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.\n *\n * `slot` is `null` for hot-pool members that haven't been acquired yet.\n * Once bound, `slot.devDomain + slot.worktree` is unique across the whole\n * runtime; the orchestrator rejects duplicate binds.\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.nullable(),\n /** Absolute path of the worktree directory once bound; null while in the pool. */\n cwd: z.string().nullable(),\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 short-lived 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. This means a stale `sb dev`\n * process can't kill a devserver belonging to a newer session.\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 /** Renew before this timestamp or the lease expires and the devserver drains. */\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 (renewed). Otherwise it either rents a hot-pool member or spawns a new\n * Vite process. The slot is locked for the 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 /** Lease TTL in seconds. Defaults to 5 min; CLI clients renew on each command. */\n ttlSeconds: z.number().int().min(30).max(60 * 60).default(300),\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 trigger draining. */\nexport const ReleaseRequest = z.object({\n leaseId: z.string().uuid(),\n})\nexport type ReleaseRequest = z.infer<typeof ReleaseRequest>\n\n/** `POST /devserver/renew` — extend the lease without changing devserver state. */\nexport const RenewRequest = z.object({\n leaseId: z.string().uuid(),\n ttlSeconds: z.number().int().min(30).max(60 * 60).default(300),\n})\nexport type RenewRequest = z.infer<typeof RenewRequest>\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` — hot-pool inventory. */\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"]}
@@ -142,6 +142,7 @@ var RuntimeError = z3.object({
142
142
  import { spawn } from "child_process";
143
143
  import { fileURLToPath } from "url";
144
144
  import { dirname, resolve } from "path";
145
+ import { existsSync, readFileSync } from "fs";
145
146
  var RUNTIME_BASE = "http://127.0.0.1:4321";
146
147
  var RuntimeRequestError = class extends Error {
147
148
  constructor(message, status, code, details) {
@@ -207,6 +208,38 @@ async function spawnDaemon(baseUrl) {
207
208
  `Storyboard Runtime did not become ready within 5s (tried to spawn ${binPath})`
208
209
  );
209
210
  }
211
+ function readClientVersion() {
212
+ try {
213
+ const here = dirname(fileURLToPath(import.meta.url));
214
+ const candidates = [
215
+ resolve(here, "..", "..", "..", "package.json"),
216
+ resolve(here, "..", "..", "package.json")
217
+ ];
218
+ for (const p of candidates) {
219
+ if (existsSync(p)) {
220
+ const pkg = JSON.parse(readFileSync(p, "utf8"));
221
+ if (typeof pkg.version === "string") return pkg.version;
222
+ }
223
+ }
224
+ } catch {
225
+ }
226
+ return "0.0.0";
227
+ }
228
+ var CLIENT_VERSION = readClientVersion();
229
+ function killExistingDaemon() {
230
+ try {
231
+ const pidPath = resolve(process.env.HOME || "", ".storyboard", "runtime.pid");
232
+ if (!existsSync(pidPath)) return;
233
+ const pid = Number(readFileSync(pidPath, "utf8").trim());
234
+ if (Number.isFinite(pid) && pid > 0) {
235
+ try {
236
+ process.kill(pid, "SIGTERM");
237
+ } catch {
238
+ }
239
+ }
240
+ } catch {
241
+ }
242
+ }
210
243
  var RuntimeClient = class {
211
244
  baseUrl;
212
245
  autoStart;
@@ -216,7 +249,14 @@ var RuntimeClient = class {
216
249
  }
217
250
  async health() {
218
251
  try {
219
- return await request(this.baseUrl, "GET", "/health", void 0, Health);
252
+ const result = await request(this.baseUrl, "GET", "/health", void 0, Health);
253
+ if (this.autoStart && CLIENT_VERSION !== "0.0.0" && result.version !== "0.0.0" && result.version !== CLIENT_VERSION) {
254
+ killExistingDaemon();
255
+ await new Promise((r) => setTimeout(r, 250));
256
+ await spawnDaemon(this.baseUrl);
257
+ return request(this.baseUrl, "GET", "/health", void 0, Health);
258
+ }
259
+ return result;
220
260
  } catch (err) {
221
261
  if (this.autoStart && err instanceof RuntimeRequestError && err.status === 0) {
222
262
  await spawnDaemon(this.baseUrl);
@@ -283,7 +323,9 @@ var CaddyAdminClient = class {
283
323
  const controller = new AbortController();
284
324
  const t = setTimeout(() => controller.abort(), this.timeoutMs);
285
325
  try {
286
- return await fetch(`${this.baseUrl}${path}`, { ...init, signal: controller.signal });
326
+ const headers = new Headers(init.headers);
327
+ if (!headers.has("origin")) headers.set("origin", this.baseUrl);
328
+ return await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal: controller.signal });
287
329
  } catch (err) {
288
330
  throw new CaddyUnreachableError(this.baseUrl, err);
289
331
  } finally {
@@ -473,7 +515,9 @@ import { createServer } from "net";
473
515
 
474
516
  // src/runtime/server/constants.ts
475
517
  import { homedir } from "os";
476
- import { join } from "path";
518
+ import { join, dirname as dirname2, resolve as resolve2 } from "path";
519
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
520
+ import { fileURLToPath as fileURLToPath2 } from "url";
477
521
  var RUNTIME_PORT = 4321;
478
522
  var RUNTIME_HOST = "127.0.0.1";
479
523
  var RUNTIME_HOME = join(homedir(), ".storyboard");
@@ -482,7 +526,24 @@ var LOCKFILE = join(RUNTIME_HOME, "runtime.lock");
482
526
  var STATEFILE = join(RUNTIME_HOME, "runtime.state.json");
483
527
  var DEVSERVER_PORT_MIN = 1240;
484
528
  var DEVSERVER_PORT_MAX = 1399;
485
- var RUNTIME_VERSION = "0.0.0";
529
+ function readPackageVersion() {
530
+ try {
531
+ const here = dirname2(fileURLToPath2(import.meta.url));
532
+ const candidates = [
533
+ resolve2(here, "..", "..", "..", "package.json"),
534
+ resolve2(here, "..", "..", "package.json")
535
+ ];
536
+ for (const p of candidates) {
537
+ if (existsSync2(p)) {
538
+ const pkg = JSON.parse(readFileSync2(p, "utf8"));
539
+ if (typeof pkg.version === "string" && pkg.version) return pkg.version;
540
+ }
541
+ }
542
+ } catch {
543
+ }
544
+ return "0.0.0";
545
+ }
546
+ var RUNTIME_VERSION = readPackageVersion();
486
547
 
487
548
  // src/runtime/devserver/port-pool.ts
488
549
  var PortExhaustedError = class extends Error {
@@ -514,11 +575,11 @@ var PortPool = class {
514
575
  }
515
576
  };
516
577
  function isPortFree(port) {
517
- return new Promise((resolve2) => {
578
+ return new Promise((resolve3) => {
518
579
  const probe = createServer();
519
- probe.once("error", () => resolve2(false));
580
+ probe.once("error", () => resolve3(false));
520
581
  probe.once("listening", () => {
521
- probe.close(() => resolve2(true));
582
+ probe.close(() => resolve3(true));
522
583
  });
523
584
  probe.listen(port, "127.0.0.1");
524
585
  });
@@ -526,7 +587,7 @@ function isPortFree(port) {
526
587
 
527
588
  // src/runtime/devserver/orchestrator.ts
528
589
  import { spawn as spawn2 } from "child_process";
529
- import { existsSync } from "fs";
590
+ import { existsSync as existsSync3 } from "fs";
530
591
  import { resolve as resolvePath } from "path";
531
592
  import { randomUUID } from "crypto";
532
593
  var SlotCwdConflictError = class extends Error {
@@ -583,7 +644,7 @@ var DevServerOrchestrator = class {
583
644
  if (input.slot.devDomain === DEFAULT_DEV_DOMAIN && !input.allowDefaultDomain) {
584
645
  throw new ForbiddenDefaultDomainError();
585
646
  }
586
- if (!existsSync(input.targetCwd)) {
647
+ if (!existsSync3(input.targetCwd)) {
587
648
  throw new Error(`targetCwd does not exist: ${input.targetCwd}`);
588
649
  }
589
650
  const key = slotKey(input.slot);
@@ -770,10 +831,10 @@ function toDevServer(ds) {
770
831
  }
771
832
  function defaultSpawnVite(cwd, port, basePath, devDomain) {
772
833
  const localVite = resolvePath(cwd, "node_modules", ".bin", "vite");
773
- const useLocal = existsSync(localVite);
834
+ const useLocal = existsSync3(localVite);
774
835
  const wrapperPath = resolvePath(import.meta.dirname ?? "", "..", "vite-plugin", "wrapper.js");
775
836
  const args = ["--port", String(port)];
776
- if (existsSync(wrapperPath)) {
837
+ if (existsSync3(wrapperPath)) {
777
838
  args.push("--config", wrapperPath);
778
839
  }
779
840
  const branchMatch = basePath.match(/^\/branch--([^/]+)\/$/);
@@ -1064,8 +1125,8 @@ function createRuntimeServer(opts = {}) {
1064
1125
  }
1065
1126
 
1066
1127
  // src/runtime/server/lock.ts
1067
- import { existsSync as existsSync2, mkdirSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync } from "fs";
1068
- import { dirname as dirname2 } from "path";
1128
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync, openSync, closeSync } from "fs";
1129
+ import { dirname as dirname3 } from "path";
1069
1130
  var RuntimeAlreadyRunningError = class extends Error {
1070
1131
  constructor(pid) {
1071
1132
  super(`Storyboard Runtime is already running (pid ${pid})`);
@@ -1074,7 +1135,7 @@ var RuntimeAlreadyRunningError = class extends Error {
1074
1135
  }
1075
1136
  };
1076
1137
  function ensureRuntimeHome() {
1077
- if (!existsSync2(RUNTIME_HOME)) mkdirSync(RUNTIME_HOME, { recursive: true });
1138
+ if (!existsSync4(RUNTIME_HOME)) mkdirSync(RUNTIME_HOME, { recursive: true });
1078
1139
  }
1079
1140
  function isProcessAlive(pid) {
1080
1141
  try {
@@ -1086,8 +1147,8 @@ function isProcessAlive(pid) {
1086
1147
  }
1087
1148
  function acquireRuntimeLock() {
1088
1149
  ensureRuntimeHome();
1089
- if (existsSync2(LOCKFILE)) {
1090
- const raw = readFileSync(LOCKFILE, "utf8").trim();
1150
+ if (existsSync4(LOCKFILE)) {
1151
+ const raw = readFileSync3(LOCKFILE, "utf8").trim();
1091
1152
  const pid = Number(raw);
1092
1153
  if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {
1093
1154
  throw new RuntimeAlreadyRunningError(pid);
@@ -1102,7 +1163,7 @@ function acquireRuntimeLock() {
1102
1163
  fd = openSync(LOCKFILE, "wx");
1103
1164
  } catch (err) {
1104
1165
  if (err.code === "EEXIST") {
1105
- const raw = readFileSync(LOCKFILE, "utf8").trim();
1166
+ const raw = readFileSync3(LOCKFILE, "utf8").trim();
1106
1167
  const pid = Number(raw) || 0;
1107
1168
  throw new RuntimeAlreadyRunningError(pid);
1108
1169
  }
@@ -1139,9 +1200,9 @@ async function startDaemon() {
1139
1200
  throw err;
1140
1201
  }
1141
1202
  const server = createRuntimeServer();
1142
- await new Promise((resolve2, reject) => {
1203
+ await new Promise((resolve3, reject) => {
1143
1204
  server.once("error", reject);
1144
- server.listen(RUNTIME_PORT, RUNTIME_HOST, () => resolve2());
1205
+ server.listen(RUNTIME_PORT, RUNTIME_HOST, () => resolve3());
1145
1206
  });
1146
1207
  console.log(`[storyboard-runtime] listening on http://${RUNTIME_HOST}:${RUNTIME_PORT}`);
1147
1208
  let shuttingDown = false;