@joycostudio/studio 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -33,6 +33,18 @@ export const grading = defineTool({
33
33
  })
34
34
  ```
35
35
 
36
+ ### Preview-only tools
37
+
38
+ A tool with nothing to tune leaves out `file`, `initial`, and `controls`. It still gets a sidebar entry, a pin, and a
39
+ ⌘/Ctrl+K entry, but Studio shows only its preview, with no inspector. Register it with the rest of the tools.
40
+
41
+ ```ts
42
+ export const gallery = defineTool({ id: 'gallery', title: 'Gallery', preview: '/gallery' })
43
+ ```
44
+
45
+ `studio.definitions` lists every tool in order. `studio.tools` holds handles for tools with a config, so preview-only
46
+ tools never reach the debug pane. Over MCP they are listed without a `file` and can't be read, edited, or captured.
47
+
36
48
  ## 2. Register tools with the runtime
37
49
 
38
50
  ```ts
@@ -48,7 +60,15 @@ export default defineConfig({ tools: [grading] })
48
60
  ```
49
61
 
50
62
  `studio dev` starts the WebSocket bridge on `ws://127.0.0.1:5179` and the HTTP MCP endpoint on
51
- `http://127.0.0.1:5179/mcp` (`--port` changes both). It stops when the wrapped command exits.
63
+ `http://127.0.0.1:5179/mcp` (`--port` or `port` in the config changes both). It stops when the wrapped command exits.
64
+
65
+ Several projects can run at once. When another project's Studio already holds the port, `studio dev` moves to the next
66
+ free one and passes it to the wrapped command as `STUDIO_PORT`. Read that variable instead of hardcoding the port:
67
+
68
+ ```ts
69
+ // next.config.ts
70
+ export default { env: { NEXT_PUBLIC_STUDIO_PORT: process.env.STUDIO_PORT ?? '5179' } }
71
+ ```
52
72
 
53
73
  ## 3. Use it in the app
54
74
 
@@ -58,7 +78,7 @@ import { createStudio } from '@joycostudio/studio/client'
58
78
 
59
79
  export const studio = createStudio({
60
80
  tools: [grading],
61
- runtime: process.env.NODE_ENV === 'development' && 'ws://127.0.0.1:5179',
81
+ runtime: process.env.NODE_ENV === 'development' && `ws://127.0.0.1:${process.env.NEXT_PUBLIC_STUDIO_PORT}`,
62
82
  })
63
83
  ```
64
84
 
@@ -103,10 +123,18 @@ Use the pin button beside a sidebar tool to keep it at the top. Pinned tools als
103
123
  **Command/Ctrl+K** menu. Pins are saved in this browser for the current origin and shared across tabs;
104
124
  unpinning restores registration order. Pinning a tool keeps the current preview selected.
105
125
 
126
+ In Studio, **Command/Ctrl+Z** undoes the selected tool's last edit and **Command/Ctrl+Y** (or **Shift+Command/Ctrl+Z**)
127
+ redoes it, also from the same-origin preview iframe. A slider drag undoes as one step, and Discard can be undone too.
128
+ Undo is per tool and per page: it restores only the values you changed, leaving edits from other tabs or MCP agents
129
+ in place, and syncs like any other edit. Text fields keep their native undo while focused. History is not persisted.
130
+ `tool.undo()`, `tool.redo()`, `tool.canUndo`, and `tool.canRedo` expose the same history to host code.
131
+
106
132
  In Studio, **Command+B** (Mac) or **Ctrl+B** slides both side panels out of view; press it again to bring them back.
107
133
  This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately,
108
- and that choice is preserved when hiding and showing the panels. Move the pointer near either screen edge to reveal a small
109
- tab; click it to bring both panels back. The tabs also appear on keyboard focus and stay visible on touch devices.
134
+ and that choice is preserved when hiding and showing the panels. Move the pointer near a hidden panel's screen edge to
135
+ reveal a small tab; click it to bring back only that panel, so you can work with just the tools or just the inspector.
136
+ With one panel open, the shortcut hides it; with both hidden, it brings both back. The tabs also appear on keyboard
137
+ focus and stay visible on touch devices.
110
138
  With reduced motion enabled, panels toggle instantly.
111
139
 
112
140
  Previews use the shared `PreviewCanvas`: Reload and the tool title sit on the left, with Expand and
@@ -115,6 +143,9 @@ pinned; enabling auto-hide overlays the header and hides it after three seconds
115
143
  focus, and open menus keep it visible, and touch devices always show the controls. Contained previews
116
144
  fit between the side panels, with the inspector stacked below on narrow screens.
117
145
 
146
+ Each hidden panel and the expanded canvas are kept in session storage, so the layout survives a refresh
147
+ in the same tab. New tabs start with panels shown and the canvas contained.
148
+
118
149
  ### Shared canvas and panel components
119
150
 
120
151
  Import these from `@joycostudio/studio/react` with `@joycostudio/studio/styles.css`:
@@ -214,8 +245,8 @@ this URL:
214
245
  http://127.0.0.1:5179/mcp
215
246
  ```
216
247
 
217
- Use the URL printed by the CLI when running on a different port. Each project's runtime needs its own port when several
218
- projects run at once. The endpoint is local to the machine running Studio; a remote/cloud agent cannot reach it through
248
+ Use the URL printed by the CLI when it runs on a different port, which happens when another project's Studio started
249
+ first. To keep a project's URL stable, give each project its own `port` in `studio.config.ts`. The endpoint is local to the machine running Studio; a remote/cloud agent cannot reach it through
219
250
  its own `localhost`.
220
251
 
221
252
  The MCP endpoint shares the running app's drafts. It supports both the 2025 MCP initialization flow and the 2026 MCP
@@ -247,6 +278,11 @@ Set the running app's URL once in `studio.config.ts` (or override it with `studi
247
278
  export default defineConfig({ tools: [grading], appUrl: 'http://localhost:3000' })
248
279
  ```
249
280
 
281
+ When `appUrl` is local, `studio dev` passes its port to the wrapped command as `PORT` (which `next dev` reads). If another
282
+ app already answers there, both move to the next free port, so captures keep reaching this project's app. Commands that
283
+ ignore `PORT` need it forwarded, e.g. `studio dev -- sh -c 'vite --port $PORT'`. An explicit `--app-url` or a `PORT`
284
+ already in the environment is left as is.
285
+
250
286
  Install Chromium once with `pnpm exec studio install-browser`. Then call `studio_capture_preview` with a tool ID.
251
287
  The existing `preview` route must use the Studio client connected to this runtime. No capture code or readiness hooks
252
288
  are needed in tool definitions, HTML components, or canvas renderers.
@@ -277,8 +313,8 @@ without authentication; do not expose it through a public proxy.
277
313
  - **Bridge**: WebSocket (`tools.list`, `tool.get`, `tool.update`, `tool.save`, `tool.discard` + `state` broadcasts) and HTTP MCP
278
314
  on the same port. `/studio`, every app tab, and MCP all go through these same runtime operations.
279
315
  - **Without a runtime** (production or offline), browser messages synchronize live configs between
280
- same-origin Studio pages, previews, and app tabs. Subscriptions and Tweakpane controls still update;
281
- nothing persists. Browsers without `BroadcastChannel` retain same-page tweaking.
316
+ same-origin Studio pages, previews, and app tabs. Subscriptions and Tweakpane controls still update,
317
+ and the changes list and hold-to-compare diff against the bundled `initial` config; nothing persists. Browsers without `BroadcastChannel` retain same-page tweaking.
282
318
 
283
319
  Run `pnpm --filter @joycostudio/studio test:browser` to verify runtime-free pane edits, iframe/tab
284
320
  sync, preview reloads, and runtime disconnect/reconnect in Chromium. Install Chromium first with
@@ -1,3 +1,19 @@
1
+ // src/core/tool.ts
2
+ var hasConfig = (tool) => tool.controls !== void 0;
3
+ var ID = /^[a-z0-9][a-z0-9-]*$/;
4
+ function defineTool(tool) {
5
+ if (!ID.test(tool.id)) throw new Error(`[studio] tool id "${tool.id}" must be kebab-case`);
6
+ const config = [tool.file, tool.initial, tool.controls].filter((part) => part !== void 0).length;
7
+ if (config === 1 || config === 2)
8
+ throw new Error(`[studio] tool "${tool.id}" needs all of file, initial, and controls, or none of them`);
9
+ if (!config && !tool.preview) throw new Error(`[studio] tool "${tool.id}" needs a preview or a config`);
10
+ return tool;
11
+ }
12
+ var DEFAULT_PORT = 5179;
13
+ function defineConfig(config) {
14
+ return config;
15
+ }
16
+
1
17
  // src/core/result.ts
2
18
  var success = (data) => ({ ok: true, data });
3
19
  var error = (code, message) => ({
@@ -73,8 +89,12 @@ function invalidControl(schema, path = "") {
73
89
  }
74
90
 
75
91
  export {
92
+ hasConfig,
93
+ defineTool,
94
+ DEFAULT_PORT,
95
+ defineConfig,
76
96
  success,
77
97
  error,
78
98
  validateConfig
79
99
  };
80
- //# sourceMappingURL=chunk-X4YLM5HA.js.map
100
+ //# sourceMappingURL=chunk-OIMLF2CL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/tool.ts","../src/core/result.ts","../src/core/validate.ts"],"sourcesContent":["import type { DebugSchema } from '@joycostudio/xyz/debug'\n\n/**\n * An SVG icon as data: `[tag, attributes]` children of a 24×24 stroke icon.\n * Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.\n */\nexport type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[]\n\ntype ToolBase = {\n /** Unique, URL-safe identifier. */\n id: string\n title?: string\n /** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */\n icon?: ToolIcon\n /** App route that renders this tool's result, shown next to the controls in /studio. */\n preview?: string\n /** Use a bare iframe for tools that already own their canvas layout and headers. */\n previewLayout?: 'canvas' | 'custom'\n}\n\n/**\n * A tool's contract. Isomorphic on purpose: the runtime loads it in Node to\n * validate and persist, the app loads it in the browser to apply and debug.\n *\n * `TConfig` is the one type that is read, edited, applied, and persisted.\n */\nexport type ToolDefinition<TConfig extends object = object> = ToolBase & {\n /** Repo-relative JSON file the config persists to. Import that same file as `initial`. */\n file: string\n /** Config used before the runtime answers (and in production). */\n initial: TConfig\n /**\n * What can be tweaked, as an Xyz/Tweakpane schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => DebugSchema\n}\n\n/** A tool with nothing to tune: a sidebar entry for its preview, without an inspector or config file. */\nexport type PreviewToolDefinition = ToolBase & {\n preview: string\n file?: never\n initial?: never\n controls?: never\n}\n\n/** A tool with a config. */\nexport type AnyTool = ToolDefinition<any>\n\n/** Anything `defineTool` returns: the kind follows from whether it declares a config. */\nexport type StudioTool = AnyTool | PreviewToolDefinition\n\nexport const hasConfig = (tool: StudioTool): tool is AnyTool => tool.controls !== undefined\n\nexport type ConfigOf<TTool> = TTool extends ToolDefinition<infer TConfig> ? TConfig : never\n\nconst ID = /^[a-z0-9][a-z0-9-]*$/\n\n/** Declare `file`, `initial`, and `controls` for a tunable tool, or only a `preview` for a preview-only one. */\nexport function defineTool<TConfig extends object>(tool: ToolDefinition<TConfig>): ToolDefinition<TConfig>\nexport function defineTool(tool: PreviewToolDefinition): PreviewToolDefinition\nexport function defineTool(tool: StudioTool): StudioTool {\n if (!ID.test(tool.id)) throw new Error(`[studio] tool id \"${tool.id}\" must be kebab-case`)\n const config = [tool.file, tool.initial, tool.controls].filter((part) => part !== undefined).length\n if (config === 1 || config === 2)\n throw new Error(`[studio] tool \"${tool.id}\" needs all of file, initial, and controls, or none of them`)\n if (!config && !tool.preview) throw new Error(`[studio] tool \"${tool.id}\" needs a preview or a config`)\n return tool\n}\n\nexport type StudioConfig = {\n tools: readonly StudioTool[]\n /** Runtime WebSocket and HTTP MCP port. Defaults to 5179. */\n port?: number\n /** Browser origins allowed on the bridge and MCP endpoint. Defaults to localhost origins. */\n origins?: readonly string[]\n /** Running app URL (e.g. http://localhost:3000), used for MCP preview captures. */\n appUrl?: string\n}\n\nexport const DEFAULT_PORT = 5179\n\nexport function defineConfig(config: StudioConfig): StudioConfig {\n return config\n}\n","export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: DebugSchema, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import path4 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/core/tool.ts
10
+ var hasConfig = (tool2) => tool2.controls !== void 0;
10
11
  var DEFAULT_PORT = 5179;
11
12
 
12
13
  // src/runtime/runtime.ts
