@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.
- package/dist/runtime/client/index.js +41 -1
- package/dist/runtime/client/index.js.map +1 -1
- package/dist/runtime/index.js +80 -19
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/server/main.js +39 -18
- package/dist/runtime/server/main.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/client/index.ts +58 -1
- package/src/runtime/proxy/caddy.ts +7 -1
- package/src/runtime/server/constants.ts +25 -2
|
@@ -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
|
-
|
|
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"]}
|
package/dist/runtime/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
578
|
+
return new Promise((resolve3) => {
|
|
518
579
|
const probe = createServer();
|
|
519
|
-
probe.once("error", () =>
|
|
580
|
+
probe.once("error", () => resolve3(false));
|
|
520
581
|
probe.once("listening", () => {
|
|
521
|
-
probe.close(() =>
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
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
|
|
1068
|
-
import { dirname as
|
|
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 (!
|
|
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 (
|
|
1090
|
-
const raw =
|
|
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 =
|
|
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((
|
|
1203
|
+
await new Promise((resolve3, reject) => {
|
|
1143
1204
|
server.once("error", reject);
|
|
1144
|
-
server.listen(RUNTIME_PORT, RUNTIME_HOST, () =>
|
|
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;
|