@joycostudio/studio 0.0.0-stage → 0.0.2

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 @@
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}\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":"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","names":["path","path","tool","path","tool","path","revision","tool","revision","handle","path","require","path","child"]}
@@ -0,0 +1,63 @@
1
+ import { DebugSchema, DebugFolder } from '@joycostudio/xyz/debug';
2
+ import { T as ToolDefinition, a as ToolState, R as Result, A as AnyTool } from './protocol-Bl62UeCa.js';
3
+
4
+ type Connection = {
5
+ phase: 'local';
6
+ } | {
7
+ phase: 'connecting';
8
+ } | {
9
+ phase: 'connected';
10
+ } | {
11
+ phase: 'offline';
12
+ };
13
+
14
+ type ToolHandle<TConfig extends object = any> = {
15
+ readonly definition: ToolDefinition<TConfig>;
16
+ /** The live config. Read it to apply; writes (from any pane, or your code) sync to the runtime. */
17
+ readonly config: TConfig;
18
+ /** Runtime-owned state; null until the runtime answers (always null without one). */
19
+ readonly state: ToolState<TConfig> | null;
20
+ /** Bumps on every config change. Handy as a React snapshot. */
21
+ readonly version: number;
22
+ /** Called after every config change, local or remote. */
23
+ subscribe(listener: (config: TConfig) => void): () => void;
24
+ /** The tool's Xyz schema bound to the live config. */
25
+ controls(): DebugSchema;
26
+ /** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */
27
+ debug(): DebugFolder;
28
+ /** True while `compare(true)` shows the saved config in place of the draft. */
29
+ readonly comparing: boolean;
30
+ /**
31
+ * Temporarily show the saved config (true) or go back to the draft (false). Local only: nothing
32
+ * reaches the runtime, and edits made while comparing are dropped when it ends.
33
+ */
34
+ compare(showSaved: boolean): void;
35
+ save(options?: {
36
+ force?: boolean;
37
+ }): Promise<Result<ToolState<TConfig>>>;
38
+ discard(): Promise<Result<ToolState<TConfig>>>;
39
+ };
40
+ type Studio = {
41
+ readonly tools: readonly ToolHandle[];
42
+ tool<TConfig extends object>(definition: ToolDefinition<TConfig>): ToolHandle<TConfig>;
43
+ readonly connection: Connection;
44
+ /** Bumps on connection or any tool state change. */
45
+ readonly version: number;
46
+ subscribe(listener: () => void): () => void;
47
+ close(): void;
48
+ };
49
+ type StudioOptions = {
50
+ tools: readonly AnyTool[];
51
+ /**
52
+ * Runtime bridge URL (e.g. `ws://127.0.0.1:5179`). Omit or pass false to stay local, as in production.
53
+ * Only browsers connect, so a module-scope `createStudio` is safe to evaluate during SSR.
54
+ */
55
+ runtime?: string | false;
56
+ };
57
+ /** Make the studio running in `target` (e.g. a preview iframe) compare `tool` as well. */
58
+ declare function postCompare(target: Window | null | undefined, tool: string, showSaved: boolean): void;
59
+ /** A `registerFrame` for Xyz's `DebugManager` / `bindDebugSchema`: refreshes live controls every animation frame. */
60
+ declare function everyFrame(refresh: () => void): () => void;
61
+ declare function createStudio({ tools, runtime }: StudioOptions): Studio;
62
+
63
+ export { type Connection, type Studio, type StudioOptions, type ToolHandle, createStudio, everyFrame, postCompare };
package/dist/client.js ADDED
@@ -0,0 +1,273 @@
1
+ import {
2
+ error
3
+ } from "./chunk-QKEAOM2U.js";
4
+
5
+ // src/client/studio.ts
6
+ import { button } from "@joycostudio/xyz/debug";
7
+
8
+ // src/client/bridge.ts
9
+ function connectBridge(url, handlers) {
10
+ let socket = null;
11
+ let nextId = 0;
12
+ let retry = 0;
13
+ let timer;
14
+ let closed = false;
15
+ const pending = /* @__PURE__ */ new Map();
16
+ const open = () => {
17
+ handlers.connection({ phase: "connecting" });
18
+ const ws = new WebSocket(url);
19
+ socket = ws;
20
+ ws.onopen = () => {
21
+ retry = 0;
22
+ handlers.connection({ phase: "connected" });
23
+ };
24
+ ws.onmessage = (event) => {
25
+ const message = JSON.parse(String(event.data));
26
+ if (message.type === "state") return handlers.state(message.state);
27
+ pending.get(message.id)?.(message.result);
28
+ pending.delete(message.id);
29
+ };
30
+ ws.onclose = () => {
31
+ socket = null;
32
+ for (const resolve of pending.values()) resolve(error("runtime_unavailable"));
33
+ pending.clear();
34
+ if (closed) return;
35
+ handlers.connection({ phase: "offline" });
36
+ timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5e3));
37
+ };
38
+ };
39
+ open();
40
+ return {
41
+ request(op, params) {
42
+ if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error("runtime_unavailable"));
43
+ const id = nextId++;
44
+ socket.send(JSON.stringify({ id, op, params }));
45
+ return new Promise((resolve) => pending.set(id, resolve));
46
+ },
47
+ close() {
48
+ closed = true;
49
+ clearTimeout(timer);
50
+ socket?.close();
51
+ }
52
+ };
53
+ }
54
+
55
+ // src/client/live.ts
56
+ function live(target, onWrite) {
57
+ const proxies = /* @__PURE__ */ new WeakMap();
58
+ const wrap = (object) => {
59
+ let proxy = proxies.get(object);
60
+ if (!proxy) {
61
+ proxy = new Proxy(object, {
62
+ get(target2, key, receiver) {
63
+ const value = Reflect.get(target2, key, receiver);
64
+ return typeof value === "object" && value !== null ? wrap(value) : value;
65
+ },
66
+ set(target2, key, value, receiver) {
67
+ const changed = Reflect.get(target2, key, receiver) !== value;
68
+ const ok = Reflect.set(target2, key, value, receiver);
69
+ if (changed) onWrite();
70
+ return ok;
71
+ }
72
+ });
73
+ proxies.set(object, proxy);
74
+ }
75
+ return proxy;
76
+ };
77
+ return wrap(target);
78
+ }
79
+ function assignDeep(target, source) {
80
+ for (const [key, value] of Object.entries(source)) {
81
+ const current = target[key];
82
+ const nested = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
83
+ if (nested(current) && nested(value))
84
+ assignDeep(current, value);
85
+ else target[key] = structuredClone(value);
86
+ }
87
+ }
88
+
89
+ // src/client/studio.ts
90
+ var COMPARE_MESSAGE = "joyco-studio:compare";
91
+ function postCompare(target, tool, showSaved) {
92
+ const message = { type: COMPARE_MESSAGE, tool, showSaved };
93
+ target?.postMessage(message, location.origin);
94
+ }
95
+ function liveSchema(schema) {
96
+ return Object.fromEntries(
97
+ Object.entries(schema).map(([key, field]) => {
98
+ if (field.kind === "button") return [key, field];
99
+ if (field.kind === "folder")
100
+ return [
101
+ key,
102
+ { ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } }
103
+ ];
104
+ return [key, { ...field, live: true }];
105
+ })
106
+ );
107
+ }
108
+ function everyFrame(refresh) {
109
+ let frame = requestAnimationFrame(function tick() {
110
+ refresh();
111
+ frame = requestAnimationFrame(tick);
112
+ });
113
+ return () => cancelAnimationFrame(frame);
114
+ }
115
+ var instances = globalThis.__joycoStudio ??= /* @__PURE__ */ new Map();
116
+ function createStudio({ tools, runtime }) {
117
+ let bridge = null;
118
+ let connection = { phase: runtime ? "connecting" : "local" };
119
+ let version = 0;
120
+ const studioListeners = /* @__PURE__ */ new Set();
121
+ const touch = () => {
122
+ version++;
123
+ for (const listener of studioListeners) listener();
124
+ };
125
+ const createHandle = (definition) => {
126
+ const raw = structuredClone(definition.initial);
127
+ const listeners = /* @__PURE__ */ new Set();
128
+ let pushing = Promise.resolve();
129
+ let scheduled = false;
130
+ let stashed = null;
131
+ const changed = () => {
132
+ handle.version++;
133
+ for (const listener of listeners) listener(handle.config);
134
+ };
135
+ const adopt = (state) => {
136
+ handle.state = state;
137
+ touch();
138
+ };
139
+ const receive = (state) => {
140
+ if (stashed) stashed = structuredClone(state.draft.value);
141
+ else assignDeep(raw, state.draft.value);
142
+ adopt(state);
143
+ if (!stashed) changed();
144
+ };
145
+ const resync = async () => {
146
+ const result = await bridge?.request("tool.get", { tool: definition.id });
147
+ if (result?.ok) receive(result.data);
148
+ };
149
+ const push = () => {
150
+ if (scheduled || !bridge) return;
151
+ scheduled = true;
152
+ pushing = Promise.resolve().then(async () => {
153
+ scheduled = false;
154
+ const result = await bridge?.request("tool.update", { tool: definition.id, value: raw });
155
+ if (!result) return;
156
+ if (result.ok) adopt(result.data);
157
+ else if (result.error !== "runtime_unavailable") {
158
+ console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`);
159
+ await resync();
160
+ }
161
+ });
162
+ };
163
+ const handle = {
164
+ definition,
165
+ config: live(raw, () => {
166
+ changed();
167
+ if (!stashed) push();
168
+ }),
169
+ state: null,
170
+ version: 0,
171
+ comparing: false,
172
+ compare(showSaved) {
173
+ if (showSaved === handle.comparing || showSaved && !handle.state) return;
174
+ if (showSaved) stashed = structuredClone(raw);
175
+ const next = showSaved ? handle.state.saved.value : stashed;
176
+ assignDeep(raw, next);
177
+ if (!showSaved) stashed = null;
178
+ handle.comparing = showSaved;
179
+ changed();
180
+ touch();
181
+ },
182
+ subscribe(listener) {
183
+ listeners.add(listener);
184
+ return () => void listeners.delete(listener);
185
+ },
186
+ controls: () => definition.controls(handle.config),
187
+ debug() {
188
+ const actions = runtime ? {
189
+ save: button("Save", () => handle.save().then(fail), "Saved \u2713"),
190
+ discard: button("Discard", () => handle.discard().then(fail), "Discarded")
191
+ } : {};
192
+ return {
193
+ title: definition.title ?? definition.id,
194
+ schema: () => ({ ...liveSchema(handle.controls()), ...actions })
195
+ };
196
+ },
197
+ async save({ force = false } = {}) {
198
+ if (!bridge) return error("runtime_unavailable");
199
+ await pushing;
200
+ const revision = handle.state?.draft.revision;
201
+ if (revision === void 0) return error("runtime_unavailable");
202
+ const result = await bridge.request("tool.save", { tool: definition.id, revision, force });
203
+ if (result.ok) adopt(result.data);
204
+ return result;
205
+ },
206
+ async discard() {
207
+ if (!bridge) return error("runtime_unavailable");
208
+ await pushing;
209
+ const result = await bridge.request("tool.discard", { tool: definition.id });
210
+ if (result.ok) receive(result.data);
211
+ return result;
212
+ },
213
+ receive,
214
+ resync
215
+ };
216
+ return handle;
217
+ };
218
+ const handles = new Map(tools.map((definition) => [definition.id, createHandle(definition)]));
219
+ const onMessage = (event) => {
220
+ const message = event.data;
221
+ if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return;
222
+ handles.get(message.tool)?.compare(message.showSaved);
223
+ };
224
+ if (typeof window !== "undefined") window.addEventListener?.("message", onMessage);
225
+ if (runtime && typeof window !== "undefined") {
226
+ bridge = connectBridge(runtime, {
227
+ state: (state) => handles.get(state.tool)?.receive(state),
228
+ connection(next) {
229
+ connection = next;
230
+ touch();
231
+ if (next.phase === "connected") for (const handle of handles.values()) void handle.resync();
232
+ }
233
+ });
234
+ }
235
+ const studio = {
236
+ tools: [...handles.values()],
237
+ tool(definition) {
238
+ const handle = handles.get(definition.id);
239
+ if (!handle) throw new Error(`[studio] tool "${definition.id}" was not passed to createStudio`);
240
+ return handle;
241
+ },
242
+ get connection() {
243
+ return connection;
244
+ },
245
+ get version() {
246
+ return version;
247
+ },
248
+ subscribe(listener) {
249
+ studioListeners.add(listener);
250
+ return () => void studioListeners.delete(listener);
251
+ },
252
+ close() {
253
+ bridge?.close();
254
+ studioListeners.clear();
255
+ if (typeof window !== "undefined") window.removeEventListener?.("message", onMessage);
256
+ if (runtime && instances.get(runtime) === studio) instances.delete(runtime);
257
+ }
258
+ };
259
+ if (bridge && runtime) {
260
+ instances.get(runtime)?.close();
261
+ instances.set(runtime, studio);
262
+ }
263
+ return studio;
264
+ }
265
+ function fail(result) {
266
+ if (!result.ok) throw new Error(result.message ?? result.error);
267
+ }
268
+ export {
269
+ createStudio,
270
+ everyFrame,
271
+ postCompare
272
+ };
273
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/studio.ts","../src/client/bridge.ts","../src/client/live.ts"],"sourcesContent":["import { button, type DebugField, type DebugFolder, type DebugSchema } from '@joycostudio/xyz/debug'\nimport type { ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\nimport type { AnyTool, ToolDefinition } from '../core/tool'\nimport { connectBridge, type BridgeClient, type Connection } from './bridge'\nimport { assignDeep, live } from './live'\n\nexport type ToolHandle<TConfig extends object = any> = {\n readonly definition: ToolDefinition<TConfig>\n /** The live config. Read it to apply; writes (from any pane, or your code) sync to the runtime. */\n readonly config: TConfig\n /** Runtime-owned state; null until the runtime answers (always null without one). */\n readonly state: ToolState<TConfig> | null\n /** Bumps on every config change. Handy as a React snapshot. */\n readonly version: number\n /** Called after every config change, local or remote. */\n subscribe(listener: (config: TConfig) => void): () => void\n /** The tool's Xyz schema bound to the live config. */\n controls(): DebugSchema\n /** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */\n debug(): DebugFolder\n /** True while `compare(true)` shows the saved config in place of the draft. */\n readonly comparing: boolean\n /**\n * Temporarily show the saved config (true) or go back to the draft (false). Local only: nothing\n * reaches the runtime, and edits made while comparing are dropped when it ends.\n */\n compare(showSaved: boolean): void\n save(options?: { force?: boolean }): Promise<Result<ToolState<TConfig>>>\n discard(): Promise<Result<ToolState<TConfig>>>\n}\n\nexport type Studio = {\n readonly tools: readonly ToolHandle[]\n tool<TConfig extends object>(definition: ToolDefinition<TConfig>): ToolHandle<TConfig>\n readonly connection: Connection\n /** Bumps on connection or any tool state change. */\n readonly version: number\n subscribe(listener: () => void): () => void\n close(): void\n}\n\nexport type StudioOptions = {\n tools: readonly AnyTool[]\n /**\n * Runtime bridge URL (e.g. `ws://127.0.0.1:5179`). Omit or pass false to stay local, as in production.\n * Only browsers connect, so a module-scope `createStudio` is safe to evaluate during SSR.\n */\n runtime?: string | false\n}\n\nconst COMPARE_MESSAGE = 'joyco-studio:compare'\ntype CompareMessage = { type: typeof COMPARE_MESSAGE; tool: string; showSaved: boolean }\n\n/** Make the studio running in `target` (e.g. a preview iframe) compare `tool` as well. */\nexport function postCompare(target: Window | null | undefined, tool: string, showSaved: boolean) {\n const message: CompareMessage = { type: COMPARE_MESSAGE, tool, showSaved }\n target?.postMessage(message, location.origin)\n}\n\n/** Mark every value control live so a pane registered with `registerFrame` reflects remote edits. */\nfunction liveSchema(schema: DebugSchema): DebugSchema {\n return Object.fromEntries(\n Object.entries(schema).map(([key, field]): [string, DebugField] => {\n if (field.kind === 'button') return [key, field]\n if (field.kind === 'folder')\n return [\n key,\n { ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } },\n ]\n return [key, { ...field, live: true }]\n }),\n )\n}\n\n/** A `registerFrame` for Xyz's `DebugManager` / `bindDebugSchema`: refreshes live controls every animation frame. */\nexport function everyFrame(refresh: () => void) {\n let frame = requestAnimationFrame(function tick() {\n refresh()\n frame = requestAnimationFrame(tick)\n })\n return () => cancelAnimationFrame(frame)\n}\n\n/** One studio per runtime URL per page: HMR re-evaluating `createStudio` replaces the previous instance instead of leaking its socket. */\nconst instances: Map<string, Studio> = ((globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio ??=\n new Map())\n\nexport function createStudio({ tools, runtime }: StudioOptions): Studio {\n let bridge: BridgeClient | null = null\n let connection: Connection = { phase: runtime ? 'connecting' : 'local' }\n let version = 0\n const studioListeners = new Set<() => void>()\n const touch = () => {\n version++\n for (const listener of studioListeners) listener()\n }\n\n const createHandle = <TConfig extends object>(definition: ToolDefinition<TConfig>) => {\n const raw = structuredClone(definition.initial)\n const listeners = new Set<(config: TConfig) => void>()\n let pushing: Promise<unknown> = Promise.resolve()\n let scheduled = false\n /** The draft to restore while comparing; null when showing the draft. */\n let stashed: TConfig | null = null\n\n const changed = () => {\n handle.version++\n for (const listener of listeners) listener(handle.config)\n }\n const adopt = (state: ToolState) => {\n handle.state = state as ToolState<TConfig>\n touch()\n }\n const receive = (state: ToolState) => {\n if (stashed) stashed = structuredClone(state.draft.value) as TConfig\n else assignDeep(raw as Record<string, unknown>, state.draft.value as Record<string, unknown>)\n adopt(state)\n if (!stashed) changed()\n }\n const resync = async () => {\n const result = await bridge?.request('tool.get', { tool: definition.id })\n if (result?.ok) receive(result.data)\n }\n const push = () => {\n if (scheduled || !bridge) return\n scheduled = true\n pushing = Promise.resolve().then(async () => {\n scheduled = false\n const result = await bridge?.request('tool.update', { tool: definition.id, value: raw })\n if (!result) return\n if (result.ok) adopt(result.data)\n else if (result.error !== 'runtime_unavailable') {\n console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`)\n await resync()\n }\n })\n }\n\n const handle = {\n definition,\n config: live(raw, () => {\n changed()\n if (!stashed) push()\n }),\n state: null as ToolState<TConfig> | null,\n version: 0,\n comparing: false,\n compare(showSaved: boolean) {\n if (showSaved === handle.comparing || (showSaved && !handle.state)) return\n if (showSaved) stashed = structuredClone(raw)\n const next = showSaved ? handle.state!.saved.value : stashed!\n assignDeep(raw as Record<string, unknown>, next as Record<string, unknown>)\n if (!showSaved) stashed = null\n handle.comparing = showSaved\n changed()\n touch()\n },\n subscribe(listener: (config: TConfig) => void) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n controls: () => definition.controls(handle.config),\n debug(): DebugFolder {\n const actions: DebugSchema = runtime\n ? {\n save: button('Save', () => handle.save().then(fail), 'Saved ✓'),\n discard: button('Discard', () => handle.discard().then(fail), 'Discarded'),\n }\n : {}\n return {\n title: definition.title ?? definition.id,\n schema: () => ({ ...liveSchema(handle.controls()), ...actions }),\n }\n },\n async save({ force = false } = {}) {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const revision = handle.state?.draft.revision\n if (revision === undefined) return error('runtime_unavailable')\n const result = await bridge.request('tool.save', { tool: definition.id, revision, force })\n if (result.ok) adopt(result.data)\n return result as Result<ToolState<TConfig>>\n },\n async discard() {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const result = await bridge.request('tool.discard', { tool: definition.id })\n if (result.ok) receive(result.data)\n return result as Result<ToolState<TConfig>>\n },\n receive,\n resync,\n }\n return handle\n }\n\n const handles = new Map(tools.map((definition) => [definition.id, createHandle(definition)]))\n\n // /studio asks its same-origin preview iframe to compare too (see `postCompare`).\n const onMessage = (event: MessageEvent) => {\n const message = event.data as CompareMessage | undefined\n if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return\n handles.get(message.tool)?.compare(message.showSaved)\n }\n if (typeof window !== 'undefined') window.addEventListener?.('message', onMessage)\n\n if (runtime && typeof window !== 'undefined') {\n bridge = connectBridge(runtime, {\n state: (state) => handles.get(state.tool)?.receive(state),\n connection(next) {\n connection = next\n touch()\n if (next.phase === 'connected') for (const handle of handles.values()) void handle.resync()\n },\n })\n }\n\n const studio: Studio = {\n tools: [...handles.values()],\n tool<TConfig extends object>(definition: ToolDefinition<TConfig>) {\n const handle = handles.get(definition.id)\n if (!handle) throw new Error(`[studio] tool \"${definition.id}\" was not passed to createStudio`)\n return handle as unknown as ToolHandle<TConfig>\n },\n get connection() {\n return connection\n },\n get version() {\n return version\n },\n subscribe(listener) {\n studioListeners.add(listener)\n return () => void studioListeners.delete(listener)\n },\n close() {\n bridge?.close()\n studioListeners.clear()\n if (typeof window !== 'undefined') window.removeEventListener?.('message', onMessage)\n if (runtime && instances.get(runtime) === studio) instances.delete(runtime)\n },\n }\n if (bridge && runtime) {\n instances.get(runtime)?.close()\n instances.set(runtime, studio)\n }\n return studio\n}\n\nfunction fail(result: Result<unknown>) {\n if (!result.ok) throw new Error(result.message ?? result.error)\n}\n","import type { Op, Operations, ServerMessage, ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\n\nexport type Connection = { phase: 'local' } | { phase: 'connecting' } | { phase: 'connected' } | { phase: 'offline' }\n\ntype Pending = (result: Result<unknown>) => void\n\n/** A reconnecting WebSocket to the Studio runtime with request/response correlation. */\nexport function connectBridge(\n url: string,\n handlers: { state: (state: ToolState) => void; connection: (connection: Connection) => void },\n) {\n let socket: WebSocket | null = null\n let nextId = 0\n let retry = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let closed = false\n const pending = new Map<number, Pending>()\n\n const open = () => {\n handlers.connection({ phase: 'connecting' })\n const ws = new WebSocket(url)\n socket = ws\n ws.onopen = () => {\n retry = 0\n handlers.connection({ phase: 'connected' })\n }\n ws.onmessage = (event) => {\n const message = JSON.parse(String(event.data)) as ServerMessage\n if (message.type === 'state') return handlers.state(message.state)\n pending.get(message.id)?.(message.result)\n pending.delete(message.id)\n }\n ws.onclose = () => {\n socket = null\n for (const resolve of pending.values()) resolve(error('runtime_unavailable'))\n pending.clear()\n if (closed) return\n handlers.connection({ phase: 'offline' })\n timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5000))\n }\n }\n open()\n\n return {\n request<TOp extends Op>(op: TOp, params: Operations[TOp]['params']): Promise<Result<Operations[TOp]['result']>> {\n if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error('runtime_unavailable'))\n const id = nextId++\n socket.send(JSON.stringify({ id, op, params }))\n return new Promise((resolve) => pending.set(id, resolve as Pending))\n },\n close() {\n closed = true\n clearTimeout(timer)\n socket?.close()\n },\n }\n}\n\nexport type BridgeClient = ReturnType<typeof connectBridge>\n","/**\n * A deep proxy that reports every write, so plain `property(config, 'key')`\n * bindings from Xyz feed Studio without the tool author wiring callbacks.\n */\nexport function live<T extends object>(target: T, onWrite: () => void): T {\n const proxies = new WeakMap<object, object>()\n const wrap = <O extends object>(object: O): O => {\n let proxy = proxies.get(object)\n if (!proxy) {\n proxy = new Proxy(object, {\n get(target, key, receiver) {\n const value = Reflect.get(target, key, receiver)\n return typeof value === 'object' && value !== null ? wrap(value) : value\n },\n set(target, key, value, receiver) {\n const changed = Reflect.get(target, key, receiver) !== value\n const ok = Reflect.set(target, key, value, receiver)\n if (changed) onWrite()\n return ok\n },\n })\n proxies.set(object, proxy)\n }\n return proxy as O\n }\n return wrap(target)\n}\n\n/** Copy `source` into `target` in place, keeping nested object identities (and every binding to them) intact. */\nexport function assignDeep(target: Record<string, unknown>, source: Record<string, unknown>) {\n for (const [key, value] of Object.entries(source)) {\n const current = target[key]\n const nested = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)\n if (nested(current) && nested(value))\n assignDeep(current as Record<string, unknown>, value as Record<string, unknown>)\n else target[key] = structuredClone(value)\n }\n}\n"],"mappings":"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","names":["target"]}
@@ -0,0 +1 @@
1
+ [data-slot=studio-command-overlay]{z-index:1000;background:#0000004d;position:fixed;inset:0}[data-slot=studio-command-menu]{z-index:1001;border:1px solid var(--sidebar-border,#26272b);border-radius:calc(var(--radius,.5rem) + 2px);background:color-mix(in srgb, var(--sidebar,#000) 85%, transparent);width:min(480px,100vw - 32px);max-height:calc(100dvh - min(20vh,160px) - 16px);color:var(--foreground,#d6d7dc);color-scheme:dark;font:12px/1.5 var(--studio-font,ui-monospace, SFMono-Regular, Menlo, monospace);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);position:fixed;top:min(20vh,160px);left:50%;overflow:auto;transform:translate(-50%);box-shadow:0 20px 25px -5px #0006,0 8px 10px -6px #0006}[data-slot=studio-command-menu],[data-slot=studio-command-menu] *{box-sizing:border-box}[data-slot=studio-command-menu] svg{width:16px;height:16px;color:inherit;flex-shrink:0}[data-slot=studio-command-menu] [data-slot=kbd]{pointer-events:none;border:1px solid var(--border,#26272b);border-radius:calc(var(--radius,.5rem) - 2px);background:var(--muted,#18191c);width:fit-content;min-width:20px;height:20px;font:inherit;color:var(--muted-foreground,#8b8c94);-webkit-user-select:none;user-select:none;border-bottom-width:2px;justify-content:center;align-items:center;gap:4px;padding:0 4px;font-size:12px;font-weight:400;display:inline-flex}[data-slot=studio-command-menu] [data-slot=kbd-group]{align-items:center;gap:4px;display:inline-flex}[data-slot=studio-command-menu] button{font:inherit;color:inherit;cursor:pointer;background:0 0;border:0;border-radius:4px}[data-slot=studio-command-menu] button:focus-visible{outline:2px solid var(--sidebar-ring,#5d5e66);outline-offset:2px}[data-slot=studio-command-menu] [data-slot=studio-command-heading]{align-items:center;gap:8px;height:40px;padding:0 12px 0 16px;display:flex}[data-slot=studio-command-menu] [data-slot=studio-command-heading] h2{font:inherit;flex:1;align-items:center;gap:10px;margin:0;display:flex}[data-slot=studio-command-menu] [data-slot=studio-command-heading] svg{width:auto;height:14px}[data-slot=studio-command-menu] [data-slot=studio-command-heading] strong{letter-spacing:.08em;white-space:nowrap;font-weight:700}[data-slot=studio-command-menu] [data-slot=studio-command-heading] button{padding:0}[data-slot=studio-command-menu] [data-slot=studio-command-heading] button:hover{background:var(--sidebar-accent,#ffffff14)}[data-slot=studio-command-menu] [data-slot=studio-command-description]{display:none}[data-slot=studio-command-menu] [data-slot=command]{height:auto;color:inherit;background:0 0;flex-direction:column;display:flex;overflow:hidden}[data-slot=studio-command-menu] [data-slot=command-input-wrapper]{border-radius:var(--radius,.5rem);background:var(--field,#ffffff0f);height:36px;color:var(--muted-foreground,#8b8c94);border:1px solid #0000;align-items:center;gap:8px;margin:8px;padding:0 8px;display:flex}[data-slot=studio-command-menu] [data-slot=command-input-wrapper]:focus-within{border-color:var(--sidebar-ring,#5d5e66)}[data-slot=studio-command-menu] [data-slot=command-input-wrapper] svg{opacity:1}[data-slot=studio-command-menu] [data-slot=command-input]{width:100%;min-width:0;height:100%;color:var(--sidebar-primary,#e6e7ea);font:inherit;background:0 0;border:0;outline:none;padding:0}[data-slot=studio-command-menu] [data-slot=command-input]::placeholder{color:var(--muted-foreground,#8b8c94)}[data-slot=studio-command-menu] [data-slot=command-list]{overscroll-behavior:contain;max-height:300px;padding:0 8px 8px;scroll-padding:4px;overflow-y:auto}[data-slot=studio-command-menu] [data-slot=command-item]{border-radius:calc(var(--radius,.5rem) - 2px);min-height:36px;font:inherit;color:var(--sidebar-foreground,#8b8c94);cursor:pointer;align-items:center;gap:8px;padding:8px;display:flex}[data-slot=studio-command-menu] [data-slot=command-item][data-selected=true]{background:var(--sidebar-accent,#ffffff14);color:var(--sidebar-accent-foreground,#d6d7dc)}[data-slot=studio-command-menu] [data-slot=studio-command-label]{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}[data-slot=studio-command-menu] [data-slot=command-empty]{text-align:center;font:inherit;color:var(--muted-foreground,#8b8c94);padding:24px 8px}[data-slot=studio-command-menu] [data-slot=studio-command-footer]{border-top:1px solid var(--sidebar-border,#26272b);color:var(--muted-foreground,#8b8c94);align-items:center;gap:16px;padding:8px 12px;font-size:10px;display:flex}[data-slot=studio-command-menu] [data-slot=studio-command-footer]>span{align-items:center;gap:6px;display:inline-flex}
@@ -0,0 +1,12 @@
1
+ import { T as ToolDefinition, R as Result } from './protocol-Bl62UeCa.js';
2
+ export { A as AnyTool, C as ConfigOf, D as DEFAULT_PORT, F as Failure, O as Op, b as Operations, c as Request, S as ServerMessage, d as StudioConfig, e as Success, f as ToolIcon, a as ToolState, g as ToolSummary, h as defineConfig, i as defineTool, j as error, s as success } from './protocol-Bl62UeCa.js';
3
+ import '@joycostudio/xyz/debug';
4
+
5
+ /**
6
+ * Accept `candidate` as a config for `tool` only if it has the exact shape of
7
+ * `tool.initial` and every control in `tool.controls` accepts its value.
8
+ * Returns a detached copy, never the input.
9
+ */
10
+ declare function validateConfig<TConfig extends object>(tool: ToolDefinition<TConfig>, candidate: unknown): Result<TConfig>;
11
+
12
+ export { Result, ToolDefinition, validateConfig };
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import {
2
+ validateConfig
3
+ } from "./chunk-VYNEG5Z7.js";
4
+ import {
5
+ error,
6
+ success
7
+ } from "./chunk-QKEAOM2U.js";
8
+
9
+ // src/core/tool.ts
10
+ var ID = /^[a-z0-9][a-z0-9-]*$/;
11
+ function defineTool(tool) {
12
+ if (!ID.test(tool.id)) throw new Error(`[studio] tool id "${tool.id}" must be kebab-case`);
13
+ return tool;
14
+ }
15
+ var DEFAULT_PORT = 5179;
16
+ function defineConfig(config) {
17
+ return config;
18
+ }
19
+ export {
20
+ DEFAULT_PORT,
21
+ defineConfig,
22
+ defineTool,
23
+ error,
24
+ success,
25
+ validateConfig
26
+ };
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/tool.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\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}\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"],"mappings":";;;;;;;;;AAqCA,IAAM,KAAK;AAEJ,SAAS,WAAmC,MAAwD;AACzG,MAAI,CAAC,GAAG,KAAK,KAAK,EAAE,EAAG,OAAM,IAAI,MAAM,qBAAqB,KAAK,EAAE,sBAAsB;AACzF,SAAO;AACT;AAYO,IAAM,eAAe;AAErB,SAAS,aAAa,QAAoC;AAC/D,SAAO;AACT;","names":[]}