@joycostudio/studio 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,546 @@
1
+ import {
2
+ validateConfig
3
+ } from "./chunk-VYNEG5Z7.js";
4
+ import {
5
+ error,
6
+ success
7
+ } from "./chunk-QKEAOM2U.js";
8
+
9
+ // src/runtime/runtime.ts
10
+ import { watch } from "fs";
11
+ import path2 from "path";
12
+
13
+ // src/runtime/files.ts
14
+ import { createHash, randomUUID } from "crypto";
15
+ import { mkdir, readFile, realpath, rename, rm, stat } from "fs/promises";
16
+ import { writeFile } from "fs/promises";
17
+ import path from "path";
18
+ var inside = (root, target) => {
19
+ const relative = path.relative(root, target);
20
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
21
+ };
22
+ var exists = (target) => stat(target).then(
23
+ () => true,
24
+ () => false
25
+ );
26
+ async function resolveToolFile(root, file) {
27
+ const fail = (reason) => new Error(`[studio] tool file "${file}" ${reason}`);
28
+ if (path.isAbsolute(file)) throw fail("must be relative to the project root");
29
+ if (path.extname(file) !== ".json") throw fail("must be a .json file");
30
+ const absolute = path.resolve(root, file);
31
+ if (!inside(root, absolute)) throw fail("escapes the project root");
32
+ let probe = absolute;
33
+ while (!await exists(probe)) probe = path.dirname(probe);
34
+ if (probe !== root && !inside(await realpath(root), await realpath(probe))) throw fail("escapes the project root");
35
+ return absolute;
36
+ }
37
+ var hashOf = (content) => createHash("sha256").update(content).digest("hex").slice(0, 16);
38
+ var serialize = (value) => `${JSON.stringify(value, null, 2)}
39
+ `;
40
+ async function readDisk(file) {
41
+ try {
42
+ const content = await readFile(file, "utf8");
43
+ return { content, hash: hashOf(content) };
44
+ } catch (cause) {
45
+ if (cause.code === "ENOENT") return null;
46
+ throw cause;
47
+ }
48
+ }
49
+ async function writeAtomic(file, content) {
50
+ await mkdir(path.dirname(file), { recursive: true });
51
+ const temp = `${file}.${randomUUID()}.tmp`;
52
+ try {
53
+ await writeFile(temp, content, "utf8");
54
+ await rename(temp, file);
55
+ } catch (cause) {
56
+ await rm(temp, { force: true });
57
+ throw cause;
58
+ }
59
+ }
60
+
61
+ // src/runtime/runtime.ts
62
+ var equal = (a, b) => {
63
+ if (a === b) return true;
64
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
65
+ const keys = Object.keys(a);
66
+ return keys.length === Object.keys(b).length && keys.every((key) => equal(a[key], b[key]));
67
+ };
68
+ async function load(tool2, file) {
69
+ const disk = await readDisk(file);
70
+ if (!disk) return { value: structuredClone(tool2.initial), hash: null };
71
+ const parsed = validateConfig(tool2, JSON.parse(disk.content));
72
+ if (!parsed.ok)
73
+ throw new Error(`[studio] ${path2.basename(file)} is not a valid "${tool2.id}" config: ${parsed.message}`);
74
+ return { value: parsed.data, hash: disk.hash };
75
+ }
76
+ async function createRuntime({ root, tools }) {
77
+ root = path2.resolve(root);
78
+ const entries = /* @__PURE__ */ new Map();
79
+ const files = /* @__PURE__ */ new Set();
80
+ for (const tool2 of tools) {
81
+ if (entries.has(tool2.id)) throw new Error(`[studio] duplicate tool id "${tool2.id}"`);
82
+ const file = await resolveToolFile(root, tool2.file);
83
+ if (files.has(file)) throw new Error(`[studio] tool "${tool2.id}" shares its file with another tool`);
84
+ files.add(file);
85
+ const saved = await load(tool2, file);
86
+ entries.set(tool2.id, {
87
+ tool: tool2,
88
+ file,
89
+ queue: Promise.resolve(),
90
+ state: {
91
+ tool: tool2.id,
92
+ draft: { value: structuredClone(saved.value), revision: 0 },
93
+ saved,
94
+ dirty: false,
95
+ conflict: false
96
+ }
97
+ });
98
+ }
99
+ const listeners = /* @__PURE__ */ new Set();
100
+ const commit = (entry, next, origin) => {
101
+ const state = { ...entry.state, ...next };
102
+ state.dirty = !equal(state.draft.value, state.saved.value);
103
+ if (!state.dirty) state.conflict = false;
104
+ entry.state = state;
105
+ for (const listener of listeners) listener(state, origin);
106
+ return state;
107
+ };
108
+ const serial = (entry, task) => {
109
+ const run = entry.queue.then(task);
110
+ entry.queue = run.catch(() => {
111
+ });
112
+ return run;
113
+ };
114
+ const handlers = {
115
+ "tools.list": async () => success(
116
+ [...entries.values()].map(({ tool: tool2 }) => ({
117
+ id: tool2.id,
118
+ title: tool2.title ?? tool2.id,
119
+ file: tool2.file,
120
+ icon: tool2.icon,
121
+ preview: tool2.preview
122
+ }))
123
+ ),
124
+ "tool.get": async ({ tool: tool2 }) => withEntry(tool2, async (entry) => success(entry.state)),
125
+ "tool.update": async ({ tool: tool2, value, revision: revision2 }, origin) => withEntry(tool2, async (entry) => {
126
+ if (revision2 !== void 0 && revision2 !== entry.state.draft.revision)
127
+ return error("stale_revision", `draft is at revision ${entry.state.draft.revision}, not ${revision2}`);
128
+ const parsed = validateConfig(entry.tool, value);
129
+ if (!parsed.ok) return parsed;
130
+ const draft = { value: parsed.data, revision: entry.state.draft.revision + 1 };
131
+ return success(commit(entry, { draft }, origin));
132
+ }),
133
+ "tool.save": async ({ tool: tool2, revision: revision2, force = false }, origin) => withEntry(
134
+ tool2,
135
+ (entry) => serial(entry, async () => {
136
+ const { draft, saved } = entry.state;
137
+ if (revision2 !== draft.revision)
138
+ return error("stale_revision", `draft is at revision ${draft.revision}, not ${revision2}`);
139
+ const disk = await readDisk(entry.file);
140
+ if (!force && (entry.state.conflict || (disk?.hash ?? null) !== saved.hash)) {
141
+ commit(entry, { conflict: true });
142
+ return error("conflict", `${entry.tool.file} changed on disk; discard to reload or save with force`);
143
+ }
144
+ const content = serialize(draft.value);
145
+ await writeAtomic(entry.file, content);
146
+ return success(
147
+ commit(entry, { saved: { value: structuredClone(draft.value), hash: hashOf(content) } }, origin)
148
+ );
149
+ })
150
+ ),
151
+ "tool.discard": async ({ tool: tool2, revision: revision2 }, origin) => withEntry(tool2, async (entry) => {
152
+ const { saved, draft } = entry.state;
153
+ if (revision2 !== void 0 && revision2 !== draft.revision)
154
+ return error("stale_revision", `draft is at revision ${draft.revision}, not ${revision2}`);
155
+ return success(
156
+ commit(entry, { draft: { value: structuredClone(saved.value), revision: draft.revision + 1 } }, origin)
157
+ );
158
+ })
159
+ };
160
+ async function withEntry(id, task) {
161
+ const entry = typeof id === "string" ? entries.get(id) : void 0;
162
+ return entry ? task(entry) : error("unknown_tool", `no tool "${String(id)}"`);
163
+ }
164
+ const reload = (entry) => serial(entry, async () => {
165
+ const disk = await readDisk(entry.file);
166
+ const { saved, draft, dirty } = entry.state;
167
+ if ((disk?.hash ?? null) === saved.hash) return;
168
+ if (!disk) return commit(entry, { saved: { ...saved, hash: null } });
169
+ let value;
170
+ try {
171
+ value = JSON.parse(disk.content);
172
+ } catch {
173
+ return console.warn(`[studio] ignoring unparsable ${entry.tool.file}`);
174
+ }
175
+ const parsed = validateConfig(entry.tool, value);
176
+ if (!parsed.ok) return console.warn(`[studio] ignoring invalid ${entry.tool.file}: ${parsed.message}`);
177
+ const next = { saved: { value: parsed.data, hash: disk.hash } };
178
+ if (dirty) next.conflict = true;
179
+ else next.draft = { value: structuredClone(parsed.data), revision: draft.revision + 1 };
180
+ commit(entry, next);
181
+ });
182
+ const timers = /* @__PURE__ */ new Map();
183
+ const watchers = [];
184
+ const byDirectory = Map.groupBy(entries.values(), (entry) => path2.dirname(entry.file));
185
+ for (const [directory, group] of byDirectory) {
186
+ try {
187
+ const watcher = watch(directory, (_, filename) => {
188
+ for (const entry of group) {
189
+ if (filename && filename !== path2.basename(entry.file)) continue;
190
+ clearTimeout(timers.get(entry));
191
+ timers.set(
192
+ entry,
193
+ setTimeout(() => void reload(entry).catch((cause) => console.error("[studio] reload failed", cause)), 50)
194
+ );
195
+ }
196
+ });
197
+ watchers.push(watcher);
198
+ } catch {
199
+ console.warn(`[studio] not watching ${path2.relative(root, directory)} (it does not exist yet)`);
200
+ }
201
+ }
202
+ return {
203
+ root,
204
+ run: (op, params, origin) => handlers[op](params, origin),
205
+ subscribe(listener) {
206
+ listeners.add(listener);
207
+ return () => listeners.delete(listener);
208
+ },
209
+ close() {
210
+ for (const watcher of watchers) watcher.close();
211
+ for (const timer of timers.values()) clearTimeout(timer);
212
+ listeners.clear();
213
+ }
214
+ };
215
+ }
216
+
217
+ // src/runtime/server.ts
218
+ import { createServer } from "http";
219
+ import { localhostHostValidation, toNodeHandler } from "@modelcontextprotocol/node";
220
+ import { createMcpHandler } from "@modelcontextprotocol/server";
221
+ import { WebSocketServer } from "ws";
222
+
223
+ // src/runtime/capture.ts
224
+ function previewSnapshot({ port, tool: tool2, revision: revision2 }) {
225
+ const instances = globalThis.__joycoStudio;
226
+ for (const [address, studio] of instances ?? []) {
227
+ const url = new URL(address);
228
+ if (!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || Number(url.port) !== port) continue;
229
+ const handle = studio.tools.find((handle2) => handle2.definition.id === tool2);
230
+ if (studio.connection.phase !== "connected" || !handle?.state || handle.comparing) continue;
231
+ if (handle.state.draft.revision < revision2) continue;
232
+ return { revision: handle.state.draft.revision, version: handle.version };
233
+ }
234
+ return null;
235
+ }
236
+ function createPreviewCapture(runtime, { appUrl, runtimePort, timeoutMs = 2e4 }) {
237
+ let browser;
238
+ let active = false;
239
+ let closed = false;
240
+ const capture = async ({
241
+ tool: tool2,
242
+ revision: revision2,
243
+ width = 1200,
244
+ height = 800
245
+ }) => {
246
+ if (closed) return error("capture_unavailable", "Studio is shutting down");
247
+ if (active) return error("capture_busy", "Another preview is being captured; retry when it finishes");
248
+ const state = await runtime.run("tool.get", { tool: tool2 });
249
+ if (!state.ok) return state;
250
+ revision2 ??= state.data.draft.revision;
251
+ if (revision2 !== state.data.draft.revision) return error("stale_revision", "Read the latest draft before capturing");
252
+ const tools = await runtime.run("tools.list", {});
253
+ if (!tools.ok) return tools;
254
+ const preview = tools.data.find((entry) => entry.id === tool2)?.preview;
255
+ if (!preview) return error("preview_missing", `Tool "${tool2}" has no preview route`);
256
+ if (!appUrl) return error("app_url_missing", "Set appUrl in studio.config.ts or pass studio dev --app-url <url>");
257
+ let url;
258
+ try {
259
+ const base = new URL(appUrl);
260
+ url = new URL(preview, base);
261
+ if (!["http:", "https:"].includes(base.protocol) || url.origin !== base.origin || url.username || url.password)
262
+ return error("invalid_preview_url", "The preview must be an HTTP(S) route on the configured app origin");
263
+ } catch {
264
+ return error("invalid_preview_url", "appUrl must be an absolute HTTP(S) URL and preview must be a valid route");
265
+ }
266
+ if (active) return error("capture_busy", "Another preview is being captured; retry when it finishes");
267
+ if (closed) return error("capture_unavailable", "Studio is shutting down");
268
+ active = true;
269
+ let context;
270
+ let timer;
271
+ let timedOut = false;
272
+ let phase = "starting the browser";
273
+ try {
274
+ browser ??= import("playwright").then(({ chromium }) => chromium.launch({ headless: true, timeout: 1e4 }));
275
+ let instance;
276
+ try {
277
+ instance = await browser;
278
+ } catch (cause) {
279
+ browser = void 0;
280
+ return error("capture_unavailable", `Run "studio install-browser" to install Chromium. ${String(cause)}`);
281
+ }
282
+ if (closed) return error("capture_unavailable", "Studio is shutting down");
283
+ if (!instance.isConnected()) {
284
+ browser = void 0;
285
+ return error("capture_unavailable", "The capture browser disconnected; retry to start it again");
286
+ }
287
+ context = await instance.newContext({ viewport: { width, height }, deviceScaleFactor: 1 });
288
+ timer = setTimeout(() => {
289
+ timedOut = true;
290
+ void context?.close().catch(() => {
291
+ });
292
+ }, timeoutMs);
293
+ const page = await context.newPage();
294
+ page.setDefaultTimeout(timeoutMs);
295
+ await page.route("**/*", (route) => {
296
+ const request = route.request();
297
+ return request.isNavigationRequest() && request.frame() === page.mainFrame() && new URL(request.url()).origin !== url.origin ? route.abort() : route.continue();
298
+ });
299
+ phase = "opening the preview";
300
+ const response = await page.goto(url.href, { waitUntil: "load" });
301
+ if (!response?.ok()) return error("preview_failed", `Preview returned HTTP ${response?.status() ?? "unknown"}`);
302
+ phase = "waiting for the preview to receive the draft";
303
+ const target = { port: runtimePort(), tool: tool2, revision: revision2 };
304
+ const synced = await page.waitForFunction(previewSnapshot, target);
305
+ const initial = await synced.jsonValue();
306
+ await synced.dispose();
307
+ if (initial?.revision !== revision2) return error("stale_revision", "The draft changed while opening the preview");
308
+ phase = "waiting for fonts, images, and animation frames";
309
+ await page.evaluate(async () => {
310
+ await document.fonts.ready;
311
+ await Promise.all(Array.from(document.images, (image) => image.decode().catch(() => {
312
+ })));
313
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
314
+ });
315
+ const before = await page.evaluate(previewSnapshot, target);
316
+ if (before?.revision !== revision2) return error("stale_revision", "The draft changed while settling the preview");
317
+ phase = "taking the screenshot";
318
+ const png = await page.screenshot({ type: "png", animations: "disabled", timeout: timeoutMs });
319
+ const after = await page.evaluate(previewSnapshot, target);
320
+ const latest = await runtime.run("tool.get", { tool: tool2 });
321
+ if (!latest.ok) return latest;
322
+ if (latest.data.draft.revision !== revision2 || after?.revision !== revision2 || after.version !== before.version)
323
+ return error("stale_revision", "The preview changed during capture; read the latest draft and retry");
324
+ return success({ tool: tool2, revision: revision2, url: page.url(), width, height, settling: "automatic", png });
325
+ } catch (cause) {
326
+ if (closed) return error("capture_unavailable", "Studio is shutting down");
327
+ return error(
328
+ timedOut || cause instanceof Error && cause.name === "TimeoutError" ? "preview_not_ready" : "capture_failed",
329
+ `${phase}: ${String(cause)}`
330
+ );
331
+ } finally {
332
+ clearTimeout(timer);
333
+ await context?.close().catch(() => {
334
+ });
335
+ active = false;
336
+ }
337
+ };
338
+ return {
339
+ capture,
340
+ async close() {
341
+ closed = true;
342
+ await browser?.then((instance) => instance.close()).catch(() => {
343
+ });
344
+ browser = void 0;
345
+ }
346
+ };
347
+ }
348
+
349
+ // src/runtime/mcp.ts
350
+ import { McpServer } from "@modelcontextprotocol/server";
351
+ import { z } from "zod/v4";
352
+ var tool = z.string().min(1).describe("Registered Studio tool ID from studio_list_tools.");
353
+ var revision = z.number().int().nonnegative().describe("Draft revision returned by the latest read or update.");
354
+ function createMcpServer(runtime, previews) {
355
+ const server = new McpServer(
356
+ { name: "joyco-studio", version: "0.0.0" },
357
+ {
358
+ instructions: "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."
359
+ }
360
+ );
361
+ const run = async (op, params) => {
362
+ const result = await runtime.run(op, params);
363
+ return {
364
+ content: [{ type: "text", text: JSON.stringify(result) }],
365
+ structuredContent: result,
366
+ isError: !result.ok
367
+ };
368
+ };
369
+ server.registerTool(
370
+ "studio_list_tools",
371
+ {
372
+ description: "List registered Studio tools with their IDs, titles, config files, and preview routes.",
373
+ inputSchema: z.object({}),
374
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
375
+ },
376
+ () => run("tools.list", {})
377
+ );
378
+ server.registerTool(
379
+ "studio_get_tool",
380
+ {
381
+ description: "Read a tool\u2019s complete draft, revision, saved value, and dirty/conflict status before editing.",
382
+ inputSchema: z.object({ tool }),
383
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
384
+ },
385
+ (params) => run("tool.get", params)
386
+ );
387
+ server.registerTool(
388
+ "studio_update_tool",
389
+ {
390
+ description: "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.",
391
+ inputSchema: z.object({ tool, revision, value: z.record(z.string(), z.unknown()) }),
392
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
393
+ },
394
+ (params) => run("tool.update", params)
395
+ );
396
+ server.registerTool(
397
+ "studio_save_tool",
398
+ {
399
+ description: "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.",
400
+ inputSchema: z.object({ tool, revision, force: z.boolean().optional() }),
401
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
402
+ },
403
+ (params) => run("tool.save", params)
404
+ );
405
+ server.registerTool(
406
+ "studio_discard_tool",
407
+ {
408
+ description: "Discard unsaved edits and restore the runtime\u2019s latest saved value in every preview. Read first; rejects a stale revision.",
409
+ inputSchema: z.object({ tool, revision }),
410
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
411
+ },
412
+ (params) => run("tool.discard", params)
413
+ );
414
+ server.registerTool(
415
+ "studio_capture_preview",
416
+ {
417
+ description: "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\xD7800. 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\u2019s scroll, login, or interaction state.",
418
+ inputSchema: z.object({
419
+ tool,
420
+ revision: revision.optional(),
421
+ width: z.number().int().min(320).max(2560).optional(),
422
+ height: z.number().int().min(240).max(1600).optional()
423
+ }),
424
+ annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true }
425
+ },
426
+ async (params) => {
427
+ const captured = await previews.capture(params);
428
+ if (!captured.ok)
429
+ return {
430
+ content: [{ type: "text", text: JSON.stringify(captured) }],
431
+ structuredContent: captured,
432
+ isError: true
433
+ };
434
+ const { png, ...metadata } = captured.data;
435
+ const result = success(metadata);
436
+ return {
437
+ content: [
438
+ { type: "text", text: JSON.stringify(result) },
439
+ { type: "image", mimeType: "image/png", data: png.toString("base64") }
440
+ ],
441
+ structuredContent: result,
442
+ isError: false
443
+ };
444
+ }
445
+ );
446
+ return server;
447
+ }
448
+
449
+ // src/runtime/server.ts
450
+ var LOCAL_ORIGIN = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
451
+ var OPS = /* @__PURE__ */ new Set(["tools.list", "tool.get", "tool.update", "tool.save", "tool.discard"]);
452
+ function parse(raw) {
453
+ try {
454
+ const message = JSON.parse(raw);
455
+ if (typeof message?.id !== "number" || !OPS.has(message.op)) return null;
456
+ return { id: message.id, op: message.op, params: message.params ?? {} };
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+ function serveBridge(runtime, { port, host = "127.0.0.1", origins, appUrl }) {
462
+ const allowed = (origin) => origin === void 0 || (origins ? origins.includes(origin) : LOCAL_ORIGIN.test(origin));
463
+ const onerror = (cause) => console.error("[studio] MCP request failed", cause);
464
+ const previews = createPreviewCapture(runtime, { appUrl, runtimePort: () => http.address().port });
465
+ const mcp = createMcpHandler(() => createMcpServer(runtime, previews), { onerror });
466
+ const handleMcp = toNodeHandler(mcp, { onerror });
467
+ const validateHost = localhostHostValidation();
468
+ const http = createServer((request, response) => {
469
+ if (request.url?.split("?")[0] !== "/mcp") {
470
+ response.writeHead(404).end();
471
+ return;
472
+ }
473
+ if (!validateHost(request, response)) return;
474
+ if (!allowed(request.headers.origin)) {
475
+ response.writeHead(403).end("Origin not allowed");
476
+ return;
477
+ }
478
+ void handleMcp(request, response);
479
+ });
480
+ const server = new WebSocketServer({
481
+ server: http,
482
+ verifyClient: ({ origin }) => allowed(origin)
483
+ });
484
+ const send = (socket, message) => {
485
+ if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(message));
486
+ };
487
+ server.on("connection", (socket) => {
488
+ socket.on("message", async (raw) => {
489
+ const request = parse(raw.toString());
490
+ if (!request) return send(socket, { type: "response", id: -1, result: error("bad_request") });
491
+ const result = await runtime.run(request.op, request.params, socket).catch((cause) => error("internal", cause instanceof Error ? cause.message : String(cause)));
492
+ send(socket, { type: "response", id: request.id, result });
493
+ });
494
+ });
495
+ const unsubscribe = runtime.subscribe((state, origin) => {
496
+ for (const client of server.clients) if (client !== origin) send(client, { type: "state", state });
497
+ });
498
+ return new Promise((resolve, reject) => {
499
+ server.once("error", (cause) => {
500
+ unsubscribe();
501
+ server.close();
502
+ void mcp.close();
503
+ void previews.close();
504
+ reject(cause);
505
+ });
506
+ http.once(
507
+ "listening",
508
+ () => resolve({
509
+ port: http.address().port,
510
+ close: async () => {
511
+ unsubscribe();
512
+ await previews.close();
513
+ for (const client of server.clients) client.terminate();
514
+ await mcp.close();
515
+ await new Promise((done) => server.close(() => done()));
516
+ await new Promise((done) => {
517
+ http.close(() => done());
518
+ http.closeAllConnections();
519
+ });
520
+ }
521
+ })
522
+ );
523
+ http.listen(port, host);
524
+ });
525
+ }
526
+
527
+ // src/runtime/load.ts
528
+ import { existsSync } from "fs";
529
+ import path3 from "path";
530
+ import { createJiti } from "jiti";
531
+ var CANDIDATES = ["studio.config.ts", "studio.config.mts", "studio.config.js", "studio.config.mjs"];
532
+ async function loadConfig(cwd, explicit) {
533
+ const file = explicit ? path3.resolve(cwd, explicit) : CANDIDATES.map((name) => path3.join(cwd, name)).find((candidate) => existsSync(candidate));
534
+ if (!file || !existsSync(file)) throw new Error(`[studio] no ${explicit ?? "studio.config.ts"} found in ${cwd}`);
535
+ const jiti = createJiti(import.meta.url, { moduleCache: false });
536
+ const config = await jiti.import(file, { default: true });
537
+ if (!Array.isArray(config?.tools))
538
+ throw new Error(`[studio] ${path3.basename(file)} must default-export defineConfig({ tools })`);
539
+ return { config, root: path3.dirname(file) };
540
+ }
541
+ export {
542
+ createRuntime,
543
+ loadConfig,
544
+ serveBridge
545
+ };
546
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/runtime.ts","../src/runtime/files.ts","../src/runtime/server.ts","../src/runtime/capture.ts","../src/runtime/mcp.ts","../src/runtime/load.ts"],"sourcesContent":["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","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":"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","names":["path","tool","path","revision","tool","revision","handle","path"]}