@@ -154,8 +155,11 @@ async function createRuntime({ root, tools }) {
154
155
  root = path2.resolve(root);
155
156
  const entries = /* @__PURE__ */ new Map();
156
157
  const files = /* @__PURE__ */ new Set();
158
+ const ids = /* @__PURE__ */ new Set();
157
159
  for (const tool2 of tools) {
158
- if (entries.has(tool2.id)) throw new Error(`[studio] duplicate tool id "${tool2.id}"`);
160
+ if (ids.has(tool2.id)) throw new Error(`[studio] duplicate tool id "${tool2.id}"`);
161
+ ids.add(tool2.id);
162
+ if (!hasConfig(tool2)) continue;
159
163
  const file = await resolveToolFile(root, tool2.file);
160
164
  if (files.has(file)) throw new Error(`[studio] tool "${tool2.id}" shares its file with another tool`);
161
165
  files.add(file);
@@ -190,7 +194,7 @@ async function createRuntime({ root, tools }) {
190
194
  };
191
195
  const handlers = {
192
196
  "tools.list": async () => success(
193
- [...entries.values()].map(({ tool: tool2 }) => ({
197
+ tools.map((tool2) => ({
194
198
  id: tool2.id,
195
199
  title: tool2.title ?? tool2.id,
196
200
  file: tool2.file,
@@ -236,7 +240,8 @@ async function createRuntime({ root, tools }) {
236
240
  };
237
241
  async function withEntry(id, task) {
238
242
  const entry = typeof id === "string" ? entries.get(id) : void 0;
239
- return entry ? task(entry) : error("unknown_tool", `no tool "${String(id)}"`);
243
+ if (entry) return task(entry);
244
+ return ids.has(id) ? error("no_config", `tool "${String(id)}" is preview-only and has no config`) : error("unknown_tool", `no tool "${String(id)}"`);
240
245
  }
241
246
  const reload = (entry) => serial(entry, async () => {
242
247
  const disk = await readDisk(entry.file);
@@ -446,7 +451,7 @@ function createMcpServer(runtime, previews) {
446
451
  server.registerTool(
447
452
  "studio_list_tools",
448
453
  {
449
- description: "List registered Studio tools with their IDs, titles, config files, and preview routes.",
454
+ description: "List registered Studio tools with their IDs, titles, config files, and preview routes. Tools without a file are preview-only: they have no config to read, edit, or capture.",
450
455
  inputSchema: z.object({}),
451
456
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
452
457
  },
@@ -616,14 +621,47 @@ async function loadConfig(cwd, explicit) {
616
621
  return { config, root: path3.dirname(file) };
617
622
  }
618
623
 
624
+ // src/runtime/ports.ts
625
+ import { connect } from "net";
626
+ var ATTEMPTS = 20;
627
+ async function serveBridgeNear(runtime, options) {
628
+ for (let port = options.port; ; port++) {
629
+ try {
630
+ return await serveBridge(runtime, { ...options, port });
631
+ } catch (cause) {
632
+ const taken = cause.code === "EADDRINUSE";
633
+ if (!taken || options.port === 0 || port - options.port >= ATTEMPTS - 1) throw cause;
634
+ }
635
+ }
636
+ }
637
+ function answers(hostname, port) {
638
+ return new Promise((resolve) => {
639
+ const socket = connect({ host: hostname.replace(/^\[|\]$/g, ""), port, timeout: 1e3 });
640
+ const done = (busy) => {
641
+ socket.destroy();
642
+ resolve(busy);
643
+ };
644
+ socket.once("connect", () => done(true));
645
+ socket.once("timeout", () => done(true));
646
+ socket.once("error", () => done(false));
647
+ });
648
+ }
649
+ async function freeAppPort(hostname, port) {
650
+ for (let candidate = port; candidate < port + ATTEMPTS; candidate++)
651
+ if (!await answers(hostname, candidate)) return candidate;
652
+ throw new Error(`[studio] ports ${port}-${port + ATTEMPTS - 1} on ${hostname} are all in use`);
653
+ }
654
+
619
655
  // src/cli.ts
620
656
  var HELP = `Usage: studio dev [--config studio.config.ts] [--port ${DEFAULT_PORT}] [--app-url http://localhost:3000] [-- <command...>]
621
657
  studio install-browser
622
658
 
623
- Starts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp.
624
- Anything after "--" runs alongside it (e.g. "next dev")
659
+ Starts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp,
660
+ moving to the next free port when another project's Studio already uses it.
661
+ Anything after "--" runs alongside it (e.g. "next dev") with STUDIO_PORT set to the runtime port,
625
662
  and the runtime stops when that command exits.
626
- Set appUrl in the config or pass --app-url to enable MCP preview capture.
663
+ Set appUrl in the config or pass --app-url to enable MCP preview capture. A configured local
664
+ appUrl port is passed to the command as PORT, moved to the next free port when taken.
627
665
  Run "studio install-browser" once to install Chromium for captures.`;
628
666
  async function main(argv) {
629
667
  if (argv[0] === "install-browser") {
@@ -655,12 +693,20 @@ async function main(argv) {
655
693
  });
656
694
  if (values.help || positionals[0] !== "dev") return console.log(HELP);
657
695
  const { config, root } = await loadConfig(process.cwd(), values.config);
696
+ const [bin, ...args] = command;
697
+ const env = { ...process.env };
698
+ let appUrl = values["app-url"] ?? config.appUrl;
699
+ const app = bin && !values["app-url"] && !env.PORT && appUrl && URL.canParse(appUrl) ? new URL(appUrl) : void 0;
700
+ if (app?.port && ["localhost", "127.0.0.1", "[::1]"].includes(app.hostname)) {
701
+ const preferred = app.port;
702
+ app.port = env.PORT = String(await freeAppPort(app.hostname, Number(preferred)));
703
+ if (app.port !== preferred) console.log(`[studio] port ${preferred} is in use, starting the app on ${app.origin}`);
704
+ appUrl = app.href;
705
+ }
658
706
  const runtime = await createRuntime({ root, tools: config.tools });
659
- const bridge = await serveBridge(runtime, {
660
- port: Number(values.port ?? config.port ?? DEFAULT_PORT),
661
- origins: config.origins,
662
- appUrl: values["app-url"] ?? config.appUrl
663
- });
707
+ const port = Number(values.port ?? config.port ?? DEFAULT_PORT);
708
+ const bridge = await serveBridgeNear(runtime, { port, origins: config.origins, appUrl });
709
+ if (bridge.port !== port) console.log(`[studio] port ${port} is in use, probably by another project's Studio`);
664
710
  console.log(
665
711
  `[studio] runtime ready on ws://127.0.0.1:${bridge.port} \u2014 ${config.tools.length} tool(s) in ${path4.relative(process.cwd(), root) || "."}`
666
712
  );
@@ -670,13 +716,12 @@ async function main(argv) {
670
716
  await bridge.close();
671
717
  process.exit(code);
672
718
  };
673
- const [bin, ...args] = command;
674
719
  if (!bin) {
675
720
  process.once("SIGINT", () => void shutdown());
676
721
  process.once("SIGTERM", () => void shutdown());
677
722
  return;
678
723
  }
679
- const child = spawn(bin, args, { stdio: "inherit", env: { ...process.env, STUDIO_PORT: String(bridge.port) } });
724
+ const child = spawn(bin, args, { stdio: "inherit", env: { ...env, STUDIO_PORT: String(bridge.port) } });
680
725
  child.once("exit", (code) => void shutdown(code ?? 0));
681
726
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => child.kill(signal));
682
727
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/core/tool.ts","../src/runtime/runtime.ts","../src/core/result.ts","../src/core/validate.ts","../src/runtime/files.ts","../src/runtime/server.ts","../src/runtime/capture.ts","../src/runtime/mcp.ts","../src/runtime/load.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { DEFAULT_PORT } from './core/tool'\nimport { createRuntime, loadConfig, serveBridge } from './runtime'\n\nconst HELP = `Usage: studio dev [--config studio.config.ts] [--port ${DEFAULT_PORT}] [--app-url http://localhost:3000] [-- <command...>]\n studio install-browser\n\nStarts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp.\nAnything after \"--\" runs alongside it (e.g. \"next dev\")\nand the runtime stops when that command exits.\nSet appUrl in the config or pass --app-url to enable MCP preview capture.\nRun \"studio install-browser\" once to install Chromium for captures.`\n\nasync function main(argv: string[]) {\n if (argv[0] === 'install-browser') {\n const require = createRequire(import.meta.url)\n const cli = path.join(\n path.dirname(require.resolve('playwright/package.json')),\n require('playwright/package.json').bin.playwright,\n )\n const child = spawn(process.execPath, [cli, 'install', 'chromium'], { stdio: 'inherit' })\n child.once('error', (cause) => {\n console.error(cause.message)\n process.exit(1)\n })\n child.once('exit', (code) => process.exit(code ?? 1))\n return\n }\n const split = argv.indexOf('--')\n const own = split === -1 ? argv : argv.slice(0, split)\n const command = split === -1 ? [] : argv.slice(split + 1)\n const { positionals, values } = parseArgs({\n args: own,\n allowPositionals: true,\n options: {\n config: { type: 'string' },\n port: { type: 'string' },\n 'app-url': { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n })\n if (values.help || positionals[0] !== 'dev') return console.log(HELP)\n\n const { config, root } = await loadConfig(process.cwd(), values.config)\n const runtime = await createRuntime({ root, tools: config.tools })\n const bridge = await serveBridge(runtime, {\n port: Number(values.port ?? config.port ?? DEFAULT_PORT),\n origins: config.origins,\n appUrl: values['app-url'] ?? config.appUrl,\n })\n console.log(\n `[studio] runtime ready on ws://127.0.0.1:${bridge.port} — ${config.tools.length} tool(s) in ${path.relative(process.cwd(), root) || '.'}`,\n )\n console.log(`[studio] MCP ready on http://127.0.0.1:${bridge.port}/mcp`)\n\n const shutdown = async (code = 0) => {\n runtime.close()\n await bridge.close()\n process.exit(code)\n }\n\n const [bin, ...args] = command\n if (!bin) {\n process.once('SIGINT', () => void shutdown())\n process.once('SIGTERM', () => void shutdown())\n return\n }\n const child = spawn(bin, args, { stdio: 'inherit', env: { ...process.env, STUDIO_PORT: String(bridge.port) } })\n child.once('exit', (code) => void shutdown(code ?? 0))\n for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => child.kill(signal))\n}\n\nmain(process.argv.slice(2)).catch((cause) => {\n console.error(cause instanceof Error ? cause.message : cause)\n process.exit(1)\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\n\n/**\n * An SVG icon as data: `[tag, attributes]` children of a 24×24 stroke icon.\n * Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.\n */\nexport type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[]\n\n/**\n * A tool's contract. Isomorphic on purpose: the runtime loads it in Node to\n * validate and persist, the app loads it in the browser to apply and debug.\n *\n * `TConfig` is the one type that is read, edited, applied, and persisted.\n */\nexport type ToolDefinition<TConfig extends object = object> = {\n /** Unique, URL-safe identifier. */\n id: string\n title?: string\n /** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */\n icon?: ToolIcon\n /** Repo-relative JSON file the config persists to. Import that same file as `initial`. */\n file: string\n /** Config used before the runtime answers (and in production). */\n initial: TConfig\n /**\n * What can be tweaked, as an Xyz/Tweakpane schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => DebugSchema\n /** App route that renders this tool's result, shown next to the controls in /studio. */\n preview?: string\n /** Use a bare iframe for tools that already own their canvas layout and headers. */\n previewLayout?: 'canvas' | 'custom'\n}\n\nexport type AnyTool = ToolDefinition<any>\n\nexport type ConfigOf<TTool> = TTool extends ToolDefinition<infer TConfig> ? TConfig : never\n\nconst ID = /^[a-z0-9][a-z0-9-]*$/\n\nexport function defineTool<TConfig extends object>(tool: ToolDefinition<TConfig>): ToolDefinition<TConfig> {\n if (!ID.test(tool.id)) throw new Error(`[studio] tool id \"${tool.id}\" must be kebab-case`)\n return tool\n}\n\nexport type StudioConfig = {\n tools: readonly AnyTool[]\n /** Runtime WebSocket and HTTP MCP port. Defaults to 5179. */\n port?: number\n /** Browser origins allowed on the bridge and MCP endpoint. Defaults to localhost origins. */\n origins?: readonly string[]\n /** Running app URL (e.g. http://localhost:3000), used for MCP preview captures. */\n appUrl?: string\n}\n\nexport const DEFAULT_PORT = 5179\n\nexport function defineConfig(config: StudioConfig): StudioConfig {\n return config\n}\n","import { watch, type FSWatcher } from 'node:fs'\nimport path from 'node:path'\nimport type { Op, Operations, ToolState, ToolSummary } from '../core/protocol'\nimport { error, success, type Result } from '../core/result'\nimport type { AnyTool } from '../core/tool'\nimport { validateConfig } from '../core/validate'\nimport { hashOf, readDisk, resolveToolFile, serialize, writeAtomic } from './files'\n\ntype Entry = {\n tool: AnyTool\n file: string\n state: ToolState\n /** Serializes disk work per tool so saves and external reloads never interleave. */\n queue: Promise<unknown>\n}\n\nexport type RuntimeListener = (state: ToolState, origin: unknown) => void\n\nexport type Runtime = {\n root: string\n run<TOp extends Op>(\n op: TOp,\n params: Operations[TOp]['params'],\n origin?: unknown,\n ): Promise<Result<Operations[TOp]['result']>>\n subscribe(listener: RuntimeListener): () => void\n close(): void\n}\n\nconst equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n const keys = Object.keys(a)\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => equal((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))\n )\n}\n\nasync function load(tool: AnyTool, file: string) {\n const disk = await readDisk(file)\n if (!disk) return { value: structuredClone(tool.initial), hash: null }\n const parsed = validateConfig(tool, JSON.parse(disk.content))\n if (!parsed.ok)\n throw new Error(`[studio] ${path.basename(file)} is not a valid \"${tool.id}\" config: ${parsed.message}`)\n return { value: parsed.data, hash: disk.hash }\n}\n\n/**\n * The single authority over tool configs: validates every write, owns draft\n * revisions, and is the only thing that touches the persisted files.\n */\nexport async function createRuntime({ root, tools }: { root: string; tools: readonly AnyTool[] }): Promise<Runtime> {\n root = path.resolve(root)\n const entries = new Map<string, Entry>()\n const files = new Set<string>()\n\n for (const tool of tools) {\n if (entries.has(tool.id)) throw new Error(`[studio] duplicate tool id \"${tool.id}\"`)\n const file = await resolveToolFile(root, tool.file)\n if (files.has(file)) throw new Error(`[studio] tool \"${tool.id}\" shares its file with another tool`)\n files.add(file)\n const saved = await load(tool, file)\n entries.set(tool.id, {\n tool,\n file,\n queue: Promise.resolve(),\n state: {\n tool: tool.id,\n draft: { value: structuredClone(saved.value), revision: 0 },\n saved,\n dirty: false,\n conflict: false,\n },\n })\n }\n\n const listeners = new Set<RuntimeListener>()\n const commit = (entry: Entry, next: Partial<ToolState>, origin?: unknown) => {\n const state = { ...entry.state, ...next }\n state.dirty = !equal(state.draft.value, state.saved.value)\n if (!state.dirty) state.conflict = false\n entry.state = state\n for (const listener of listeners) listener(state, origin)\n return state\n }\n const serial = <T>(entry: Entry, task: () => Promise<T>) => {\n const run = entry.queue.then(task)\n entry.queue = run.catch(() => {})\n return run\n }\n\n const handlers: { [TOp in Op]: (params: any, origin: unknown) => Promise<Result<Operations[TOp]['result']>> } = {\n 'tools.list': async () =>\n success(\n [...entries.values()].map(({ tool }): ToolSummary => ({\n id: tool.id,\n title: tool.title ?? tool.id,\n file: tool.file,\n icon: tool.icon,\n preview: tool.preview,\n })),\n ),\n\n 'tool.get': async ({ tool }) => withEntry(tool, async (entry) => success(entry.state)),\n\n 'tool.update': async ({ tool, value, revision }, origin) =>\n withEntry(tool, async (entry) => {\n if (revision !== undefined && revision !== entry.state.draft.revision)\n return error('stale_revision', `draft is at revision ${entry.state.draft.revision}, not ${revision}`)\n const parsed = validateConfig(entry.tool, value)\n if (!parsed.ok) return parsed\n const draft = { value: parsed.data, revision: entry.state.draft.revision + 1 }\n return success(commit(entry, { draft }, origin))\n }),\n\n 'tool.save': async ({ tool, revision, force = false }, origin) =>\n withEntry(tool, (entry) =>\n serial(entry, async () => {\n const { draft, saved } = entry.state\n if (revision !== draft.revision)\n return error('stale_revision', `draft is at revision ${draft.revision}, not ${revision}`)\n const disk = await readDisk(entry.file)\n if (!force && (entry.state.conflict || (disk?.hash ?? null) !== saved.hash)) {\n commit(entry, { conflict: true })\n return error('conflict', `${entry.tool.file} changed on disk; discard to reload or save with force`)\n }\n const content = serialize(draft.value)\n await writeAtomic(entry.file, content)\n return success(\n commit(entry, { saved: { value: structuredClone(draft.value), hash: hashOf(content) } }, origin),\n )\n }),\n ),\n\n 'tool.discard': async ({ tool, revision }, origin) =>\n withEntry(tool, async (entry) => {\n const { saved, draft } = entry.state\n if (revision !== undefined && revision !== draft.revision)\n return error('stale_revision', `draft is at revision ${draft.revision}, not ${revision}`)\n return success(\n commit(entry, { draft: { value: structuredClone(saved.value), revision: draft.revision + 1 } }, origin),\n )\n }),\n }\n\n async function withEntry<T>(id: unknown, task: (entry: Entry) => Promise<Result<T>>): Promise<Result<T>> {\n const entry = typeof id === 'string' ? entries.get(id) : undefined\n return entry ? task(entry) : error('unknown_tool', `no tool \"${String(id)}\"`)\n }\n\n /** Pick up edits made outside Studio (editor, git checkout, an agent). */\n const reload = (entry: Entry) =>\n serial(entry, async () => {\n const disk = await readDisk(entry.file)\n const { saved, draft, dirty } = entry.state\n if ((disk?.hash ?? null) === saved.hash) return\n if (!disk) return commit(entry, { saved: { ...saved, hash: null } })\n let value: unknown\n try {\n value = JSON.parse(disk.content)\n } catch {\n return console.warn(`[studio] ignoring unparsable ${entry.tool.file}`)\n }\n const parsed = validateConfig(entry.tool, value)\n if (!parsed.ok) return console.warn(`[studio] ignoring invalid ${entry.tool.file}: ${parsed.message}`)\n const next: Partial<ToolState> = { saved: { value: parsed.data, hash: disk.hash } }\n if (dirty) next.conflict = true\n else next.draft = { value: structuredClone(parsed.data), revision: draft.revision + 1 }\n commit(entry, next)\n })\n\n const timers = new Map<Entry, NodeJS.Timeout>()\n const watchers: FSWatcher[] = []\n const byDirectory = Map.groupBy(entries.values(), (entry) => path.dirname(entry.file))\n for (const [directory, group] of byDirectory) {\n try {\n const watcher = watch(directory, (_, filename) => {\n for (const entry of group) {\n if (filename && filename !== path.basename(entry.file)) continue\n clearTimeout(timers.get(entry))\n timers.set(\n entry,\n setTimeout(() => void reload(entry).catch((cause) => console.error('[studio] reload failed', cause)), 50),\n )\n }\n })\n watchers.push(watcher)\n } catch {\n console.warn(`[studio] not watching ${path.relative(root, directory)} (it does not exist yet)`)\n }\n }\n\n return {\n root,\n run: (op, params, origin) => handlers[op](params, origin) as never,\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n close() {\n for (const watcher of watchers) watcher.close()\n for (const timer of timers.values()) clearTimeout(timer)\n listeners.clear()\n },\n }\n}\n","export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: DebugSchema, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n","import { createHash, randomUUID } from 'node:crypto'\nimport { mkdir, readFile, realpath, rename, rm, stat } from 'node:fs/promises'\nimport { writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nconst inside = (root: string, target: string) => {\n const relative = path.relative(root, target)\n return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)\n}\n\nconst exists = (target: string) =>\n stat(target).then(\n () => true,\n () => false,\n )\n\n/**\n * Resolve a tool's persistence target, refusing anything that is not a `.json`\n * file inside `root` — including paths that escape through a symlink.\n */\nexport async function resolveToolFile(root: string, file: string): Promise<string> {\n const fail = (reason: string) => new Error(`[studio] tool file \"${file}\" ${reason}`)\n if (path.isAbsolute(file)) throw fail('must be relative to the project root')\n if (path.extname(file) !== '.json') throw fail('must be a .json file')\n const absolute = path.resolve(root, file)\n if (!inside(root, absolute)) throw fail('escapes the project root')\n\n let probe = absolute\n while (!(await exists(probe))) probe = path.dirname(probe)\n if (probe !== root && !inside(await realpath(root), await realpath(probe))) throw fail('escapes the project root')\n return absolute\n}\n\nexport const hashOf = (content: string) => createHash('sha256').update(content).digest('hex').slice(0, 16)\n\nexport const serialize = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\n\n/** The file's content and hash, or null when it does not exist. */\nexport async function readDisk(file: string): Promise<{ content: string; hash: string } | null> {\n try {\n const content = await readFile(file, 'utf8')\n return { content, hash: hashOf(content) }\n } catch (cause) {\n if ((cause as NodeJS.ErrnoException).code === 'ENOENT') return null\n throw cause\n }\n}\n\n/** Write through a sibling temp file + rename so readers never see a half-written config. */\nexport async function writeAtomic(file: string, content: string) {\n await mkdir(path.dirname(file), { recursive: true })\n const temp = `${file}.${randomUUID()}.tmp`\n try {\n await writeFile(temp, content, 'utf8')\n await rename(temp, file)\n } catch (cause) {\n await rm(temp, { force: true })\n throw cause\n }\n}\n","import { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { localhostHostValidation, toNodeHandler } from '@modelcontextprotocol/node'\nimport { createMcpHandler } from '@modelcontextprotocol/server'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { Op, Request, ServerMessage } from '../core/protocol'\nimport { error } from '../core/result'\nimport { createPreviewCapture } from './capture'\nimport { createMcpServer } from './mcp'\nimport type { Runtime } from './runtime'\n\nconst LOCAL_ORIGIN = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d+)?$/\nconst OPS = new Set<string>(['tools.list', 'tool.get', 'tool.update', 'tool.save', 'tool.discard'] satisfies Op[])\n\nexport type BridgeOptions = {\n port: number\n host?: string\n /** Browser origins allowed to connect. Defaults to localhost. Non-browser clients send no origin. */\n origins?: readonly string[]\n /** Running app URL used to resolve registered preview routes for MCP capture. */\n appUrl?: string\n}\n\nexport type Bridge = { port: number; close(): Promise<void> }\n\nfunction parse(raw: string): Request | null {\n try {\n const message = JSON.parse(raw)\n if (typeof message?.id !== 'number' || !OPS.has(message.op)) return null\n return { id: message.id, op: message.op, params: message.params ?? {} }\n } catch {\n return null\n }\n}\n\n/** Share one runtime between the browser WebSocket bridge and the /mcp HTTP endpoint. */\nexport function serveBridge(\n runtime: Runtime,\n { port, host = '127.0.0.1', origins, appUrl }: BridgeOptions,\n): Promise<Bridge> {\n const allowed = (origin: string | undefined) =>\n origin === undefined || (origins ? origins.includes(origin) : LOCAL_ORIGIN.test(origin))\n\n const onerror = (cause: Error) => console.error('[studio] MCP request failed', cause)\n const previews = createPreviewCapture(runtime, { appUrl, runtimePort: () => (http.address() as AddressInfo).port })\n const mcp = createMcpHandler(() => createMcpServer(runtime, previews), { onerror })\n const handleMcp = toNodeHandler(mcp, { onerror })\n const validateHost = localhostHostValidation()\n const http = createServer((request, response) => {\n if (request.url?.split('?')[0] !== '/mcp') {\n response.writeHead(404).end()\n return\n }\n if (!validateHost(request, response)) return\n if (!allowed(request.headers.origin)) {\n response.writeHead(403).end('Origin not allowed')\n return\n }\n void handleMcp(request, response)\n })\n const server = new WebSocketServer({\n server: http,\n verifyClient: ({ origin }: { origin?: string }) => allowed(origin),\n })\n const send = (socket: WebSocket, message: ServerMessage) => {\n if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(message))\n }\n\n server.on('connection', (socket) => {\n socket.on('message', async (raw) => {\n const request = parse(raw.toString())\n if (!request) return send(socket, { type: 'response', id: -1, result: error('bad_request') })\n const result = await runtime\n .run(request.op, request.params, socket)\n .catch((cause: unknown) => error('internal', cause instanceof Error ? cause.message : String(cause)))\n send(socket, { type: 'response', id: request.id, result })\n })\n })\n\n const unsubscribe = runtime.subscribe((state, origin) => {\n for (const client of server.clients) if (client !== origin) send(client, { type: 'state', state })\n })\n\n return new Promise((resolve, reject) => {\n server.once('error', (cause) => {\n unsubscribe()\n server.close()\n void mcp.close()\n void previews.close()\n reject(cause)\n })\n http.once('listening', () =>\n resolve({\n port: (http.address() as AddressInfo).port,\n close: async () => {\n unsubscribe()\n await previews.close()\n for (const client of server.clients) client.terminate()\n await mcp.close()\n await new Promise<void>((done) => server.close(() => done()))\n await new Promise<void>((done) => {\n http.close(() => done())\n http.closeAllConnections()\n })\n },\n }),\n )\n http.listen(port, host)\n })\n}\n","import type { Browser, BrowserContext } from 'playwright'\nimport type { Studio } from '../client/studio'\nimport { error, success, type Result } from '../core/result'\nimport type { Runtime } from './runtime'\n\nexport type CaptureParams = { tool: string; revision?: number; width?: number; height?: number }\ntype CaptureImage = {\n tool: string\n revision: number\n url: string\n width: number\n height: number\n settling: 'automatic'\n png: Buffer\n}\n\n/** Runs in the preview page. Reuse createStudio's HMR registry; tools need no capture integration. */\nfunction previewSnapshot({ port, tool, revision }: { port: number; tool: string; revision: number }) {\n const instances = (globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio\n for (const [address, studio] of instances ?? []) {\n const url = new URL(address)\n if (!['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) || Number(url.port) !== port) continue\n const handle = studio.tools.find((handle) => handle.definition.id === tool)\n if (studio.connection.phase !== 'connected' || !handle?.state || handle.comparing) continue\n if (handle.state.draft.revision < revision) continue\n return { revision: handle.state.draft.revision, version: handle.version }\n }\n return null\n}\n\n/** One lazy browser per bridge, one isolated page per capture. Never writes a draft or a file. */\nexport function createPreviewCapture(\n runtime: Runtime,\n { appUrl, runtimePort, timeoutMs = 20_000 }: { appUrl?: string; runtimePort: () => number; timeoutMs?: number },\n) {\n let browser: Promise<Browser> | undefined\n let active = false\n let closed = false\n\n const capture = async ({\n tool,\n revision,\n width = 1200,\n height = 800,\n }: CaptureParams): Promise<Result<CaptureImage>> => {\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n if (active) return error('capture_busy', 'Another preview is being captured; retry when it finishes')\n const state = await runtime.run('tool.get', { tool })\n if (!state.ok) return state\n revision ??= state.data.draft.revision\n if (revision !== state.data.draft.revision) return error('stale_revision', 'Read the latest draft before capturing')\n const tools = await runtime.run('tools.list', {})\n if (!tools.ok) return tools\n const preview = tools.data.find((entry) => entry.id === tool)?.preview\n if (!preview) return error('preview_missing', `Tool \"${tool}\" has no preview route`)\n if (!appUrl) return error('app_url_missing', 'Set appUrl in studio.config.ts or pass studio dev --app-url <url>')\n let url: URL\n try {\n const base = new URL(appUrl)\n url = new URL(preview, base)\n if (!['http:', 'https:'].includes(base.protocol) || url.origin !== base.origin || url.username || url.password)\n return error('invalid_preview_url', 'The preview must be an HTTP(S) route on the configured app origin')\n } catch {\n return error('invalid_preview_url', 'appUrl must be an absolute HTTP(S) URL and preview must be a valid route')\n }\n\n // Recheck after the asynchronous runtime reads so concurrent MCP callers cannot race this lock.\n if (active) return error('capture_busy', 'Another preview is being captured; retry when it finishes')\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n active = true\n let context: BrowserContext | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n let timedOut = false\n let phase = 'starting the browser'\n try {\n browser ??= import('playwright').then(({ chromium }) => chromium.launch({ headless: true, timeout: 10_000 }))\n let instance: Browser\n try {\n instance = await browser\n } catch (cause) {\n browser = undefined\n return error('capture_unavailable', `Run \"studio install-browser\" to install Chromium. ${String(cause)}`)\n }\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n if (!instance.isConnected()) {\n browser = undefined\n return error('capture_unavailable', 'The capture browser disconnected; retry to start it again')\n }\n context = await instance.newContext({ viewport: { width, height }, deviceScaleFactor: 1 })\n timer = setTimeout(() => {\n timedOut = true\n void context?.close().catch(() => {})\n }, timeoutMs)\n const page = await context.newPage()\n page.setDefaultTimeout(timeoutMs)\n // Preview routes may load external assets, but must not navigate to a different app.\n await page.route('**/*', (route) => {\n const request = route.request()\n return request.isNavigationRequest() &&\n request.frame() === page.mainFrame() &&\n new URL(request.url()).origin !== url.origin\n ? route.abort()\n : route.continue()\n })\n phase = 'opening the preview'\n const response = await page.goto(url.href, { waitUntil: 'load' })\n if (!response?.ok()) return error('preview_failed', `Preview returned HTTP ${response?.status() ?? 'unknown'}`)\n\n phase = 'waiting for the preview to receive the draft'\n const target = { port: runtimePort(), tool, revision }\n const synced = await page.waitForFunction(previewSnapshot, target)\n const initial = await synced.jsonValue()\n await synced.dispose()\n if (initial?.revision !== revision) return error('stale_revision', 'The draft changed while opening the preview')\n\n phase = 'waiting for fonts, images, and animation frames'\n await page.evaluate(async () => {\n await document.fonts.ready\n await Promise.all(Array.from(document.images, (image) => image.decode().catch(() => {})))\n await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))\n })\n const before = await page.evaluate(previewSnapshot, target)\n if (before?.revision !== revision) return error('stale_revision', 'The draft changed while settling the preview')\n\n phase = 'taking the screenshot'\n const png = await page.screenshot({ type: 'png', animations: 'disabled', timeout: timeoutMs })\n const after = await page.evaluate(previewSnapshot, target)\n const latest = await runtime.run('tool.get', { tool })\n if (!latest.ok) return latest\n if (latest.data.draft.revision !== revision || after?.revision !== revision || after.version !== before.version)\n return error('stale_revision', 'The preview changed during capture; read the latest draft and retry')\n return success({ tool, revision, url: page.url(), width, height, settling: 'automatic', png })\n } catch (cause) {\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n return error(\n timedOut || (cause instanceof Error && cause.name === 'TimeoutError') ? 'preview_not_ready' : 'capture_failed',\n `${phase}: ${String(cause)}`,\n )\n } finally {\n clearTimeout(timer)\n await context?.close().catch(() => {})\n active = false\n }\n }\n\n return {\n capture,\n async close() {\n closed = true\n await browser?.then((instance) => instance.close()).catch(() => {})\n browser = undefined\n },\n }\n}\n\nexport type PreviewCapture = ReturnType<typeof createPreviewCapture>\n","import { McpServer, type CallToolResult } from '@modelcontextprotocol/server'\nimport { z } from 'zod/v4'\nimport type { Op, Operations } from '../core/protocol'\nimport { success } from '../core/result'\nimport type { PreviewCapture } from './capture'\nimport type { Runtime } from './runtime'\n\nconst tool = z.string().min(1).describe('Registered Studio tool ID from studio_list_tools.')\nconst revision = z.number().int().nonnegative().describe('Draft revision returned by the latest read or update.')\n\n/** MCP is another caller of the live runtime; it never owns a second draft or writes files itself. */\nexport function createMcpServer(runtime: Runtime, previews: PreviewCapture) {\n const server = new McpServer(\n { name: 'joyco-studio', version: '0.0.0' },\n {\n instructions:\n 'List Studio tools, read the selected tool, then update its complete draft value. Updates appear live in the playground. Saving is a separate operation. If a revision is stale, read again and reconcile your changes before retrying.',\n },\n )\n const run = async <T extends Op>(op: T, params: Operations[T]['params']): Promise<CallToolResult> => {\n const result = await runtime.run(op, params)\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }],\n structuredContent: result,\n isError: !result.ok,\n }\n }\n\n server.registerTool(\n 'studio_list_tools',\n {\n description: 'List registered Studio tools with their IDs, titles, config files, and preview routes.',\n inputSchema: z.object({}),\n annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },\n },\n () => run('tools.list', {}),\n )\n server.registerTool(\n 'studio_get_tool',\n {\n description: 'Read a tool’s complete draft, revision, saved value, and dirty/conflict status before editing.',\n inputSchema: z.object({ tool }),\n annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },\n },\n (params) => run('tool.get', params),\n )\n server.registerTool(\n 'studio_update_tool',\n {\n description:\n 'Replace the complete draft config and update all connected previews. Read first and preserve fields you are not changing. The runtime validates types, slider bounds, and select options. Does not save to disk; rejects a stale revision.',\n inputSchema: z.object({ tool, revision, value: z.record(z.string(), z.unknown()) }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n },\n (params) => run('tool.update', params),\n )\n server.registerTool(\n 'studio_save_tool',\n {\n description:\n 'Persist a draft to its registered JSON file. Pass the latest draft revision. Rejects stale revisions and changes made on disk; force explicitly overwrites a disk conflict.',\n inputSchema: z.object({ tool, revision, force: z.boolean().optional() }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },\n },\n (params) => run('tool.save', params),\n )\n server.registerTool(\n 'studio_discard_tool',\n {\n description:\n 'Discard unsaved edits and restore the runtime’s latest saved value in every preview. Read first; rejects a stale revision.',\n inputSchema: z.object({ tool, revision }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n },\n (params) => run('tool.discard', params),\n )\n server.registerTool(\n 'studio_capture_preview',\n {\n description:\n 'Capture a registered preview as a PNG in a fresh browser page using the live draft. Requires appUrl and Chromium (studio install-browser). Defaults to the current revision and 1200×800. Waits for draft sync, fonts, images, and two animation frames; automatic settling does not guarantee arbitrary async scenes are finished. Rejects a draft change during capture. Does not share your open tab’s scroll, login, or interaction state.',\n inputSchema: z.object({\n tool,\n revision: revision.optional(),\n width: z.number().int().min(320).max(2560).optional(),\n height: z.number().int().min(240).max(1600).optional(),\n }),\n annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true },\n },\n async (params): Promise<CallToolResult> => {\n const captured = await previews.capture(params)\n if (!captured.ok)\n return {\n content: [{ type: 'text', text: JSON.stringify(captured) }],\n structuredContent: captured,\n isError: true,\n }\n const { png, ...metadata } = captured.data\n const result = success(metadata)\n return {\n content: [\n { type: 'text', text: JSON.stringify(result) },\n { type: 'image', mimeType: 'image/png', data: png.toString('base64') },\n ],\n structuredContent: result,\n isError: false,\n }\n },\n )\n return server\n}\n","import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport { createJiti } from 'jiti'\nimport type { StudioConfig } from '../core/tool'\n\nconst CANDIDATES = ['studio.config.ts', 'studio.config.mts', 'studio.config.js', 'studio.config.mjs']\n\n/** Load `studio.config.*` (TypeScript included). Its directory is the project root. */\nexport async function loadConfig(cwd: string, explicit?: string) {\n const file = explicit\n ? path.resolve(cwd, explicit)\n : CANDIDATES.map((name) => path.join(cwd, name)).find((candidate) => existsSync(candidate))\n if (!file || !existsSync(file)) throw new Error(`[studio] no ${explicit ?? 'studio.config.ts'} found in ${cwd}`)\n const jiti = createJiti(import.meta.url, { moduleCache: false })\n const config = await jiti.import<StudioConfig>(file, { default: true })\n if (!Array.isArray(config?.tools))\n throw new Error(`[studio] ${path.basename(file)} must default-export defineConfig({ tools })`)\n return { config, root: path.dirname(file) }\n}\n"],"mappings":";;;AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,OAAOA,WAAU;AACjB,SAAS,iBAAiB;;;ACqDnB,IAAM,eAAe;;;ACxD5B,SAAS,aAA6B;AACtC,OAAOC,WAAU;;;ACUV,IAAM,UAAU,CAAI,UAAyB,EAAE,IAAI,MAAM,KAAK;AAE9D,IAAM,QAAQ,CAAC,MAAc,aAA+B;AAAA,EACjE,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,GAAI,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ;AAC7C;;;ACRO,SAAS,eACdC,OACA,WACiB;AACjB,QAAM,WAAW,cAAcA,MAAK,SAAS,WAAW,QAAQ;AAChE,MAAI,SAAU,QAAO,MAAM,kBAAkB,QAAQ;AACrD,QAAM,SAAS,gBAAgB,SAAS;AACxC,QAAM,UAAU,eAAeA,MAAK,SAAS,MAAM,CAAC;AACpD,SAAO,UAAU,MAAM,kBAAkB,OAAO,IAAI,QAAQ,MAAM;AACpE;AAEA,SAAS,KAAK,OAAgB;AAC5B,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,SAAO,OAAO;AAChB;AAEA,SAAS,cAAc,WAAoB,OAAgBC,OAA6B;AACtF,QAAM,WAAW,KAAK,SAAS;AAC/B,MAAI,KAAK,KAAK,MAAM,SAAU,QAAO,GAAGA,KAAI,cAAc,QAAQ,SAAS,KAAK,KAAK,CAAC;AACtF,MAAI,aAAa,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,GAAGA,KAAI;AACpE,MAAI,aAAa,SAAS;AACxB,UAAM,CAAC,KAAK,IAAI;AAChB,QAAI,UAAU,OAAW,QAAO;AAChC,eAAW,CAAC,OAAO,IAAI,KAAM,MAAoB,QAAQ,GAAG;AAC1D,YAAM,SAAS,cAAc,OAAO,MAAM,GAAGA,KAAI,IAAI,KAAK,GAAG;AAC7D,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EACF;AACA,MAAI,aAAa,UAAU;AACzB,UAAM,MAAM;AACZ,UAAM,MAAM;AACZ,eAAW,OAAO,OAAO,KAAK,GAAG,EAAG,KAAI,EAAE,OAAO,KAAM,QAAO,GAAGA,KAAI,IAAI,GAAG;AAC5E,eAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,UAAI,EAAE,OAAO,KAAM,QAAO,GAAGA,KAAI,IAAI,GAAG;AACxC,YAAM,SAAS,cAAc,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,GAAGA,KAAI,IAAI,GAAG,EAAE;AACjE,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,QAAQ,CAAC,UACb,OAAO,UAAU,YACjB,UAAU,QACV,CAAC,KAAK,KAAK,GAAG,EAAE,MAAM,CAAC,YAAY,OAAO,SAAU,MAAkC,OAAO,CAAC,CAAC;AAEjG,SAAS,eAAe,QAAqBA,QAAO,IAAmB;AACrE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,MAAM,SAAS,SAAU;AAC7B,UAAM,OAAO,GAAGA,KAAI,GAAG,MAAM,SAAS,GAAG;AACzC,QAAI,MAAM,SAAS,UAAU;AAC3B,YAAM,SAAS,eAAe,MAAM,WAAW,OAAO,GAAG,GAAG,IAAI,GAAG;AACnE,UAAI,OAAQ,QAAO;AACnB;AAAA,IACF;AACA,UAAM,QAAiB,MAAM,KAAK;AAClC,QAAI,MAAM,SAAS,UAAU;AAC3B,UAAI,OAAO,UAAU,YAAY,QAAQ,MAAM,OAAO,QAAQ,MAAM;AAClE,eAAO,GAAG,IAAI,2BAA2B,MAAM,GAAG,KAAK,MAAM,GAAG,UAAU,KAAK,UAAU,KAAK,CAAC;AAAA,IACnG,WAAW,MAAM,SAAS,QAAQ;AAChC,UAAI,OAAO,UAAU,UAAW,QAAO,GAAG,IAAI;AAAA,IAChD,WAAW,MAAM,SAAS,UAAU;AAClC,YAAM,UAAU,OAAO,OAAO,MAAM,OAAO;AAC3C,UAAI,CAAC,QAAQ,SAAS,KAAwB;AAC5C,eAAO,GAAG,IAAI,qBAAqB,KAAK,UAAU,OAAO,CAAC,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA,IAC5F,WAAW,MAAM,SAAS,gBAAgB;AACxC,UAAI,OAAO,UAAU,SAAU,QAAO,GAAG,IAAI;AAAA,IAC/C,WAAW,MAAM,SAAS,SAAS;AACjC,UAAI,CAAC,MAAM,KAAK,EAAG,QAAO,GAAG,IAAI;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AACT;;;AClFA,SAAS,YAAY,kBAAkB;AACvC,SAAS,OAAO,UAAU,UAAU,QAAQ,IAAI,YAAY;AAC5D,SAAS,iBAAiB;AAC1B,OAAO,UAAU;AAEjB,IAAM,SAAS,CAAC,MAAc,WAAmB;AAC/C,QAAM,WAAW,KAAK,SAAS,MAAM,MAAM;AAC3C,SAAO,aAAa,MAAM,CAAC,SAAS,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,QAAQ;AACnF;AAEA,IAAM,SAAS,CAAC,WACd,KAAK,MAAM,EAAE;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AACR;AAMF,eAAsB,gBAAgB,MAAc,MAA+B;AACjF,QAAM,OAAO,CAAC,WAAmB,IAAI,MAAM,uBAAuB,IAAI,KAAK,MAAM,EAAE;AACnF,MAAI,KAAK,WAAW,IAAI,EAAG,OAAM,KAAK,sCAAsC;AAC5E,MAAI,KAAK,QAAQ,IAAI,MAAM,QAAS,OAAM,KAAK,sBAAsB;AACrE,QAAM,WAAW,KAAK,QAAQ,MAAM,IAAI;AACxC,MAAI,CAAC,OAAO,MAAM,QAAQ,EAAG,OAAM,KAAK,0BAA0B;AAElE,MAAI,QAAQ;AACZ,SAAO,CAAE,MAAM,OAAO,KAAK,EAAI,SAAQ,KAAK,QAAQ,KAAK;AACzD,MAAI,UAAU,QAAQ,CAAC,OAAO,MAAM,SAAS,IAAI,GAAG,MAAM,SAAS,KAAK,CAAC,EAAG,OAAM,KAAK,0BAA0B;AACjH,SAAO;AACT;AAEO,IAAM,SAAS,CAAC,YAAoB,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAElG,IAAM,YAAY,CAAC,UAAmB,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA;AAG9E,eAAsB,SAAS,MAAiE;AAC9F,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,MAAM,MAAM;AAC3C,WAAO,EAAE,SAAS,MAAM,OAAO,OAAO,EAAE;AAAA,EAC1C,SAAS,OAAO;AACd,QAAK,MAAgC,SAAS,SAAU,QAAO;AAC/D,UAAM;AAAA,EACR;AACF;AAGA,eAAsB,YAAY,MAAc,SAAiB;AAC/D,QAAM,MAAM,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,QAAM,OAAO,GAAG,IAAI,IAAI,WAAW,CAAC;AACpC,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,MAAM;AACrC,UAAM,OAAO,MAAM,IAAI;AAAA,EACzB,SAAS,OAAO;AACd,UAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAC9B,UAAM;AAAA,EACR;AACF;;;AH9BA,IAAM,QAAQ,CAAC,GAAY,MAAwB;AACjD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,MAAM,QAAQ,MAAM,KAAM,QAAO;AACvF,QAAM,OAAO,OAAO,KAAK,CAAC;AAC1B,SACE,KAAK,WAAW,OAAO,KAAK,CAAC,EAAE,UAC/B,KAAK,MAAM,CAAC,QAAQ,MAAO,EAA8B,GAAG,GAAI,EAA8B,GAAG,CAAC,CAAC;AAEvG;AAEA,eAAe,KAAKC,OAAe,MAAc;AAC/C,QAAM,OAAO,MAAM,SAAS,IAAI;AAChC,MAAI,CAAC,KAAM,QAAO,EAAE,OAAO,gBAAgBA,MAAK,OAAO,GAAG,MAAM,KAAK;AACrE,QAAM,SAAS,eAAeA,OAAM,KAAK,MAAM,KAAK,OAAO,CAAC;AAC5D,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,YAAYC,MAAK,SAAS,IAAI,CAAC,oBAAoBD,MAAK,EAAE,aAAa,OAAO,OAAO,EAAE;AACzG,SAAO,EAAE,OAAO,OAAO,MAAM,MAAM,KAAK,KAAK;AAC/C;AAMA,eAAsB,cAAc,EAAE,MAAM,MAAM,GAAkE;AAClH,SAAOC,MAAK,QAAQ,IAAI;AACxB,QAAM,UAAU,oBAAI,IAAmB;AACvC,QAAM,QAAQ,oBAAI,IAAY;AAE9B,aAAWD,SAAQ,OAAO;AACxB,QAAI,QAAQ,IAAIA,MAAK,EAAE,EAAG,OAAM,IAAI,MAAM,+BAA+BA,MAAK,EAAE,GAAG;AACnF,UAAM,OAAO,MAAM,gBAAgB,MAAMA,MAAK,IAAI;AAClD,QAAI,MAAM,IAAI,IAAI,EAAG,OAAM,IAAI,MAAM,kBAAkBA,MAAK,EAAE,qCAAqC;AACnG,UAAM,IAAI,IAAI;AACd,UAAM,QAAQ,MAAM,KAAKA,OAAM,IAAI;AACnC,YAAQ,IAAIA,MAAK,IAAI;AAAA,MACnB,MAAAA;AAAA,MACA;AAAA,MACA,OAAO,QAAQ,QAAQ;AAAA,MACvB,OAAO;AAAA,QACL,MAAMA,MAAK;AAAA,QACX,OAAO,EAAE,OAAO,gBAAgB,MAAM,KAAK,GAAG,UAAU,EAAE;AAAA,QAC1D;AAAA,QACA,OAAO;AAAA,QACP,UAAU;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,oBAAI,IAAqB;AAC3C,QAAM,SAAS,CAAC,OAAc,MAA0B,WAAqB;AAC3E,UAAM,QAAQ,EAAE,GAAG,MAAM,OAAO,GAAG,KAAK;AACxC,UAAM,QAAQ,CAAC,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,KAAK;AACzD,QAAI,CAAC,MAAM,MAAO,OAAM,WAAW;AACnC,UAAM,QAAQ;AACd,eAAW,YAAY,UAAW,UAAS,OAAO,MAAM;AACxD,WAAO;AAAA,EACT;AACA,QAAM,SAAS,CAAI,OAAc,SAA2B;AAC1D,UAAM,MAAM,MAAM,MAAM,KAAK,IAAI;AACjC,UAAM,QAAQ,IAAI,MAAM,MAAM;AAAA,IAAC,CAAC;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,WAA0G;AAAA,IAC9G,cAAc,YACZ;AAAA,MACE,CAAC,GAAG,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,MAAAA,MAAK,OAAoB;AAAA,QACpD,IAAIA,MAAK;AAAA,QACT,OAAOA,MAAK,SAASA,MAAK;AAAA,QAC1B,MAAMA,MAAK;AAAA,QACX,MAAMA,MAAK;AAAA,QACX,SAASA,MAAK;AAAA,MAChB,EAAE;AAAA,IACJ;AAAA,IAEF,YAAY,OAAO,EAAE,MAAAA,MAAK,MAAM,UAAUA,OAAM,OAAO,UAAU,QAAQ,MAAM,KAAK,CAAC;AAAA,IAErF,eAAe,OAAO,EAAE,MAAAA,OAAM,OAAO,UAAAE,UAAS,GAAG,WAC/C,UAAUF,OAAM,OAAO,UAAU;AAC/B,UAAIE,cAAa,UAAaA,cAAa,MAAM,MAAM,MAAM;AAC3D,eAAO,MAAM,kBAAkB,wBAAwB,MAAM,MAAM,MAAM,QAAQ,SAASA,SAAQ,EAAE;AACtG,YAAM,SAAS,eAAe,MAAM,MAAM,KAAK;AAC/C,UAAI,CAAC,OAAO,GAAI,QAAO;AACvB,YAAM,QAAQ,EAAE,OAAO,OAAO,MAAM,UAAU,MAAM,MAAM,MAAM,WAAW,EAAE;AAC7E,aAAO,QAAQ,OAAO,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;AAAA,IACjD,CAAC;AAAA,IAEH,aAAa,OAAO,EAAE,MAAAF,OAAM,UAAAE,WAAU,QAAQ,MAAM,GAAG,WACrD;AAAA,MAAUF;AAAA,MAAM,CAAC,UACf,OAAO,OAAO,YAAY;AACxB,cAAM,EAAE,OAAO,MAAM,IAAI,MAAM;AAC/B,YAAIE,cAAa,MAAM;AACrB,iBAAO,MAAM,kBAAkB,wBAAwB,MAAM,QAAQ,SAASA,SAAQ,EAAE;AAC1F,cAAM,OAAO,MAAM,SAAS,MAAM,IAAI;AACtC,YAAI,CAAC,UAAU,MAAM,MAAM,aAAa,MAAM,QAAQ,UAAU,MAAM,OAAO;AAC3E,iBAAO,OAAO,EAAE,UAAU,KAAK,CAAC;AAChC,iBAAO,MAAM,YAAY,GAAG,MAAM,KAAK,IAAI,wDAAwD;AAAA,QACrG;AACA,cAAM,UAAU,UAAU,MAAM,KAAK;AACrC,cAAM,YAAY,MAAM,MAAM,OAAO;AACrC,eAAO;AAAA,UACL,OAAO,OAAO,EAAE,OAAO,EAAE,OAAO,gBAAgB,MAAM,KAAK,GAAG,MAAM,OAAO,OAAO,EAAE,EAAE,GAAG,MAAM;AAAA,QACjG;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEF,gBAAgB,OAAO,EAAE,MAAAF,OAAM,UAAAE,UAAS,GAAG,WACzC,UAAUF,OAAM,OAAO,UAAU;AAC/B,YAAM,EAAE,OAAO,MAAM,IAAI,MAAM;AAC/B,UAAIE,cAAa,UAAaA,cAAa,MAAM;AAC/C,eAAO,MAAM,kBAAkB,wBAAwB,MAAM,QAAQ,SAASA,SAAQ,EAAE;AAC1F,aAAO;AAAA,QACL,OAAO,OAAO,EAAE,OAAO,EAAE,OAAO,gBAAgB,MAAM,KAAK,GAAG,UAAU,MAAM,WAAW,EAAE,EAAE,GAAG,MAAM;AAAA,MACxG;AAAA,IACF,CAAC;AAAA,EACL;AAEA,iBAAe,UAAa,IAAa,MAAgE;AACvG,UAAM,QAAQ,OAAO,OAAO,WAAW,QAAQ,IAAI,EAAE,IAAI;AACzD,WAAO,QAAQ,KAAK,KAAK,IAAI,MAAM,gBAAgB,YAAY,OAAO,EAAE,CAAC,GAAG;AAAA,EAC9E;AAGA,QAAM,SAAS,CAAC,UACd,OAAO,OAAO,YAAY;AACxB,UAAM,OAAO,MAAM,SAAS,MAAM,IAAI;AACtC,UAAM,EAAE,OAAO,OAAO,MAAM,IAAI,MAAM;AACtC,SAAK,MAAM,QAAQ,UAAU,MAAM,KAAM;AACzC,QAAI,CAAC,KAAM,QAAO,OAAO,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,MAAM,KAAK,EAAE,CAAC;AACnE,QAAI;AACJ,QAAI;AACF,cAAQ,KAAK,MAAM,KAAK,OAAO;AAAA,IACjC,QAAQ;AACN,aAAO,QAAQ,KAAK,gCAAgC,MAAM,KAAK,IAAI,EAAE;AAAA,IACvE;AACA,UAAM,SAAS,eAAe,MAAM,MAAM,KAAK;AAC/C,QAAI,CAAC,OAAO,GAAI,QAAO,QAAQ,KAAK,6BAA6B,MAAM,KAAK,IAAI,KAAK,OAAO,OAAO,EAAE;AACrG,UAAM,OAA2B,EAAE,OAAO,EAAE,OAAO,OAAO,MAAM,MAAM,KAAK,KAAK,EAAE;AAClF,QAAI,MAAO,MAAK,WAAW;AAAA,QACtB,MAAK,QAAQ,EAAE,OAAO,gBAAgB,OAAO,IAAI,GAAG,UAAU,MAAM,WAAW,EAAE;AACtF,WAAO,OAAO,IAAI;AAAA,EACpB,CAAC;AAEH,QAAM,SAAS,oBAAI,IAA2B;AAC9C,QAAM,WAAwB,CAAC;AAC/B,QAAM,cAAc,IAAI,QAAQ,QAAQ,OAAO,GAAG,CAAC,UAAUD,MAAK,QAAQ,MAAM,IAAI,CAAC;AACrF,aAAW,CAAC,WAAW,KAAK,KAAK,aAAa;AAC5C,QAAI;AACF,YAAM,UAAU,MAAM,WAAW,CAAC,GAAG,aAAa;AAChD,mBAAW,SAAS,OAAO;AACzB,cAAI,YAAY,aAAaA,MAAK,SAAS,MAAM,IAAI,EAAG;AACxD,uBAAa,OAAO,IAAI,KAAK,CAAC;AAC9B,iBAAO;AAAA,YACL;AAAA,YACA,WAAW,MAAM,KAAK,OAAO,KAAK,EAAE,MAAM,CAAC,UAAU,QAAQ,MAAM,0BAA0B,KAAK,CAAC,GAAG,EAAE;AAAA,UAC1G;AAAA,QACF;AAAA,MACF,CAAC;AACD,eAAS,KAAK,OAAO;AAAA,IACvB,QAAQ;AACN,cAAQ,KAAK,yBAAyBA,MAAK,SAAS,MAAM,SAAS,CAAC,0BAA0B;AAAA,IAChG;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,KAAK,CAAC,IAAI,QAAQ,WAAW,SAAS,EAAE,EAAE,QAAQ,MAAM;AAAA,IACxD,UAAU,UAAU;AAClB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,IACxC;AAAA,IACA,QAAQ;AACN,iBAAW,WAAW,SAAU,SAAQ,MAAM;AAC9C,iBAAW,SAAS,OAAO,OAAO,EAAG,cAAa,KAAK;AACvD,gBAAU,MAAM;AAAA,IAClB;AAAA,EACF;AACF;;;AI9MA,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB,qBAAqB;AACvD,SAAS,wBAAwB;AACjC,SAAS,uBAAuC;;;ACahD,SAAS,gBAAgB,EAAE,MAAM,MAAAE,OAAM,UAAAC,UAAS,GAAqD;AACnG,QAAM,YAAa,WAAuD;AAC1E,aAAW,CAAC,SAAS,MAAM,KAAK,aAAa,CAAC,GAAG;AAC/C,UAAM,MAAM,IAAI,IAAI,OAAO;AAC3B,QAAI,CAAC,CAAC,aAAa,aAAa,OAAO,EAAE,SAAS,IAAI,QAAQ,KAAK,OAAO,IAAI,IAAI,MAAM,KAAM;AAC9F,UAAM,SAAS,OAAO,MAAM,KAAK,CAACC,YAAWA,QAAO,WAAW,OAAOF,KAAI;AAC1E,QAAI,OAAO,WAAW,UAAU,eAAe,CAAC,QAAQ,SAAS,OAAO,UAAW;AACnF,QAAI,OAAO,MAAM,MAAM,WAAWC,UAAU;AAC5C,WAAO,EAAE,UAAU,OAAO,MAAM,MAAM,UAAU,SAAS,OAAO,QAAQ;AAAA,EAC1E;AACA,SAAO;AACT;AAGO,SAAS,qBACd,SACA,EAAE,QAAQ,aAAa,YAAY,IAAO,GAC1C;AACA,MAAI;AACJ,MAAI,SAAS;AACb,MAAI,SAAS;AAEb,QAAM,UAAU,OAAO;AAAA,IACrB,MAAAD;AAAA,IACA,UAAAC;AAAA,IACA,QAAQ;AAAA,IACR,SAAS;AAAA,EACX,MAAoD;AAClD,QAAI,OAAQ,QAAO,MAAM,uBAAuB,yBAAyB;AACzE,QAAI,OAAQ,QAAO,MAAM,gBAAgB,2DAA2D;AACpG,UAAM,QAAQ,MAAM,QAAQ,IAAI,YAAY,EAAE,MAAAD,MAAK,CAAC;AACpD,QAAI,CAAC,MAAM,GAAI,QAAO;AACtB,IAAAC,cAAa,MAAM,KAAK,MAAM;AAC9B,QAAIA,cAAa,MAAM,KAAK,MAAM,SAAU,QAAO,MAAM,kBAAkB,wCAAwC;AACnH,UAAM,QAAQ,MAAM,QAAQ,IAAI,cAAc,CAAC,CAAC;AAChD,QAAI,CAAC,MAAM,GAAI,QAAO;AACtB,UAAM,UAAU,MAAM,KAAK,KAAK,CAAC,UAAU,MAAM,OAAOD,KAAI,GAAG;AAC/D,QAAI,CAAC,QAAS,QAAO,MAAM,mBAAmB,SAASA,KAAI,wBAAwB;AACnF,QAAI,CAAC,OAAQ,QAAO,MAAM,mBAAmB,mEAAmE;AAChH,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,IAAI,IAAI,MAAM;AAC3B,YAAM,IAAI,IAAI,SAAS,IAAI;AAC3B,UAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,WAAW,KAAK,UAAU,IAAI,YAAY,IAAI;AACpG,eAAO,MAAM,uBAAuB,mEAAmE;AAAA,IAC3G,QAAQ;AACN,aAAO,MAAM,uBAAuB,0EAA0E;AAAA,IAChH;AAGA,QAAI,OAAQ,QAAO,MAAM,gBAAgB,2DAA2D;AACpG,QAAI,OAAQ,QAAO,MAAM,uBAAuB,yBAAyB;AACzE,aAAS;AACT,QAAI;AACJ,QAAI;AACJ,QAAI,WAAW;AACf,QAAI,QAAQ;AACZ,QAAI;AACF,kBAAY,OAAO,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,MAAM,SAAS,OAAO,EAAE,UAAU,MAAM,SAAS,IAAO,CAAC,CAAC;AAC5G,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM;AAAA,MACnB,SAAS,OAAO;AACd,kBAAU;AACV,eAAO,MAAM,uBAAuB,qDAAqD,OAAO,KAAK,CAAC,EAAE;AAAA,MAC1G;AACA,UAAI,OAAQ,QAAO,MAAM,uBAAuB,yBAAyB;AACzE,UAAI,CAAC,SAAS,YAAY,GAAG;AAC3B,kBAAU;AACV,eAAO,MAAM,uBAAuB,2DAA2D;AAAA,MACjG;AACA,gBAAU,MAAM,SAAS,WAAW,EAAE,UAAU,EAAE,OAAO,OAAO,GAAG,mBAAmB,EAAE,CAAC;AACzF,cAAQ,WAAW,MAAM;AACvB,mBAAW;AACX,aAAK,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACtC,GAAG,SAAS;AACZ,YAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,WAAK,kBAAkB,SAAS;AAEhC,YAAM,KAAK,MAAM,QAAQ,CAAC,UAAU;AAClC,cAAM,UAAU,MAAM,QAAQ;AAC9B,eAAO,QAAQ,oBAAoB,KACjC,QAAQ,MAAM,MAAM,KAAK,UAAU,KACnC,IAAI,IAAI,QAAQ,IAAI,CAAC,EAAE,WAAW,IAAI,SACpC,MAAM,MAAM,IACZ,MAAM,SAAS;AAAA,MACrB,CAAC;AACD,cAAQ;AACR,YAAM,WAAW,MAAM,KAAK,KAAK,IAAI,MAAM,EAAE,WAAW,OAAO,CAAC;AAChE,UAAI,CAAC,UAAU,GAAG,EAAG,QAAO,MAAM,kBAAkB,yBAAyB,UAAU,OAAO,KAAK,SAAS,EAAE;AAE9G,cAAQ;AACR,YAAM,SAAS,EAAE,MAAM,YAAY,GAAG,MAAAA,OAAM,UAAAC,UAAS;AACrD,YAAM,SAAS,MAAM,KAAK,gBAAgB,iBAAiB,MAAM;AACjE,YAAM,UAAU,MAAM,OAAO,UAAU;AACvC,YAAM,OAAO,QAAQ;AACrB,UAAI,SAAS,aAAaA,UAAU,QAAO,MAAM,kBAAkB,6CAA6C;AAEhH,cAAQ;AACR,YAAM,KAAK,SAAS,YAAY;AAC9B,cAAM,SAAS,MAAM;AACrB,cAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,QAAQ,CAAC,UAAU,MAAM,OAAO,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC,CAAC,CAAC;AACxF,cAAM,IAAI,QAAc,CAAC,YAAY,sBAAsB,MAAM,sBAAsB,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,MAC1G,CAAC;AACD,YAAM,SAAS,MAAM,KAAK,SAAS,iBAAiB,MAAM;AAC1D,UAAI,QAAQ,aAAaA,UAAU,QAAO,MAAM,kBAAkB,8CAA8C;AAEhH,cAAQ;AACR,YAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,OAAO,YAAY,YAAY,SAAS,UAAU,CAAC;AAC7F,YAAM,QAAQ,MAAM,KAAK,SAAS,iBAAiB,MAAM;AACzD,YAAM,SAAS,MAAM,QAAQ,IAAI,YAAY,EAAE,MAAAD,MAAK,CAAC;AACrD,UAAI,CAAC,OAAO,GAAI,QAAO;AACvB,UAAI,OAAO,KAAK,MAAM,aAAaC,aAAY,OAAO,aAAaA,aAAY,MAAM,YAAY,OAAO;AACtG,eAAO,MAAM,kBAAkB,qEAAqE;AACtG,aAAO,QAAQ,EAAE,MAAAD,OAAM,UAAAC,WAAU,KAAK,KAAK,IAAI,GAAG,OAAO,QAAQ,UAAU,aAAa,IAAI,CAAC;AAAA,IAC/F,SAAS,OAAO;AACd,UAAI,OAAQ,QAAO,MAAM,uBAAuB,yBAAyB;AACzE,aAAO;AAAA,QACL,YAAa,iBAAiB,SAAS,MAAM,SAAS,iBAAkB,sBAAsB;AAAA,QAC9F,GAAG,KAAK,KAAK,OAAO,KAAK,CAAC;AAAA,MAC5B;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAClB,YAAM,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACrC,eAAS;AAAA,IACX;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,QAAQ;AACZ,eAAS;AACT,YAAM,SAAS,KAAK,CAAC,aAAa,SAAS,MAAM,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAClE,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;;;ACzJA,SAAS,iBAAsC;AAC/C,SAAS,SAAS;AAMlB,IAAM,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,mDAAmD;AAC3F,IAAM,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,uDAAuD;AAGzG,SAAS,gBAAgB,SAAkB,UAA0B;AAC1E,QAAM,SAAS,IAAI;AAAA,IACjB,EAAE,MAAM,gBAAgB,SAAS,QAAQ;AAAA,IACzC;AAAA,MACE,cACE;AAAA,IACJ;AAAA,EACF;AACA,QAAM,MAAM,OAAqB,IAAO,WAA6D;AACnG,UAAM,SAAS,MAAM,QAAQ,IAAI,IAAI,MAAM;AAC3C,WAAO;AAAA,MACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM,EAAE,CAAC;AAAA,MACxD,mBAAmB;AAAA,MACnB,SAAS,CAAC,OAAO;AAAA,IACnB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,aAAa,EAAE,OAAO,CAAC,CAAC;AAAA,MACxB,aAAa,EAAE,cAAc,MAAM,gBAAgB,MAAM,eAAe,MAAM;AAAA,IAChF;AAAA,IACA,MAAM,IAAI,cAAc,CAAC,CAAC;AAAA,EAC5B;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,aAAa,EAAE,OAAO,EAAE,KAAK,CAAC;AAAA,MAC9B,aAAa,EAAE,cAAc,MAAM,gBAAgB,MAAM,eAAe,MAAM;AAAA,IAChF;AAAA,IACA,CAAC,WAAW,IAAI,YAAY,MAAM;AAAA,EACpC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa,EAAE,OAAO,EAAE,MAAM,UAAU,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,MAClF,aAAa,EAAE,cAAc,OAAO,iBAAiB,MAAM,gBAAgB,OAAO,eAAe,MAAM;AAAA,IACzG;AAAA,IACA,CAAC,WAAW,IAAI,eAAe,MAAM;AAAA,EACvC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa,EAAE,OAAO,EAAE,MAAM,UAAU,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAAA,MACvE,aAAa,EAAE,cAAc,OAAO,iBAAiB,MAAM,gBAAgB,MAAM,eAAe,MAAM;AAAA,IACxG;AAAA,IACA,CAAC,WAAW,IAAI,aAAa,MAAM;AAAA,EACrC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAAA,MACxC,aAAa,EAAE,cAAc,OAAO,iBAAiB,MAAM,gBAAgB,OAAO,eAAe,MAAM;AAAA,IACzG;AAAA,IACA,CAAC,WAAW,IAAI,gBAAgB,MAAM;AAAA,EACxC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,aAAa,EAAE,OAAO;AAAA,QACpB;AAAA,QACA,UAAU,SAAS,SAAS;AAAA,QAC5B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,SAAS;AAAA,QACpD,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,SAAS;AAAA,MACvD,CAAC;AAAA,MACD,aAAa,EAAE,cAAc,MAAM,gBAAgB,OAAO,eAAe,KAAK;AAAA,IAChF;AAAA,IACA,OAAO,WAAoC;AACzC,YAAM,WAAW,MAAM,SAAS,QAAQ,MAAM;AAC9C,UAAI,CAAC,SAAS;AACZ,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,QAAQ,EAAE,CAAC;AAAA,UAC1D,mBAAmB;AAAA,UACnB,SAAS;AAAA,QACX;AACF,YAAM,EAAE,KAAK,GAAG,SAAS,IAAI,SAAS;AACtC,YAAM,SAAS,QAAQ,QAAQ;AAC/B,aAAO;AAAA,QACL,SAAS;AAAA,UACP,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM,EAAE;AAAA,UAC7C,EAAE,MAAM,SAAS,UAAU,aAAa,MAAM,IAAI,SAAS,QAAQ,EAAE;AAAA,QACvE;AAAA,QACA,mBAAmB;AAAA,QACnB,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AFnGA,IAAM,eAAe;AACrB,IAAM,MAAM,oBAAI,IAAY,CAAC,cAAc,YAAY,eAAe,aAAa,cAAc,CAAgB;AAajH,SAAS,MAAM,KAA6B;AAC1C,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,GAAG;AAC9B,QAAI,OAAO,SAAS,OAAO,YAAY,CAAC,IAAI,IAAI,QAAQ,EAAE,EAAG,QAAO;AACpE,WAAO,EAAE,IAAI,QAAQ,IAAI,IAAI,QAAQ,IAAI,QAAQ,QAAQ,UAAU,CAAC,EAAE;AAAA,EACxE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,YACd,SACA,EAAE,MAAM,OAAO,aAAa,SAAS,OAAO,GAC3B;AACjB,QAAM,UAAU,CAAC,WACf,WAAW,WAAc,UAAU,QAAQ,SAAS,MAAM,IAAI,aAAa,KAAK,MAAM;AAExF,QAAM,UAAU,CAAC,UAAiB,QAAQ,MAAM,+BAA+B,KAAK;AACpF,QAAM,WAAW,qBAAqB,SAAS,EAAE,QAAQ,aAAa,MAAO,KAAK,QAAQ,EAAkB,KAAK,CAAC;AAClH,QAAM,MAAM,iBAAiB,MAAM,gBAAgB,SAAS,QAAQ,GAAG,EAAE,QAAQ,CAAC;AAClF,QAAM,YAAY,cAAc,KAAK,EAAE,QAAQ,CAAC;AAChD,QAAM,eAAe,wBAAwB;AAC7C,QAAM,OAAO,aAAa,CAAC,SAAS,aAAa;AAC/C,QAAI,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,MAAM,QAAQ;AACzC,eAAS,UAAU,GAAG,EAAE,IAAI;AAC5B;AAAA,IACF;AACA,QAAI,CAAC,aAAa,SAAS,QAAQ,EAAG;AACtC,QAAI,CAAC,QAAQ,QAAQ,QAAQ,MAAM,GAAG;AACpC,eAAS,UAAU,GAAG,EAAE,IAAI,oBAAoB;AAChD;AAAA,IACF;AACA,SAAK,UAAU,SAAS,QAAQ;AAAA,EAClC,CAAC;AACD,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,QAAQ;AAAA,IACR,cAAc,CAAC,EAAE,OAAO,MAA2B,QAAQ,MAAM;AAAA,EACnE,CAAC;AACD,QAAM,OAAO,CAAC,QAAmB,YAA2B;AAC1D,QAAI,OAAO,eAAe,OAAO,KAAM,QAAO,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,EAC5E;AAEA,SAAO,GAAG,cAAc,CAAC,WAAW;AAClC,WAAO,GAAG,WAAW,OAAO,QAAQ;AAClC,YAAM,UAAU,MAAM,IAAI,SAAS,CAAC;AACpC,UAAI,CAAC,QAAS,QAAO,KAAK,QAAQ,EAAE,MAAM,YAAY,IAAI,IAAI,QAAQ,MAAM,aAAa,EAAE,CAAC;AAC5F,YAAM,SAAS,MAAM,QAClB,IAAI,QAAQ,IAAI,QAAQ,QAAQ,MAAM,EACtC,MAAM,CAAC,UAAmB,MAAM,YAAY,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,CAAC;AACtG,WAAK,QAAQ,EAAE,MAAM,YAAY,IAAI,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D,CAAC;AAAA,EACH,CAAC;AAED,QAAM,cAAc,QAAQ,UAAU,CAAC,OAAO,WAAW;AACvD,eAAW,UAAU,OAAO,QAAS,KAAI,WAAW,OAAQ,MAAK,QAAQ,EAAE,MAAM,SAAS,MAAM,CAAC;AAAA,EACnG,CAAC;AAED,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAO,KAAK,SAAS,CAAC,UAAU;AAC9B,kBAAY;AACZ,aAAO,MAAM;AACb,WAAK,IAAI,MAAM;AACf,WAAK,SAAS,MAAM;AACpB,aAAO,KAAK;AAAA,IACd,CAAC;AACD,SAAK;AAAA,MAAK;AAAA,MAAa,MACrB,QAAQ;AAAA,QACN,MAAO,KAAK,QAAQ,EAAkB;AAAA,QACtC,OAAO,YAAY;AACjB,sBAAY;AACZ,gBAAM,SAAS,MAAM;AACrB,qBAAW,UAAU,OAAO,QAAS,QAAO,UAAU;AACtD,gBAAM,IAAI,MAAM;AAChB,gBAAM,IAAI,QAAc,CAAC,SAAS,OAAO,MAAM,MAAM,KAAK,CAAC,CAAC;AAC5D,gBAAM,IAAI,QAAc,CAAC,SAAS;AAChC,iBAAK,MAAM,MAAM,KAAK,CAAC;AACvB,iBAAK,oBAAoB;AAAA,UAC3B,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH;AACA,SAAK,OAAO,MAAM,IAAI;AAAA,EACxB,CAAC;AACH;;;AG7GA,SAAS,kBAAkB;AAC3B,OAAOE,WAAU;AACjB,SAAS,kBAAkB;AAG3B,IAAM,aAAa,CAAC,oBAAoB,qBAAqB,oBAAoB,mBAAmB;AAGpG,eAAsB,WAAW,KAAa,UAAmB;AAC/D,QAAM,OAAO,WACTA,MAAK,QAAQ,KAAK,QAAQ,IAC1B,WAAW,IAAI,CAAC,SAASA,MAAK,KAAK,KAAK,IAAI,CAAC,EAAE,KAAK,CAAC,cAAc,WAAW,SAAS,CAAC;AAC5F,MAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,EAAG,OAAM,IAAI,MAAM,eAAe,YAAY,kBAAkB,aAAa,GAAG,EAAE;AAC/G,QAAM,OAAO,WAAW,YAAY,KAAK,EAAE,aAAa,MAAM,CAAC;AAC/D,QAAM,SAAS,MAAM,KAAK,OAAqB,MAAM,EAAE,SAAS,KAAK,CAAC;AACtE,MAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK;AAC9B,UAAM,IAAI,MAAM,YAAYA,MAAK,SAAS,IAAI,CAAC,8CAA8C;AAC/F,SAAO,EAAE,QAAQ,MAAMA,MAAK,QAAQ,IAAI,EAAE;AAC5C;;;ATXA,IAAM,OAAO,yDAAyD,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASlF,eAAe,KAAK,MAAgB;AAClC,MAAI,KAAK,CAAC,MAAM,mBAAmB;AACjC,UAAMC,WAAU,cAAc,YAAY,GAAG;AAC7C,UAAM,MAAMC,MAAK;AAAA,MACfA,MAAK,QAAQD,SAAQ,QAAQ,yBAAyB,CAAC;AAAA,MACvDA,SAAQ,yBAAyB,EAAE,IAAI;AAAA,IACzC;AACA,UAAME,SAAQ,MAAM,QAAQ,UAAU,CAAC,KAAK,WAAW,UAAU,GAAG,EAAE,OAAO,UAAU,CAAC;AACxF,IAAAA,OAAM,KAAK,SAAS,CAAC,UAAU;AAC7B,cAAQ,MAAM,MAAM,OAAO;AAC3B,cAAQ,KAAK,CAAC;AAAA,IAChB,CAAC;AACD,IAAAA,OAAM,KAAK,QAAQ,CAAC,SAAS,QAAQ,KAAK,QAAQ,CAAC,CAAC;AACpD;AAAA,EACF;AACA,QAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,QAAM,MAAM,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;AACrD,QAAM,UAAU,UAAU,KAAK,CAAC,IAAI,KAAK,MAAM,QAAQ,CAAC;AACxD,QAAM,EAAE,aAAa,OAAO,IAAI,UAAU;AAAA,IACxC,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,SAAS;AAAA,MACP,QAAQ,EAAE,MAAM,SAAS;AAAA,MACzB,MAAM,EAAE,MAAM,SAAS;AAAA,MACvB,WAAW,EAAE,MAAM,SAAS;AAAA,MAC5B,MAAM,EAAE,MAAM,WAAW,OAAO,IAAI;AAAA,IACtC;AAAA,EACF,CAAC;AACD,MAAI,OAAO,QAAQ,YAAY,CAAC,MAAM,MAAO,QAAO,QAAQ,IAAI,IAAI;AAEpE,QAAM,EAAE,QAAQ,KAAK,IAAI,MAAM,WAAW,QAAQ,IAAI,GAAG,OAAO,MAAM;AACtE,QAAM,UAAU,MAAM,cAAc,EAAE,MAAM,OAAO,OAAO,MAAM,CAAC;AACjE,QAAM,SAAS,MAAM,YAAY,SAAS;AAAA,IACxC,MAAM,OAAO,OAAO,QAAQ,OAAO,QAAQ,YAAY;AAAA,IACvD,SAAS,OAAO;AAAA,IAChB,QAAQ,OAAO,SAAS,KAAK,OAAO;AAAA,EACtC,CAAC;AACD,UAAQ;AAAA,IACN,4CAA4C,OAAO,IAAI,WAAM,OAAO,MAAM,MAAM,eAAeD,MAAK,SAAS,QAAQ,IAAI,GAAG,IAAI,KAAK,GAAG;AAAA,EAC1I;AACA,UAAQ,IAAI,0CAA0C,OAAO,IAAI,MAAM;AAEvE,QAAM,WAAW,OAAO,OAAO,MAAM;AACnC,YAAQ,MAAM;AACd,UAAM,OAAO,MAAM;AACnB,YAAQ,KAAK,IAAI;AAAA,EACnB;AAEA,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,MAAI,CAAC,KAAK;AACR,YAAQ,KAAK,UAAU,MAAM,KAAK,SAAS,CAAC;AAC5C,YAAQ,KAAK,WAAW,MAAM,KAAK,SAAS,CAAC;AAC7C;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,KAAK,MAAM,EAAE,OAAO,WAAW,KAAK,EAAE,GAAG,QAAQ,KAAK,aAAa,OAAO,OAAO,IAAI,EAAE,EAAE,CAAC;AAC9G,QAAM,KAAK,QAAQ,CAAC,SAAS,KAAK,SAAS,QAAQ,CAAC,CAAC;AACrD,aAAW,UAAU,CAAC,UAAU,SAAS,EAAY,SAAQ,KAAK,QAAQ,MAAM,MAAM,KAAK,MAAM,CAAC;AACpG;AAEA,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU;AAC3C,UAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAC5D,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["path","path","tool","path","tool","path","revision","tool","revision","handle","path","require","path","child"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/core/tool.ts","../src/runtime/runtime.ts","../src/core/result.ts","../src/core/validate.ts","../src/runtime/files.ts","../src/runtime/server.ts","../src/runtime/capture.ts","../src/runtime/mcp.ts","../src/runtime/load.ts","../src/runtime/ports.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { DEFAULT_PORT } from './core/tool'\nimport { createRuntime, loadConfig } from './runtime'\nimport { freeAppPort, serveBridgeNear } from './runtime/ports'\n\nconst HELP = `Usage: studio dev [--config studio.config.ts] [--port ${DEFAULT_PORT}] [--app-url http://localhost:3000] [-- <command...>]\n studio install-browser\n\nStarts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp,\nmoving to the next free port when another project's Studio already uses it.\nAnything after \"--\" runs alongside it (e.g. \"next dev\") with STUDIO_PORT set to the runtime port,\nand the runtime stops when that command exits.\nSet appUrl in the config or pass --app-url to enable MCP preview capture. A configured local\nappUrl port is passed to the command as PORT, moved to the next free port when taken.\nRun \"studio install-browser\" once to install Chromium for captures.`\n\nasync function main(argv: string[]) {\n if (argv[0] === 'install-browser') {\n const require = createRequire(import.meta.url)\n const cli = path.join(\n path.dirname(require.resolve('playwright/package.json')),\n require('playwright/package.json').bin.playwright,\n )\n const child = spawn(process.execPath, [cli, 'install', 'chromium'], { stdio: 'inherit' })\n child.once('error', (cause) => {\n console.error(cause.message)\n process.exit(1)\n })\n child.once('exit', (code) => process.exit(code ?? 1))\n return\n }\n const split = argv.indexOf('--')\n const own = split === -1 ? argv : argv.slice(0, split)\n const command = split === -1 ? [] : argv.slice(split + 1)\n const { positionals, values } = parseArgs({\n args: own,\n allowPositionals: true,\n options: {\n config: { type: 'string' },\n port: { type: 'string' },\n 'app-url': { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n })\n if (values.help || positionals[0] !== 'dev') return console.log(HELP)\n\n const { config, root } = await loadConfig(process.cwd(), values.config)\n const [bin, ...args] = command\n const env = { ...process.env }\n let appUrl = values['app-url'] ?? config.appUrl\n // Projects share the default app port too: claim a free one for the wrapped app so captures follow it.\n const app = bin && !values['app-url'] && !env.PORT && appUrl && URL.canParse(appUrl) ? new URL(appUrl) : undefined\n if (app?.port && ['localhost', '127.0.0.1', '[::1]'].includes(app.hostname)) {\n const preferred = app.port\n app.port = env.PORT = String(await freeAppPort(app.hostname, Number(preferred)))\n if (app.port !== preferred) console.log(`[studio] port ${preferred} is in use, starting the app on ${app.origin}`)\n appUrl = app.href\n }\n\n const runtime = await createRuntime({ root, tools: config.tools })\n const port = Number(values.port ?? config.port ?? DEFAULT_PORT)\n const bridge = await serveBridgeNear(runtime, { port, origins: config.origins, appUrl })\n if (bridge.port !== port) console.log(`[studio] port ${port} is in use, probably by another project's Studio`)\n console.log(\n `[studio] runtime ready on ws://127.0.0.1:${bridge.port} — ${config.tools.length} tool(s) in ${path.relative(process.cwd(), root) || '.'}`,\n )\n console.log(`[studio] MCP ready on http://127.0.0.1:${bridge.port}/mcp`)\n\n const shutdown = async (code = 0) => {\n runtime.close()\n await bridge.close()\n process.exit(code)\n }\n\n if (!bin) {\n process.once('SIGINT', () => void shutdown())\n process.once('SIGTERM', () => void shutdown())\n return\n }\n const child = spawn(bin, args, { stdio: 'inherit', env: { ...env, STUDIO_PORT: String(bridge.port) } })\n child.once('exit', (code) => void shutdown(code ?? 0))\n for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => child.kill(signal))\n}\n\nmain(process.argv.slice(2)).catch((cause) => {\n console.error(cause instanceof Error ? cause.message : cause)\n process.exit(1)\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\n\n/**\n * An SVG icon as data: `[tag, attributes]` children of a 24×24 stroke icon.\n * Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.\n */\nexport type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[]\n\ntype ToolBase = {\n /** Unique, URL-safe identifier. */\n id: string\n title?: string\n /** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */\n icon?: ToolIcon\n /** App route that renders this tool's result, shown next to the controls in /studio. */\n preview?: string\n /** Use a bare iframe for tools that already own their canvas layout and headers. */\n previewLayout?: 'canvas' | 'custom'\n}\n\n/**\n * A tool's contract. Isomorphic on purpose: the runtime loads it in Node to\n * validate and persist, the app loads it in the browser to apply and debug.\n *\n * `TConfig` is the one type that is read, edited, applied, and persisted.\n */\nexport type ToolDefinition<TConfig extends object = object> = ToolBase & {\n /** Repo-relative JSON file the config persists to. Import that same file as `initial`. */\n file: string\n /** Config used before the runtime answers (and in production). */\n initial: TConfig\n /**\n * What can be tweaked, as an Xyz/Tweakpane schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => DebugSchema\n}\n\n/** A tool with nothing to tune: a sidebar entry for its preview, without an inspector or config file. */\nexport type PreviewToolDefinition = ToolBase & {\n preview: string\n file?: never\n initial?: never\n controls?: never\n}\n\n/** A tool with a config. */\nexport type AnyTool = ToolDefinition<any>\n\n/** Anything `defineTool` returns: the kind follows from whether it declares a config. */\nexport type StudioTool = AnyTool | PreviewToolDefinition\n\nexport const hasConfig = (tool: StudioTool): tool is AnyTool => tool.controls !== undefined\n\nexport type ConfigOf<TTool> = TTool extends ToolDefinition<infer TConfig> ? TConfig : never\n\nconst ID = /^[a-z0-9][a-z0-9-]*$/\n\n/** Declare `file`, `initial`, and `controls` for a tunable tool, or only a `preview` for a preview-only one. */\nexport function defineTool<TConfig extends object>(tool: ToolDefinition<TConfig>): ToolDefinition<TConfig>\nexport function defineTool(tool: PreviewToolDefinition): PreviewToolDefinition\nexport function defineTool(tool: StudioTool): StudioTool {\n if (!ID.test(tool.id)) throw new Error(`[studio] tool id \"${tool.id}\" must be kebab-case`)\n const config = [tool.file, tool.initial, tool.controls].filter((part) => part !== undefined).length\n if (config === 1 || config === 2)\n throw new Error(`[studio] tool \"${tool.id}\" needs all of file, initial, and controls, or none of them`)\n if (!config && !tool.preview) throw new Error(`[studio] tool \"${tool.id}\" needs a preview or a config`)\n return tool\n}\n\nexport type StudioConfig = {\n tools: readonly StudioTool[]\n /** Runtime WebSocket and HTTP MCP port. Defaults to 5179. */\n port?: number\n /** Browser origins allowed on the bridge and MCP endpoint. Defaults to localhost origins. */\n origins?: readonly string[]\n /** Running app URL (e.g. http://localhost:3000), used for MCP preview captures. */\n appUrl?: string\n}\n\nexport const DEFAULT_PORT = 5179\n\nexport function defineConfig(config: StudioConfig): StudioConfig {\n return config\n}\n","import { watch, type FSWatcher } from 'node:fs'\nimport path from 'node:path'\nimport type { Op, Operations, ToolState, ToolSummary } from '../core/protocol'\nimport { error, success, type Result } from '../core/result'\nimport { hasConfig, type AnyTool, type StudioTool } from '../core/tool'\nimport { validateConfig } from '../core/validate'\nimport { hashOf, readDisk, resolveToolFile, serialize, writeAtomic } from './files'\n\ntype Entry = {\n tool: AnyTool\n file: string\n state: ToolState\n /** Serializes disk work per tool so saves and external reloads never interleave. */\n queue: Promise<unknown>\n}\n\nexport type RuntimeListener = (state: ToolState, origin: unknown) => void\n\nexport type Runtime = {\n root: string\n run<TOp extends Op>(\n op: TOp,\n params: Operations[TOp]['params'],\n origin?: unknown,\n ): Promise<Result<Operations[TOp]['result']>>\n subscribe(listener: RuntimeListener): () => void\n close(): void\n}\n\nconst equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n const keys = Object.keys(a)\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => equal((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))\n )\n}\n\nasync function load(tool: AnyTool, file: string) {\n const disk = await readDisk(file)\n if (!disk) return { value: structuredClone(tool.initial), hash: null }\n const parsed = validateConfig(tool, JSON.parse(disk.content))\n if (!parsed.ok)\n throw new Error(`[studio] ${path.basename(file)} is not a valid \"${tool.id}\" config: ${parsed.message}`)\n return { value: parsed.data, hash: disk.hash }\n}\n\n/**\n * The single authority over tool configs: validates every write, owns draft\n * revisions, and is the only thing that touches the persisted files.\n */\nexport async function createRuntime({ root, tools }: { root: string; tools: readonly StudioTool[] }): Promise<Runtime> {\n root = path.resolve(root)\n const entries = new Map<string, Entry>()\n const files = new Set<string>()\n\n const ids = new Set<string>()\n for (const tool of tools) {\n if (ids.has(tool.id)) throw new Error(`[studio] duplicate tool id \"${tool.id}\"`)\n ids.add(tool.id)\n // Preview-only tools are listed, but have no config for the runtime to own.\n if (!hasConfig(tool)) continue\n const file = await resolveToolFile(root, tool.file)\n if (files.has(file)) throw new Error(`[studio] tool \"${tool.id}\" shares its file with another tool`)\n files.add(file)\n const saved = await load(tool, file)\n entries.set(tool.id, {\n tool,\n file,\n queue: Promise.resolve(),\n state: {\n tool: tool.id,\n draft: { value: structuredClone(saved.value), revision: 0 },\n saved,\n dirty: false,\n conflict: false,\n },\n })\n }\n\n const listeners = new Set<RuntimeListener>()\n const commit = (entry: Entry, next: Partial<ToolState>, origin?: unknown) => {\n const state = { ...entry.state, ...next }\n state.dirty = !equal(state.draft.value, state.saved.value)\n if (!state.dirty) state.conflict = false\n entry.state = state\n for (const listener of listeners) listener(state, origin)\n return state\n }\n const serial = <T>(entry: Entry, task: () => Promise<T>) => {\n const run = entry.queue.then(task)\n entry.queue = run.catch(() => {})\n return run\n }\n\n const handlers: { [TOp in Op]: (params: any, origin: unknown) => Promise<Result<Operations[TOp]['result']>> } = {\n 'tools.list': async () =>\n success(\n tools.map((tool): ToolSummary => ({\n id: tool.id,\n title: tool.title ?? tool.id,\n file: tool.file,\n icon: tool.icon,\n preview: tool.preview,\n })),\n ),\n\n 'tool.get': async ({ tool }) => withEntry(tool, async (entry) => success(entry.state)),\n\n 'tool.update': async ({ tool, value, revision }, origin) =>\n withEntry(tool, async (entry) => {\n if (revision !== undefined && revision !== entry.state.draft.revision)\n return error('stale_revision', `draft is at revision ${entry.state.draft.revision}, not ${revision}`)\n const parsed = validateConfig(entry.tool, value)\n if (!parsed.ok) return parsed\n const draft = { value: parsed.data, revision: entry.state.draft.revision + 1 }\n return success(commit(entry, { draft }, origin))\n }),\n\n 'tool.save': async ({ tool, revision, force = false }, origin) =>\n withEntry(tool, (entry) =>\n serial(entry, async () => {\n const { draft, saved } = entry.state\n if (revision !== draft.revision)\n return error('stale_revision', `draft is at revision ${draft.revision}, not ${revision}`)\n const disk = await readDisk(entry.file)\n if (!force && (entry.state.conflict || (disk?.hash ?? null) !== saved.hash)) {\n commit(entry, { conflict: true })\n return error('conflict', `${entry.tool.file} changed on disk; discard to reload or save with force`)\n }\n const content = serialize(draft.value)\n await writeAtomic(entry.file, content)\n return success(\n commit(entry, { saved: { value: structuredClone(draft.value), hash: hashOf(content) } }, origin),\n )\n }),\n ),\n\n 'tool.discard': async ({ tool, revision }, origin) =>\n withEntry(tool, async (entry) => {\n const { saved, draft } = entry.state\n if (revision !== undefined && revision !== draft.revision)\n return error('stale_revision', `draft is at revision ${draft.revision}, not ${revision}`)\n return success(\n commit(entry, { draft: { value: structuredClone(saved.value), revision: draft.revision + 1 } }, origin),\n )\n }),\n }\n\n async function withEntry<T>(id: unknown, task: (entry: Entry) => Promise<Result<T>>): Promise<Result<T>> {\n const entry = typeof id === 'string' ? entries.get(id) : undefined\n if (entry) return task(entry)\n return ids.has(id as string)\n ? error('no_config', `tool \"${String(id)}\" is preview-only and has no config`)\n : error('unknown_tool', `no tool \"${String(id)}\"`)\n }\n\n /** Pick up edits made outside Studio (editor, git checkout, an agent). */\n const reload = (entry: Entry) =>\n serial(entry, async () => {\n const disk = await readDisk(entry.file)\n const { saved, draft, dirty } = entry.state\n if ((disk?.hash ?? null) === saved.hash) return\n if (!disk) return commit(entry, { saved: { ...saved, hash: null } })\n let value: unknown\n try {\n value = JSON.parse(disk.content)\n } catch {\n return console.warn(`[studio] ignoring unparsable ${entry.tool.file}`)\n }\n const parsed = validateConfig(entry.tool, value)\n if (!parsed.ok) return console.warn(`[studio] ignoring invalid ${entry.tool.file}: ${parsed.message}`)\n const next: Partial<ToolState> = { saved: { value: parsed.data, hash: disk.hash } }\n if (dirty) next.conflict = true\n else next.draft = { value: structuredClone(parsed.data), revision: draft.revision + 1 }\n commit(entry, next)\n })\n\n const timers = new Map<Entry, NodeJS.Timeout>()\n const watchers: FSWatcher[] = []\n const byDirectory = Map.groupBy(entries.values(), (entry) => path.dirname(entry.file))\n for (const [directory, group] of byDirectory) {\n try {\n const watcher = watch(directory, (_, filename) => {\n for (const entry of group) {\n if (filename && filename !== path.basename(entry.file)) continue\n clearTimeout(timers.get(entry))\n timers.set(\n entry,\n setTimeout(() => void reload(entry).catch((cause) => console.error('[studio] reload failed', cause)), 50),\n )\n }\n })\n watchers.push(watcher)\n } catch {\n console.warn(`[studio] not watching ${path.relative(root, directory)} (it does not exist yet)`)\n }\n }\n\n return {\n root,\n run: (op, params, origin) => handlers[op](params, origin) as never,\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n close() {\n for (const watcher of watchers) watcher.close()\n for (const timer of timers.values()) clearTimeout(timer)\n listeners.clear()\n },\n }\n}\n","export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: DebugSchema, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n","import { createHash, randomUUID } from 'node:crypto'\nimport { mkdir, readFile, realpath, rename, rm, stat } from 'node:fs/promises'\nimport { writeFile } from 'node:fs/promises'\nimport path from 'node:path'\n\nconst inside = (root: string, target: string) => {\n const relative = path.relative(root, target)\n return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)\n}\n\nconst exists = (target: string) =>\n stat(target).then(\n () => true,\n () => false,\n )\n\n/**\n * Resolve a tool's persistence target, refusing anything that is not a `.json`\n * file inside `root` — including paths that escape through a symlink.\n */\nexport async function resolveToolFile(root: string, file: string): Promise<string> {\n const fail = (reason: string) => new Error(`[studio] tool file \"${file}\" ${reason}`)\n if (path.isAbsolute(file)) throw fail('must be relative to the project root')\n if (path.extname(file) !== '.json') throw fail('must be a .json file')\n const absolute = path.resolve(root, file)\n if (!inside(root, absolute)) throw fail('escapes the project root')\n\n let probe = absolute\n while (!(await exists(probe))) probe = path.dirname(probe)\n if (probe !== root && !inside(await realpath(root), await realpath(probe))) throw fail('escapes the project root')\n return absolute\n}\n\nexport const hashOf = (content: string) => createHash('sha256').update(content).digest('hex').slice(0, 16)\n\nexport const serialize = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\n\n/** The file's content and hash, or null when it does not exist. */\nexport async function readDisk(file: string): Promise<{ content: string; hash: string } | null> {\n try {\n const content = await readFile(file, 'utf8')\n return { content, hash: hashOf(content) }\n } catch (cause) {\n if ((cause as NodeJS.ErrnoException).code === 'ENOENT') return null\n throw cause\n }\n}\n\n/** Write through a sibling temp file + rename so readers never see a half-written config. */\nexport async function writeAtomic(file: string, content: string) {\n await mkdir(path.dirname(file), { recursive: true })\n const temp = `${file}.${randomUUID()}.tmp`\n try {\n await writeFile(temp, content, 'utf8')\n await rename(temp, file)\n } catch (cause) {\n await rm(temp, { force: true })\n throw cause\n }\n}\n","import { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { localhostHostValidation, toNodeHandler } from '@modelcontextprotocol/node'\nimport { createMcpHandler } from '@modelcontextprotocol/server'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { Op, Request, ServerMessage } from '../core/protocol'\nimport { error } from '../core/result'\nimport { createPreviewCapture } from './capture'\nimport { createMcpServer } from './mcp'\nimport type { Runtime } from './runtime'\n\nconst LOCAL_ORIGIN = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d+)?$/\nconst OPS = new Set<string>(['tools.list', 'tool.get', 'tool.update', 'tool.save', 'tool.discard'] satisfies Op[])\n\nexport type BridgeOptions = {\n port: number\n host?: string\n /** Browser origins allowed to connect. Defaults to localhost. Non-browser clients send no origin. */\n origins?: readonly string[]\n /** Running app URL used to resolve registered preview routes for MCP capture. */\n appUrl?: string\n}\n\nexport type Bridge = { port: number; close(): Promise<void> }\n\nfunction parse(raw: string): Request | null {\n try {\n const message = JSON.parse(raw)\n if (typeof message?.id !== 'number' || !OPS.has(message.op)) return null\n return { id: message.id, op: message.op, params: message.params ?? {} }\n } catch {\n return null\n }\n}\n\n/** Share one runtime between the browser WebSocket bridge and the /mcp HTTP endpoint. */\nexport function serveBridge(\n runtime: Runtime,\n { port, host = '127.0.0.1', origins, appUrl }: BridgeOptions,\n): Promise<Bridge> {\n const allowed = (origin: string | undefined) =>\n origin === undefined || (origins ? origins.includes(origin) : LOCAL_ORIGIN.test(origin))\n\n const onerror = (cause: Error) => console.error('[studio] MCP request failed', cause)\n const previews = createPreviewCapture(runtime, { appUrl, runtimePort: () => (http.address() as AddressInfo).port })\n const mcp = createMcpHandler(() => createMcpServer(runtime, previews), { onerror })\n const handleMcp = toNodeHandler(mcp, { onerror })\n const validateHost = localhostHostValidation()\n const http = createServer((request, response) => {\n if (request.url?.split('?')[0] !== '/mcp') {\n response.writeHead(404).end()\n return\n }\n if (!validateHost(request, response)) return\n if (!allowed(request.headers.origin)) {\n response.writeHead(403).end('Origin not allowed')\n return\n }\n void handleMcp(request, response)\n })\n const server = new WebSocketServer({\n server: http,\n verifyClient: ({ origin }: { origin?: string }) => allowed(origin),\n })\n const send = (socket: WebSocket, message: ServerMessage) => {\n if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(message))\n }\n\n server.on('connection', (socket) => {\n socket.on('message', async (raw) => {\n const request = parse(raw.toString())\n if (!request) return send(socket, { type: 'response', id: -1, result: error('bad_request') })\n const result = await runtime\n .run(request.op, request.params, socket)\n .catch((cause: unknown) => error('internal', cause instanceof Error ? cause.message : String(cause)))\n send(socket, { type: 'response', id: request.id, result })\n })\n })\n\n const unsubscribe = runtime.subscribe((state, origin) => {\n for (const client of server.clients) if (client !== origin) send(client, { type: 'state', state })\n })\n\n return new Promise((resolve, reject) => {\n server.once('error', (cause) => {\n unsubscribe()\n server.close()\n void mcp.close()\n void previews.close()\n reject(cause)\n })\n http.once('listening', () =>\n resolve({\n port: (http.address() as AddressInfo).port,\n close: async () => {\n unsubscribe()\n await previews.close()\n for (const client of server.clients) client.terminate()\n await mcp.close()\n await new Promise<void>((done) => server.close(() => done()))\n await new Promise<void>((done) => {\n http.close(() => done())\n http.closeAllConnections()\n })\n },\n }),\n )\n http.listen(port, host)\n })\n}\n","import type { Browser, BrowserContext } from 'playwright'\nimport type { Studio } from '../client/studio'\nimport { error, success, type Result } from '../core/result'\nimport type { Runtime } from './runtime'\n\nexport type CaptureParams = { tool: string; revision?: number; width?: number; height?: number }\ntype CaptureImage = {\n tool: string\n revision: number\n url: string\n width: number\n height: number\n settling: 'automatic'\n png: Buffer\n}\n\n/** Runs in the preview page. Reuse createStudio's HMR registry; tools need no capture integration. */\nfunction previewSnapshot({ port, tool, revision }: { port: number; tool: string; revision: number }) {\n const instances = (globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio\n for (const [address, studio] of instances ?? []) {\n const url = new URL(address)\n if (!['localhost', '127.0.0.1', '[::1]'].includes(url.hostname) || Number(url.port) !== port) continue\n const handle = studio.tools.find((handle) => handle.definition.id === tool)\n if (studio.connection.phase !== 'connected' || !handle?.state || handle.comparing) continue\n if (handle.state.draft.revision < revision) continue\n return { revision: handle.state.draft.revision, version: handle.version }\n }\n return null\n}\n\n/** One lazy browser per bridge, one isolated page per capture. Never writes a draft or a file. */\nexport function createPreviewCapture(\n runtime: Runtime,\n { appUrl, runtimePort, timeoutMs = 20_000 }: { appUrl?: string; runtimePort: () => number; timeoutMs?: number },\n) {\n let browser: Promise<Browser> | undefined\n let active = false\n let closed = false\n\n const capture = async ({\n tool,\n revision,\n width = 1200,\n height = 800,\n }: CaptureParams): Promise<Result<CaptureImage>> => {\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n if (active) return error('capture_busy', 'Another preview is being captured; retry when it finishes')\n const state = await runtime.run('tool.get', { tool })\n if (!state.ok) return state\n revision ??= state.data.draft.revision\n if (revision !== state.data.draft.revision) return error('stale_revision', 'Read the latest draft before capturing')\n const tools = await runtime.run('tools.list', {})\n if (!tools.ok) return tools\n const preview = tools.data.find((entry) => entry.id === tool)?.preview\n if (!preview) return error('preview_missing', `Tool \"${tool}\" has no preview route`)\n if (!appUrl) return error('app_url_missing', 'Set appUrl in studio.config.ts or pass studio dev --app-url <url>')\n let url: URL\n try {\n const base = new URL(appUrl)\n url = new URL(preview, base)\n if (!['http:', 'https:'].includes(base.protocol) || url.origin !== base.origin || url.username || url.password)\n return error('invalid_preview_url', 'The preview must be an HTTP(S) route on the configured app origin')\n } catch {\n return error('invalid_preview_url', 'appUrl must be an absolute HTTP(S) URL and preview must be a valid route')\n }\n\n // Recheck after the asynchronous runtime reads so concurrent MCP callers cannot race this lock.\n if (active) return error('capture_busy', 'Another preview is being captured; retry when it finishes')\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n active = true\n let context: BrowserContext | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n let timedOut = false\n let phase = 'starting the browser'\n try {\n browser ??= import('playwright').then(({ chromium }) => chromium.launch({ headless: true, timeout: 10_000 }))\n let instance: Browser\n try {\n instance = await browser\n } catch (cause) {\n browser = undefined\n return error('capture_unavailable', `Run \"studio install-browser\" to install Chromium. ${String(cause)}`)\n }\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n if (!instance.isConnected()) {\n browser = undefined\n return error('capture_unavailable', 'The capture browser disconnected; retry to start it again')\n }\n context = await instance.newContext({ viewport: { width, height }, deviceScaleFactor: 1 })\n timer = setTimeout(() => {\n timedOut = true\n void context?.close().catch(() => {})\n }, timeoutMs)\n const page = await context.newPage()\n page.setDefaultTimeout(timeoutMs)\n // Preview routes may load external assets, but must not navigate to a different app.\n await page.route('**/*', (route) => {\n const request = route.request()\n return request.isNavigationRequest() &&\n request.frame() === page.mainFrame() &&\n new URL(request.url()).origin !== url.origin\n ? route.abort()\n : route.continue()\n })\n phase = 'opening the preview'\n const response = await page.goto(url.href, { waitUntil: 'load' })\n if (!response?.ok()) return error('preview_failed', `Preview returned HTTP ${response?.status() ?? 'unknown'}`)\n\n phase = 'waiting for the preview to receive the draft'\n const target = { port: runtimePort(), tool, revision }\n const synced = await page.waitForFunction(previewSnapshot, target)\n const initial = await synced.jsonValue()\n await synced.dispose()\n if (initial?.revision !== revision) return error('stale_revision', 'The draft changed while opening the preview')\n\n phase = 'waiting for fonts, images, and animation frames'\n await page.evaluate(async () => {\n await document.fonts.ready\n await Promise.all(Array.from(document.images, (image) => image.decode().catch(() => {})))\n await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))\n })\n const before = await page.evaluate(previewSnapshot, target)\n if (before?.revision !== revision) return error('stale_revision', 'The draft changed while settling the preview')\n\n phase = 'taking the screenshot'\n const png = await page.screenshot({ type: 'png', animations: 'disabled', timeout: timeoutMs })\n const after = await page.evaluate(previewSnapshot, target)\n const latest = await runtime.run('tool.get', { tool })\n if (!latest.ok) return latest\n if (latest.data.draft.revision !== revision || after?.revision !== revision || after.version !== before.version)\n return error('stale_revision', 'The preview changed during capture; read the latest draft and retry')\n return success({ tool, revision, url: page.url(), width, height, settling: 'automatic', png })\n } catch (cause) {\n if (closed) return error('capture_unavailable', 'Studio is shutting down')\n return error(\n timedOut || (cause instanceof Error && cause.name === 'TimeoutError') ? 'preview_not_ready' : 'capture_failed',\n `${phase}: ${String(cause)}`,\n )\n } finally {\n clearTimeout(timer)\n await context?.close().catch(() => {})\n active = false\n }\n }\n\n return {\n capture,\n async close() {\n closed = true\n await browser?.then((instance) => instance.close()).catch(() => {})\n browser = undefined\n },\n }\n}\n\nexport type PreviewCapture = ReturnType<typeof createPreviewCapture>\n","import { McpServer, type CallToolResult } from '@modelcontextprotocol/server'\nimport { z } from 'zod/v4'\nimport type { Op, Operations } from '../core/protocol'\nimport { success } from '../core/result'\nimport type { PreviewCapture } from './capture'\nimport type { Runtime } from './runtime'\n\nconst tool = z.string().min(1).describe('Registered Studio tool ID from studio_list_tools.')\nconst revision = z.number().int().nonnegative().describe('Draft revision returned by the latest read or update.')\n\n/** MCP is another caller of the live runtime; it never owns a second draft or writes files itself. */\nexport function createMcpServer(runtime: Runtime, previews: PreviewCapture) {\n const server = new McpServer(\n { name: 'joyco-studio', version: '0.0.0' },\n {\n instructions:\n 'List Studio tools, read the selected tool, then update its complete draft value. Updates appear live in the playground. Saving is a separate operation. If a revision is stale, read again and reconcile your changes before retrying.',\n },\n )\n const run = async <T extends Op>(op: T, params: Operations[T]['params']): Promise<CallToolResult> => {\n const result = await runtime.run(op, params)\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }],\n structuredContent: result,\n isError: !result.ok,\n }\n }\n\n server.registerTool(\n 'studio_list_tools',\n {\n description:\n 'List registered Studio tools with their IDs, titles, config files, and preview routes. Tools without a file are preview-only: they have no config to read, edit, or capture.',\n inputSchema: z.object({}),\n annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },\n },\n () => run('tools.list', {}),\n )\n server.registerTool(\n 'studio_get_tool',\n {\n description: 'Read a tool’s complete draft, revision, saved value, and dirty/conflict status before editing.',\n inputSchema: z.object({ tool }),\n annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },\n },\n (params) => run('tool.get', params),\n )\n server.registerTool(\n 'studio_update_tool',\n {\n description:\n 'Replace the complete draft config and update all connected previews. Read first and preserve fields you are not changing. The runtime validates types, slider bounds, and select options. Does not save to disk; rejects a stale revision.',\n inputSchema: z.object({ tool, revision, value: z.record(z.string(), z.unknown()) }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n },\n (params) => run('tool.update', params),\n )\n server.registerTool(\n 'studio_save_tool',\n {\n description:\n 'Persist a draft to its registered JSON file. Pass the latest draft revision. Rejects stale revisions and changes made on disk; force explicitly overwrites a disk conflict.',\n inputSchema: z.object({ tool, revision, force: z.boolean().optional() }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },\n },\n (params) => run('tool.save', params),\n )\n server.registerTool(\n 'studio_discard_tool',\n {\n description:\n 'Discard unsaved edits and restore the runtime’s latest saved value in every preview. Read first; rejects a stale revision.',\n inputSchema: z.object({ tool, revision }),\n annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n },\n (params) => run('tool.discard', params),\n )\n server.registerTool(\n 'studio_capture_preview',\n {\n description:\n 'Capture a registered preview as a PNG in a fresh browser page using the live draft. Requires appUrl and Chromium (studio install-browser). Defaults to the current revision and 1200×800. Waits for draft sync, fonts, images, and two animation frames; automatic settling does not guarantee arbitrary async scenes are finished. Rejects a draft change during capture. Does not share your open tab’s scroll, login, or interaction state.',\n inputSchema: z.object({\n tool,\n revision: revision.optional(),\n width: z.number().int().min(320).max(2560).optional(),\n height: z.number().int().min(240).max(1600).optional(),\n }),\n annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true },\n },\n async (params): Promise<CallToolResult> => {\n const captured = await previews.capture(params)\n if (!captured.ok)\n return {\n content: [{ type: 'text', text: JSON.stringify(captured) }],\n structuredContent: captured,\n isError: true,\n }\n const { png, ...metadata } = captured.data\n const result = success(metadata)\n return {\n content: [\n { type: 'text', text: JSON.stringify(result) },\n { type: 'image', mimeType: 'image/png', data: png.toString('base64') },\n ],\n structuredContent: result,\n isError: false,\n }\n },\n )\n return server\n}\n","import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport { createJiti } from 'jiti'\nimport type { StudioConfig } from '../core/tool'\n\nconst CANDIDATES = ['studio.config.ts', 'studio.config.mts', 'studio.config.js', 'studio.config.mjs']\n\n/** Load `studio.config.*` (TypeScript included). Its directory is the project root. */\nexport async function loadConfig(cwd: string, explicit?: string) {\n const file = explicit\n ? path.resolve(cwd, explicit)\n : CANDIDATES.map((name) => path.join(cwd, name)).find((candidate) => existsSync(candidate))\n if (!file || !existsSync(file)) throw new Error(`[studio] no ${explicit ?? 'studio.config.ts'} found in ${cwd}`)\n const jiti = createJiti(import.meta.url, { moduleCache: false })\n const config = await jiti.import<StudioConfig>(file, { default: true })\n if (!Array.isArray(config?.tools))\n throw new Error(`[studio] ${path.basename(file)} must default-export defineConfig({ tools })`)\n return { config, root: path.dirname(file) }\n}\n","import { connect } from 'node:net'\nimport type { Runtime } from './runtime'\nimport { serveBridge, type Bridge, type BridgeOptions } from './server'\n\nconst ATTEMPTS = 20\n\n/** Serve on `port`, or on the next free one when another Studio (or anything else) already holds it. */\nexport async function serveBridgeNear(runtime: Runtime, options: BridgeOptions): Promise<Bridge> {\n for (let port = options.port; ; port++) {\n try {\n return await serveBridge(runtime, { ...options, port })\n } catch (cause) {\n const taken = (cause as NodeJS.ErrnoException).code === 'EADDRINUSE'\n if (!taken || options.port === 0 || port - options.port >= ATTEMPTS - 1) throw cause\n }\n }\n}\n\n/** Whether something already answers at `hostname:port`, on any address family `hostname` resolves to. */\nfunction answers(hostname: string, port: number) {\n return new Promise<boolean>((resolve) => {\n const socket = connect({ host: hostname.replace(/^\\[|\\]$/g, ''), port, timeout: 1000 })\n const done = (busy: boolean) => {\n socket.destroy()\n resolve(busy)\n }\n socket.once('connect', () => done(true))\n socket.once('timeout', () => done(true))\n socket.once('error', () => done(false))\n })\n}\n\n/** First port from `port` that nothing answers on, so a wrapped app server can claim it. */\nexport async function freeAppPort(hostname: string, port: number): Promise<number> {\n for (let candidate = port; candidate < port + ATTEMPTS; candidate++)\n if (!(await answers(hostname, candidate))) return candidate\n throw new Error(`[studio] ports ${port}-${port + ATTEMPTS - 1} on ${hostname} are all in use`)\n}\n"],"mappings":"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","names":["path","tool","path","tool","path","tool","path","revision","tool","revision","handle","path","require","path","child"]}