@a-company/atelier 0.28.2 → 0.36.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.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/commands/studio.ts"],"sourcesContent":["#!/usr/bin/env node\n// @a-company/atelier-cli — Entry point for the `atelier` command\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { validateCommand } from \"./commands/validate.js\";\nimport { infoCommand } from \"./commands/info.js\";\nimport { stillCommand } from \"./commands/still.js\";\nimport { renderCommand } from \"./commands/render.js\";\nimport { exportSvgCommand } from \"./commands/export-svg.js\";\nimport { exportLottieCommand } from \"./commands/export-lottie.js\";\nimport { assetsCommand } from \"./commands/assets.js\";\nimport { variablesCommand } from \"./commands/variables.js\";\nimport { studioCommand } from \"./commands/studio.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"atelier\")\n .description(\"Atelier animation CLI\")\n .version(createRequire(import.meta.url)(\"../package.json\").version);\n\n// Register commands\nvalidateCommand(program);\ninfoCommand(program);\nstillCommand(program);\nrenderCommand(program);\nexportSvgCommand(program);\nexportLottieCommand(program);\nassetsCommand(program);\nvariablesCommand(program);\nstudioCommand(program);\n\nprogram.parse();\n","/**\n * `atelier studio [file]` — launch the browser-based Atelier editor.\n *\n * Spins up a Vite dev server with a temporary app that imports AtelierStudio,\n * provides a file API for reading/writing .atelier files from CWD, and opens\n * the browser.\n *\n * Usage:\n * atelier studio → browse all .atelier files in CWD\n * atelier studio my-animation.atelier → open specific file\n * atelier studio --port 8080 → custom port\n * atelier studio --no-open → don't auto-open browser\n */\n\nimport { resolve, join, relative, dirname } from \"node:path\";\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { exec } from \"node:child_process\";\nimport type { Command } from \"commander\";\n\n/** Recursively glob for .atelier files under a directory. */\nfunction findAtelierFiles(dir: string, base: string = dir): string[] {\n const results: string[] = [];\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return results;\n }\n for (const entry of entries) {\n if (entry === \"node_modules\" || entry === \"dist\" || entry === \".git\") continue;\n const full = join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n results.push(...findAtelierFiles(full, base));\n } else if (entry.endsWith(\".atelier\")) {\n results.push(relative(base, full));\n }\n }\n return results.sort();\n}\n\n/** Validate that a file path is safe (relative, no traversal). */\nfunction isSafePath(filePath: string): boolean {\n if (!filePath || filePath.includes(\"..\") || filePath.startsWith(\"/\")) return false;\n const resolved = resolve(process.cwd(), filePath);\n return resolved.startsWith(process.cwd());\n}\n\nfunction getInlineHTML(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Atelier Studio</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\n </head>\n <body>\n <div id=\"studio\"></div>\n <script type=\"module\" src=\"/main.ts\"></script>\n </body>\n</html>`;\n}\n\nfunction getInlineApp(initialFile: string | null): string {\n const initialFileStr = initialFile ? JSON.stringify(initialFile) : \"null\";\n return `import { AtelierStudio, exportDocument, ImageCache } from \"@a-company/atelier-studio\";\nimport \"@a-company/atelier-studio/styles.css\";\nimport { parseAtelier, serializeAtelier } from \"@a-company/atelier-schema\";\n\n// ── Types ──\ninterface FileEntry {\n path: string;\n name: string;\n folder: string;\n}\n\n// ── State ──\nlet studio: AtelierStudio | null = null;\nlet currentFile: string | null = null;\nlet files: FileEntry[] = [];\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\n\n// ── API helpers ──\nasync function fetchFiles(): Promise<FileEntry[]> {\n const res = await fetch(\"/api/files\");\n return res.json();\n}\n\nasync function fetchFileContent(path: string): Promise<string> {\n const res = await fetch(\"/api/file?path=\" + encodeURIComponent(path));\n return res.text();\n}\n\nasync function saveFileContent(path: string, content: string): Promise<void> {\n await fetch(\"/api/file?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"text/plain\" },\n body: content,\n });\n}\n\nasync function saveExportBlob(path: string, blob: Blob): Promise<void> {\n const buf = await blob.arrayBuffer();\n await fetch(\"/api/export?path=\" + encodeURIComponent(path), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/octet-stream\" },\n body: buf,\n });\n}\n\nasync function exportAll(format: \"gif\" | \"mp4\" | \"webm\"): Promise<void> {\n if (files.length === 0) return;\n\n // Create progress overlay\n const overlay = document.createElement(\"div\");\n overlay.style.cssText = \"position:fixed;inset:0;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:10000\";\n const card = document.createElement(\"div\");\n card.style.cssText = \"background:#333;border:1px solid #4A4A4A;border-radius:8px;padding:32px 40px;min-width:360px;color:#F5F0EB;font-family:'Cormorant Garamond',Georgia,serif\";\n overlay.appendChild(card);\n document.body.appendChild(overlay);\n\n const title = document.createElement(\"div\");\n title.style.cssText = \"font-size:18px;margin-bottom:16px;font-weight:600\";\n title.textContent = \"Exporting All Files…\";\n card.appendChild(title);\n\n const fileLabel = document.createElement(\"div\");\n fileLabel.style.cssText = \"font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace\";\n card.appendChild(fileLabel);\n\n const progress = document.createElement(\"progress\");\n progress.style.cssText = \"width:100%;height:6px;appearance:none;-webkit-appearance:none\";\n progress.max = files.length;\n progress.value = 0;\n card.appendChild(progress);\n\n const statusText = document.createElement(\"div\");\n statusText.style.cssText = \"font-size:12px;color:#A89F95;margin-top:8px\";\n card.appendChild(statusText);\n\n let exported = 0;\n let errors = 0;\n\n for (const file of files) {\n fileLabel.textContent = file.path;\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length;\n\n try {\n const content = await fetchFileContent(file.path);\n const result = parseAtelier(content);\n if (!result.success) {\n errors++;\n progress.value = exported + errors;\n continue;\n }\n\n const doc = result.data;\n const w = doc.canvas.width;\n const h = doc.canvas.height;\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const imageCache = new ImageCache();\n\n const exportResult = await exportDocument(doc, canvas, imageCache, {\n format,\n onProgress: ({ percent }) => {\n statusText.textContent = (exported + errors + 1) + \" / \" + files.length + \" — \" + percent + \"%\";\n },\n });\n\n // Save alongside the source file: e.g. \"dir/my-anim.atelier\" → \"dir/my-anim.gif\"\n const outPath = file.path.replace(/\\\\.atelier$/, \".\" + format);\n await saveExportBlob(outPath, exportResult.blob);\n exported++;\n } catch (e) {\n console.error(\"Export failed:\", file.path, e);\n errors++;\n }\n progress.value = exported + errors;\n }\n\n // Done\n title.textContent = \"Export Complete\";\n fileLabel.textContent = \"\";\n statusText.textContent = exported + \" exported\" + (errors > 0 ? \", \" + errors + \" failed\" : \"\");\n if (errors > 0) console.warn(\"Export All finished with \" + errors + \" error(s). Check console for details.\");\n\n const closeBtn = document.createElement(\"button\");\n closeBtn.style.cssText = \"margin-top:16px;padding:6px 20px;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;cursor:pointer;font-family:inherit;font-size:13px\";\n closeBtn.textContent = \"Close\";\n closeBtn.addEventListener(\"click\", () => document.body.removeChild(overlay));\n card.appendChild(closeBtn);\n}\n\n// ── Theme (matches branded theme from showcase) ──\nconst theme = {\n bg: \"#2C2C2C\",\n bgSecondary: \"#333333\",\n bgTertiary: \"#3D3D3D\",\n text: \"#F5F0EB\",\n textMuted: \"#A89F95\",\n textAccent: \"#F5F0EB\",\n border: \"#4A4A4A\",\n buttonBg: \"#3D3D3D\",\n buttonHover: \"#4A4A4A\",\n buttonActive: \"#555555\",\n accent: \"#C75B39\",\n accentHover: \"#D4724E\",\n sliderTrack: \"#4A4A4A\",\n sliderThumb: \"#C75B39\",\n fontFamily: \"'Cormorant Garamond', Georgia, serif\",\n fontMono: \"'SF Mono', 'Fira Code', monospace\",\n canvasShadow: \"0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)\",\n};\n\n// ── Styles ──\nconst style = document.createElement(\"style\");\nstyle.textContent = \\`\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }\n body { font-family: 'Cormorant Garamond', Georgia, serif; }\n #studio { display: flex; height: 100vh; width: 100vw; }\n\n .sidebar {\n width: 260px;\n min-width: 260px;\n background: #333333;\n border-right: 1px solid #4A4A4A;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .sidebar__header {\n padding: 16px 20px;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: #A89F95;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .sidebar__header span {\n color: #C75B39;\n font-size: 13px;\n }\n .sidebar__list {\n flex: 1;\n overflow-y: auto;\n padding: 8px 0;\n }\n .sidebar__list::-webkit-scrollbar { width: 6px; }\n .sidebar__list::-webkit-scrollbar-track { background: transparent; }\n .sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }\n\n .sidebar__folder {\n padding: 10px 20px 4px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 1.5px;\n text-transform: uppercase;\n color: #A89F95;\n }\n .sidebar__item {\n padding: 8px 20px 8px 28px;\n font-size: 13px;\n cursor: pointer;\n color: #A89F95;\n transition: background 0.15s, color 0.15s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11.5px;\n }\n .sidebar__item:hover { background: #363636; color: #F5F0EB; }\n .sidebar__item--active {\n background: rgba(199, 91, 57, 0.12) !important;\n color: #C75B39 !important;\n }\n\n .main {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n .main__status {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 16px;\n background: #333333;\n border-bottom: 1px solid #4A4A4A;\n font-size: 11px;\n color: #A89F95;\n font-family: 'SF Mono', 'Fira Code', monospace;\n gap: 12px;\n }\n .main__status .save-indicator {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n transition: opacity 0.3s;\n }\n .main__status .save-indicator--saving { color: #C75B39; }\n .main__status .save-indicator--saved { color: #6B8E6B; }\n .main__editor {\n flex: 1;\n overflow: hidden;\n }\n .main__empty {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #A89F95;\n font-size: 18px;\n }\n\\`;\ndocument.head.appendChild(style);\n\n// ── Build UI ──\nconst root = document.getElementById(\"studio\")!;\nconst sidebar = document.createElement(\"div\");\nsidebar.className = \"sidebar\";\n\nconst sidebarHeader = document.createElement(\"div\");\nsidebarHeader.className = \"sidebar__header\";\nsidebarHeader.innerHTML = '<span>&#9670;</span> ATELIER STUDIO';\nsidebar.appendChild(sidebarHeader);\n\nconst sidebarList = document.createElement(\"div\");\nsidebarList.className = \"sidebar__list\";\nsidebar.appendChild(sidebarList);\n\nconst sidebarFooter = document.createElement(\"div\");\nsidebarFooter.style.cssText = \"padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center\";\nconst exportAllSelect = document.createElement(\"select\");\nexportAllSelect.style.cssText = \"flex:1;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;padding:4px 8px;font-size:11px;font-family:'SF Mono','Fira Code',monospace;cursor:pointer\";\nfor (const [val, label] of [[\"gif\",\"GIF\"],[\"mp4\",\"MP4\"],[\"webm\",\"WebM\"]] as const) {\n const o = document.createElement(\"option\");\n o.value = val;\n o.textContent = label;\n exportAllSelect.appendChild(o);\n}\nsidebarFooter.appendChild(exportAllSelect);\nconst exportAllBtn = document.createElement(\"button\");\nexportAllBtn.style.cssText = \"background:#C75B39;color:#F5F0EB;border:none;border-radius:4px;padding:5px 12px;font-size:11px;font-family:inherit;cursor:pointer;white-space:nowrap\";\nexportAllBtn.textContent = \"Export All\";\nexportAllBtn.addEventListener(\"click\", () => {\n exportAll(exportAllSelect.value as \"gif\" | \"mp4\" | \"webm\");\n});\nsidebarFooter.appendChild(exportAllBtn);\nsidebar.appendChild(sidebarFooter);\n\nconst main = document.createElement(\"div\");\nmain.className = \"main\";\n\nconst statusBar = document.createElement(\"div\");\nstatusBar.className = \"main__status\";\nmain.appendChild(statusBar);\n\nconst editorContainer = document.createElement(\"div\");\neditorContainer.className = \"main__editor\";\nmain.appendChild(editorContainer);\n\nroot.appendChild(sidebar);\nroot.appendChild(main);\n\n// ── File list rendering ──\nfunction renderFileList(): void {\n sidebarList.innerHTML = \"\";\n let lastFolder = \"\";\n\n for (const file of files) {\n if (file.folder && file.folder !== lastFolder) {\n lastFolder = file.folder;\n const folder = document.createElement(\"div\");\n folder.className = \"sidebar__folder\";\n folder.textContent = file.folder;\n sidebarList.appendChild(folder);\n }\n\n const item = document.createElement(\"div\");\n item.className = \"sidebar__item\" + (file.path === currentFile ? \" sidebar__item--active\" : \"\");\n item.textContent = file.name;\n item.title = file.path;\n item.addEventListener(\"click\", () => loadFile(file.path));\n sidebarList.appendChild(item);\n }\n}\n\n// ── Load a file into the studio ──\nasync function loadFile(path: string): Promise<void> {\n currentFile = path;\n renderFileList();\n\n const content = await fetchFileContent(path);\n const result = parseAtelier(content);\n\n if (!result.success) {\n editorContainer.innerHTML = \"\";\n const err = document.createElement(\"div\");\n err.className = \"main__empty\";\n err.style.flexDirection = \"column\";\n err.style.gap = \"8px\";\n err.innerHTML = '<div style=\"color:#C75B39\">Parse Error</div><div style=\"font-size:13px;font-family:monospace\">' +\n result.errors.map(e => e.path + \": \" + e.message).join(\"<br>\") + \"</div>\";\n editorContainer.appendChild(err);\n return;\n }\n\n statusBar.innerHTML = '<span>' + path + '</span><span class=\"save-indicator save-indicator--saved\">&#10003; saved</span>';\n\n if (studio) {\n studio.destroy();\n studio = null;\n }\n\n // Set filename for export downloads (strip path and .atelier extension)\n const baseName = path.split(\"/\").pop()?.replace(/\\\\.atelier$/, \"\") || null;\n\n studio = new AtelierStudio(editorContainer, {\n mode: \"full\",\n initialTab: \"yaml\",\n allowSave: true,\n onDocumentChange: (doc) => {\n // Auto-save with debounce\n const indicator = statusBar.querySelector(\".save-indicator\");\n if (indicator) {\n indicator.className = \"save-indicator save-indicator--saving\";\n indicator.innerHTML = \"&#9679; saving...\";\n }\n\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(async () => {\n if (!currentFile) return;\n const yaml = serializeAtelier(doc);\n await saveFileContent(currentFile, yaml);\n const ind = statusBar.querySelector(\".save-indicator\");\n if (ind) {\n ind.className = \"save-indicator save-indicator--saved\";\n ind.innerHTML = \"&#10003; saved\";\n }\n }, 800);\n },\n });\n studio.setTheme(theme);\n studio.setFilename(baseName);\n studio.loadDocument(result.data);\n}\n\n// ── Boot ──\nasync function boot(): Promise<void> {\n files = await fetchFiles();\n\n if (files.length === 0) {\n editorContainer.innerHTML = \"\";\n const empty = document.createElement(\"div\");\n empty.className = \"main__empty\";\n empty.textContent = \"No .atelier files found in this directory\";\n editorContainer.appendChild(empty);\n statusBar.textContent = \"No files\";\n renderFileList();\n return;\n }\n\n renderFileList();\n\n const initialFile = ${initialFileStr};\n const target = initialFile\n ? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))\n : files[0];\n\n if (target) {\n await loadFile(target.path);\n }\n}\n\nboot();\n`;\n}\n\n/** Register the `studio` subcommand on the Commander program. */\nexport function studioCommand(program: Command): void {\n program\n .command(\"studio [file]\")\n .description(\"Launch the browser-based Atelier editor\")\n .option(\"-p, --port <number>\", \"Port to serve on\", \"4321\")\n .option(\"--no-open\", \"Don't auto-open browser\")\n .action(\n async (\n file: string | undefined,\n options: { port: string; open: boolean },\n ) => {\n const port = parseInt(options.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n console.error(`Invalid port: ${options.port}`);\n process.exit(1);\n }\n\n const cwd = process.cwd();\n\n // Find the CLI package directory (where node_modules lives).\n // This file is at packages/cli/dist/cli.js (or src/commands/studio.ts in dev).\n const cliPackageDir = resolve(dirname(new URL(import.meta.url).pathname), \"..\");\n\n // Create temp directory with inline app.\n // Use realpathSync to resolve macOS /var -> /private/var symlink,\n // which Vite normalizes internally when resolving file paths.\n const tmpId = randomBytes(4).toString(\"hex\");\n const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);\n mkdirSync(tmpDirRaw, { recursive: true });\n const tmpDir = realpathSync(tmpDirRaw);\n\n writeFileSync(join(tmpDir, \"index.html\"), getInlineHTML());\n writeFileSync(join(tmpDir, \"main.ts\"), getInlineApp(file ?? null));\n\n // Symlink node_modules into temp dir so Vite can resolve @a-company/* packages.\n // Works for both monorepo (pnpm workspace links) and npm global install.\n const cliNodeModules = join(cliPackageDir, \"node_modules\");\n if (existsSync(cliNodeModules)) {\n try {\n symlinkSync(cliNodeModules, join(tmpDir, \"node_modules\"), \"dir\");\n } catch {\n // Non-fatal: aliases will handle resolution if symlink fails\n }\n }\n\n console.log(`Starting Atelier Studio...`);\n console.log(` Working directory: ${cwd}`);\n\n // Dynamically import Vite (it's a peer/optional dep)\n let vite: typeof import(\"vite\");\n try {\n vite = await import(\"vite\");\n } catch {\n console.error(\"Vite is required for `atelier studio`.\");\n console.error(\"Install it: pnpm add -D vite\");\n process.exit(1);\n return;\n }\n\n const server = await vite.createServer({\n root: tmpDir,\n server: {\n port,\n strictPort: false,\n fs: {\n strict: false,\n },\n },\n plugins: [\n {\n name: \"atelier-api\",\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n\n if (url.pathname === \"/api/files\") {\n const atelierFiles = findAtelierFiles(cwd);\n const entries = atelierFiles.map((p) => {\n const parts = p.split(\"/\");\n return {\n path: p,\n name: parts[parts.length - 1].replace(\".atelier\", \"\"),\n folder: parts.length > 1 ? parts.slice(0, -1).join(\"/\") : \"\",\n };\n });\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(entries));\n return;\n }\n\n if (url.pathname === \"/api/file\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n\n if (req.method === \"GET\") {\n try {\n const content = readFileSync(absPath, \"utf-8\");\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(content);\n } catch {\n res.statusCode = 404;\n res.end(\"File not found\");\n }\n return;\n }\n\n if (req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => { body += chunk.toString(); });\n req.on(\"end\", () => {\n try {\n writeFileSync(absPath, body, \"utf-8\");\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n }\n\n if (url.pathname === \"/api/export\" && req.method === \"POST\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => { chunks.push(chunk); });\n req.on(\"end\", () => {\n try {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, Buffer.concat(chunks));\n res.end(\"OK\");\n } catch {\n res.statusCode = 500;\n res.end(\"Write failed\");\n }\n });\n return;\n }\n\n if (url.pathname === \"/api/cwd\") {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ cwd }));\n return;\n }\n\n next();\n });\n },\n },\n ],\n logLevel: \"warn\",\n });\n\n await server.listen();\n\n const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;\n const url = resolvedUrl;\n\n console.log(` Server running at: ${url}`);\n\n const atelierFiles = findAtelierFiles(cwd);\n console.log(` Found ${atelierFiles.length} .atelier file(s)`);\n\n if (file) {\n console.log(` Opening: ${file}`);\n }\n\n console.log(` Press Ctrl+C to stop\\n`);\n\n if (options.open) {\n exec(`open \"${url}\"`);\n }\n\n // Keep alive and handle cleanup\n const cleanup = () => {\n console.log(\"\\nShutting down...\");\n server.close();\n try {\n rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // ignore cleanup errors\n }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAGA,SAAS,qBAAqB;AAC9B,SAAS,eAAe;;;ACUxB,SAAS,SAAS,MAAM,UAAU,eAAe;AACjD,SAAS,WAAW,eAAe,QAAQ,cAAc,aAAa,UAAU,cAAc,aAAa,kBAAkB;AAC7H,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AAIrB,SAAS,iBAAiB,KAAa,OAAe,KAAe;AACnE,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACJ,MAAI;AACF,cAAU,YAAY,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,UAAU,kBAAkB,UAAU,UAAU,UAAU,OAAQ;AACtE,UAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,QAAI;AACJ,QAAI;AACF,aAAO,SAAS,IAAI;AAAA,IACtB,QAAQ;AACN;AAAA,IACF;AACA,QAAI,KAAK,YAAY,GAAG;AACtB,cAAQ,KAAK,GAAG,iBAAiB,MAAM,IAAI,CAAC;AAAA,IAC9C,WAAW,MAAM,SAAS,UAAU,GAAG;AACrC,cAAQ,KAAK,SAAS,MAAM,IAAI,CAAC;AAAA,IACnC;AAAA,EACF;AACA,SAAO,QAAQ,KAAK;AACtB;AAGA,SAAS,WAAW,UAA2B;AAC7C,MAAI,CAAC,YAAY,SAAS,SAAS,IAAI,KAAK,SAAS,WAAW,GAAG,EAAG,QAAO;AAC7E,QAAM,WAAW,QAAQ,QAAQ,IAAI,GAAG,QAAQ;AAChD,SAAO,SAAS,WAAW,QAAQ,IAAI,CAAC;AAC1C;AAEA,SAAS,gBAAwB;AAC/B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeT;AAEA,SAAS,aAAa,aAAoC;AACxD,QAAM,iBAAiB,cAAc,KAAK,UAAU,WAAW,IAAI;AACnE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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","names":["program","server","url","atelierFiles"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/lint.ts","../src/commands/studio.ts"],"sourcesContent":["#!/usr/bin/env node\n// @a-company/atelier-cli — Entry point for the `atelier` command\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { validateCommand } from \"./commands/validate.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { trimCommand } from \"./commands/trim.js\";\nimport { transcribeCommand } from \"./commands/transcribe.js\";\nimport { transcriptCommand } from \"./commands/transcript.js\";\nimport { captionsCommand } from \"./commands/captions.js\";\nimport { recipeCommand } from \"./commands/recipe.js\";\nimport { applyRecipeCommand } from \"./commands/apply-recipe.js\";\nimport { carouselCommand } from \"./commands/carousel.js\";\nimport { infoCommand } from \"./commands/info.js\";\nimport { stillCommand } from \"./commands/still.js\";\nimport { renderCommand } from \"./commands/render.js\";\nimport { exportSvgCommand } from \"./commands/export-svg.js\";\nimport { exportLottieCommand } from \"./commands/export-lottie.js\";\nimport { exportImageCommand } from \"./commands/export-image.js\";\nimport { assetsCommand } from \"./commands/assets.js\";\nimport { variablesCommand } from \"./commands/variables.js\";\nimport { studioCommand } from \"./commands/studio.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"atelier\")\n .description(\"Atelier animation CLI\")\n .version(createRequire(import.meta.url)(\"../package.json\").version);\n\n// Register commands\nvalidateCommand(program);\nlintCommand(program);\ntrimCommand(program);\ntranscribeCommand(program);\ntranscriptCommand(program);\ncaptionsCommand(program);\nrecipeCommand(program);\napplyRecipeCommand(program);\ncarouselCommand(program);\ninfoCommand(program);\nstillCommand(program);\nrenderCommand(program);\nexportSvgCommand(program);\nexportLottieCommand(program);\nexportImageCommand(program);\nassetsCommand(program);\nvariablesCommand(program);\nstudioCommand(program);\n\nprogram.parse();\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { Command } from \"commander\";\nimport type { VideoVisual } from \"@a-company/atelier-types\";\nimport { parseAtelier, validateVideoLayer } from \"@a-company/atelier-schema\";\nimport { validateAllDeltas } from \"@a-company/atelier-core\";\n\nexport interface GateResult {\n gate: string;\n pass: boolean;\n errors: string[];\n}\n\nexport interface LintResult {\n file: string;\n valid: boolean;\n gates: GateResult[];\n}\n\n/**\n * Run all gates against a single .atelier file.\n * Gates checked:\n * ^valid-document — Zod schema conformance\n * ^valid-delta — no overlapping deltas on same layer+property\n * ^valid-video-layer — semantic video layer constraints\n */\nexport function lintFile(filePath: string): LintResult {\n const absPath = resolve(filePath);\n\n let content: string;\n try {\n content = readFileSync(absPath, \"utf-8\");\n } catch {\n return {\n file: absPath,\n valid: false,\n gates: [\n {\n gate: \"^valid-document\",\n pass: false,\n errors: [`Cannot read file: ${absPath}`],\n },\n ],\n };\n }\n\n const gates: GateResult[] = [];\n\n // ── Gate 1: ^valid-document ──────────────────────────────────\n const parseResult = parseAtelier(content);\n if (!parseResult.success) {\n gates.push({\n gate: \"^valid-document\",\n pass: false,\n errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`),\n });\n // Can't run further gates without a valid document\n return { file: absPath, valid: false, gates };\n }\n\n gates.push({ gate: \"^valid-document\", pass: true, errors: [] });\n const doc = parseResult.data;\n\n // ── Gate 2: ^valid-delta ─────────────────────────────────────\n const deltaErrors: string[] = [];\n for (const [stateName, state] of Object.entries(doc.states)) {\n const overlaps = validateAllDeltas(state.deltas);\n for (const overlap of overlaps) {\n deltaErrors.push(`State \"${stateName}\": ${overlap.message}`);\n }\n }\n gates.push({\n gate: \"^valid-delta\",\n pass: deltaErrors.length === 0,\n errors: deltaErrors,\n });\n\n // ── Gate 3: ^valid-video-layer ───────────────────────────────\n const videoErrors: string[] = [];\n for (const layer of doc.layers) {\n if (layer.visual.type !== \"video\") continue;\n const visual = layer.visual as VideoVisual;\n const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;\n const result = validateVideoLayer(visual, duration);\n if (!result.success) {\n for (const err of result.errors) {\n videoErrors.push(`Layer \"${layer.id}\" (${err.path}): ${err.message}`);\n }\n }\n }\n gates.push({\n gate: \"^valid-video-layer\",\n pass: videoErrors.length === 0,\n errors: videoErrors,\n });\n\n const valid = gates.every((g) => g.pass);\n return { file: absPath, valid, gates };\n}\n\n/** Format a LintResult for terminal output */\nfunction formatResult(result: LintResult): string {\n const lines: string[] = [];\n const status = result.valid ? \"PASS\" : \"FAIL\";\n lines.push(`${status} ${result.file}`);\n\n for (const gate of result.gates) {\n const gateStatus = gate.pass ? \" ✓\" : \" ✗\";\n lines.push(`${gateStatus} ${gate.gate}`);\n for (const err of gate.errors) {\n lines.push(` ${err}`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Register the `lint` subcommand on the Commander program.\n */\nexport function lintCommand(program: Command): void {\n program\n .command(\"lint <files...>\")\n .description(\n \"Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)\",\n )\n .option(\"--json\", \"Output results as JSON array\")\n .action((files: string[], opts: { json?: boolean }) => {\n const results = files.map(lintFile);\n\n if (opts.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n for (const result of results) {\n console.log(formatResult(result));\n }\n }\n\n const allValid = results.every((r) => r.valid);\n if (!allValid) process.exit(1);\n });\n}\n","/**\n * `atelier studio [file]` — launch the browser-based Atelier editor.\n *\n * Spins up a Vite dev server with a temporary app that imports AtelierStudio,\n * provides a file API for reading/writing .atelier files from CWD, and opens\n * the browser.\n *\n * Usage:\n * atelier studio → browse all .atelier files in CWD\n * atelier studio my-animation.atelier → open specific file\n * atelier studio --port 8080 → custom port\n * atelier studio --no-open → don't auto-open browser\n */\n\nimport { resolve, join, relative, dirname, sep } from \"node:path\";\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync, copyFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { exec } from \"node:child_process\";\nimport type { Command } from \"commander\";\nimport {\n DocumentStore,\n WebSocketServerTransport,\n createServer as createMcpServer,\n BRIDGE_PROTOCOL_VERSION,\n isBridgeEnvelope,\n type BridgeEnvelope,\n} from \"@a-company/atelier-mcp\";\nimport { parseAtelier, serializeAtelier } from \"@a-company/atelier-schema\";\nimport { WebSocketServer, type WebSocket as WsWebSocket } from \"ws\";\n\n/** Recursively glob for .atelier files under a directory. */\nfunction findAtelierFiles(dir: string, base: string = dir): string[] {\n const results: string[] = [];\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return results;\n }\n for (const entry of entries) {\n if (entry === \"node_modules\" || entry === \"dist\" || entry === \".git\") continue;\n const full = join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n results.push(...findAtelierFiles(full, base));\n } else if (entry.endsWith(\".atelier\")) {\n results.push(relative(base, full));\n }\n }\n return results.sort();\n}\n\n/**\n * Validate that a file path is safe (relative, contained within cwd).\n *\n * Rejects absolute inputs and any path that resolves outside the current\n * working directory. The containment test uses an exact-or-trailing-separator\n * prefix match so a sibling directory whose name merely starts with cwd\n * (e.g. cwd `/a/proj` vs resolved `/a/project-x`) is NOT treated as inside.\n * We intentionally do NOT reject a literal \"..\" substring — that blocks legit\n * names like `my..backup.atelier` while adding no real security (the resolved\n * containment check is the actual guard).\n */\nexport function isSafePath(filePath: string): boolean {\n if (!filePath || filePath.startsWith(\"/\")) return false;\n const cwd = process.cwd();\n const resolved = resolve(cwd, filePath);\n return resolved === cwd || resolved.startsWith(cwd + sep);\n}\n\n/**\n * Write a file, creating any missing parent directories first. Used by the\n * POST /api/file handler so creating e.g. `notes/foo.atelier` in a subfolder\n * doesn't ENOENT (mirrors what the /api/export handler already did).\n */\nexport function writeFileEnsuringDir(absPath: string, body: string): void {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, body, \"utf-8\");\n}\n\n/**\n * Shared origin-check used by both REST middleware and WS upgrade handler.\n * ^localhost-only: WebSocket upgrade requests bypass connect middleware\n * entirely, so the bridge MUST re-run this check before accepting the\n * upgrade — otherwise an arbitrary cross-origin browser tab could connect\n * to /bridge or /mcp and drive the studio.\n */\nexport function isAllowedOrigin(origin: string | undefined, port: number): boolean {\n if (!origin) return false;\n return (\n origin === `http://localhost:${port}` ||\n origin === `http://127.0.0.1:${port}`\n );\n}\n\n/**\n * Origin check for the /mcp WS upgrade. Unlike /bridge (a real browser origin),\n * non-browser MCP clients (Claude Desktop, etc.) send NO Origin header — under\n * the strict isAllowedOrigin check they'd all 403 and the MCP-over-WS feature\n * would be unreachable. So we tolerate a MISSING Origin here but still reject a\n * PRESENT-but-foreign one (a browser tab on evil.com still can't reach /mcp).\n * The loopback bind remains the primary protection.\n */\nexport function isAllowedMcpOrigin(origin: string | undefined, port: number): boolean {\n return origin === undefined || isAllowedOrigin(origin, port);\n}\n\n/**\n * Decide whether a store change should be broadcast to the browser (as an\n * `llm:mutation` envelope) AND re-persisted to disk. Only LLM/agent edits\n * qualify: \"system\" (GET-hydration of a file the user just opened) and \"human\"\n * (the browser is already current) must stay silent — otherwise a plain\n * file-open would fire a phantom \"agent edited document\" toast + bogus undo\n * entry and needlessly rewrite the just-read bytes back to disk.\n */\nexport function shouldBroadcastMutation(source: string): boolean {\n return source === \"llm\";\n}\n\n// ─── Bridge & MCP-over-WS wiring ────────────────────────────────────────────\n//\n// The bridge connects an external MCP client (e.g. Claude Desktop running\n// `atelier-mcp` over WS) to the in-browser AtelierStudio. The full loop:\n//\n// external LLM tool ──► McpServer (per WS conn) ──► shared DocumentStore\n// │\n// onChange (source≠\"human\")\n// ▼\n// broadcast `llm:mutation` envelope\n// ▼\n// browser applies via studio.applyMutation\n//\n// browser human edit ──► fetch /api/file (POST) ──► writeFileSync ──► store.set(source:\"human\")\n// │\n// onChange (filtered: human)\n// ▼\n// NOT broadcast (skip echo)\n//\n// browser doc:patch (WS) ──► store.set(source:\"human\") + writeFileSync │\n// ▼\n// NOT broadcast (skip echo)\n//\n// Single-doc invariant for v1: the studio session tracks one currentDocId\n// (the most-recently-loaded file path). When a new bridge client connects\n// mid-session it receives `doc:loaded` for whatever's currently active.\n\n/** State held across the studio session — exported so tests can drive it. */\nexport interface BridgeSessionState {\n store: DocumentStore;\n /** Most-recently-loaded file path; null until first load. */\n currentDocId: string | null;\n}\n\n/**\n * Wrap a `ws.WebSocket` (already accepted as a /bridge connection) so it\n * receives the `hello` + initial `doc:loaded` handshake and joins the\n * broadcast set.\n *\n * Returned `dispose` removes the client from the set and unhooks listeners.\n */\nexport function attachBridgeClient(\n ws: WsWebSocket,\n state: BridgeSessionState,\n clients: Set<WsWebSocket>,\n loadDocFromDisk: (docId: string) => string | null,\n /** Persist a human-sourced inbound doc:patch envelope to disk. */\n persistHumanPatch: (docId: string, doc: import(\"@a-company/atelier-types\").AtelierDocument) => void,\n): () => void {\n clients.add(ws);\n\n const clientId = randomBytes(6).toString(\"hex\");\n const send = (env: BridgeEnvelope): void => {\n if (ws.readyState !== ws.OPEN) return;\n ws.send(JSON.stringify(env));\n };\n\n send({ type: \"hello\", clientId, protocolVersion: BRIDGE_PROTOCOL_VERSION });\n\n if (state.currentDocId) {\n const existing = state.store.get(state.currentDocId);\n if (existing) {\n send({ type: \"doc:loaded\", documentId: state.currentDocId, doc: existing });\n } else {\n // Re-hydrate from disk if the store hasn't seen it yet (e.g. bridge\n // client connected before the browser loaded any file).\n const raw = loadDocFromDisk(state.currentDocId);\n if (raw) {\n const parsed = parseAtelier(raw);\n if (parsed.success) {\n state.store.set(state.currentDocId, parsed.data, \"system\");\n send({ type: \"doc:loaded\", documentId: state.currentDocId, doc: parsed.data });\n }\n }\n }\n }\n\n const onMessage = (data: unknown): void => {\n let text: string;\n if (typeof data === \"string\") text = data;\n else if (data instanceof Buffer) text = data.toString(\"utf-8\");\n else if (Array.isArray(data)) text = Buffer.concat(data as Buffer[]).toString(\"utf-8\");\n else text = String(data);\n\n let env: unknown;\n try {\n env = JSON.parse(text);\n } catch {\n send({ type: \"error\", code: \"parse_error\", message: \"invalid JSON\" });\n return;\n }\n\n if (!isBridgeEnvelope(env)) {\n send({ type: \"error\", code: \"invalid_envelope\", message: \"unknown envelope shape\" });\n return;\n }\n\n if (env.type === \"doc:patch\") {\n // Browser-originated edit. Persist + mirror into the store with\n // source:\"human\" so the onChange filter skips re-broadcasting it\n // (which would echo back to the same browser that just emitted it).\n try {\n state.store.set(env.documentId, env.doc, \"human\");\n state.currentDocId = env.documentId;\n persistHumanPatch(env.documentId, env.doc);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n send({ type: \"error\", code: \"persist_failed\", message: msg, opId: env.opId });\n }\n return;\n }\n\n if (env.type === \"doc:load\") {\n const existing = state.store.get(env.documentId);\n if (existing) {\n send({ type: \"doc:loaded\", documentId: env.documentId, doc: existing });\n return;\n }\n const raw = loadDocFromDisk(env.documentId);\n if (raw) {\n const parsed = parseAtelier(raw);\n if (parsed.success) {\n state.store.set(env.documentId, parsed.data, \"system\");\n send({ type: \"doc:loaded\", documentId: env.documentId, doc: parsed.data });\n return;\n }\n }\n send({ type: \"error\", code: \"not_found\", message: `document ${env.documentId} not found` });\n return;\n }\n\n // hello / doc:loaded / llm:mutation / error are server-→client only.\n // Ignore silently; a future protocol revision may use them bidirectionally.\n };\n\n const onClose = (): void => {\n clients.delete(ws);\n };\n\n ws.on(\"message\", onMessage);\n ws.on(\"close\", onClose);\n ws.on(\"error\", onClose);\n\n return () => {\n clients.delete(ws);\n try {\n ws.close();\n } catch {\n // ignore\n }\n };\n}\n\n/**\n * Broadcast a single envelope to all connected bridge clients.\n * Silently skips clients that are not OPEN — the close handler removes\n * them on the next tick.\n */\nexport function broadcastToBridge(\n clients: Set<WsWebSocket>,\n envelope: BridgeEnvelope,\n): void {\n const payload = JSON.stringify(envelope);\n for (const ws of clients) {\n if (ws.readyState === ws.OPEN) {\n try {\n ws.send(payload);\n } catch {\n // Drop on send-failure; the close handler cleans up.\n }\n }\n }\n}\n\nfunction getInlineHTML(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Atelier Studio</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap\" rel=\"stylesheet\">\n </head>\n <body>\n <div id=\"studio\"></div>\n <script type=\"module\" src=\"/main.ts\"></script>\n </body>\n</html>`;\n}\n\n/**\n * Generate the tiny `main.ts` entry shim that Vite serves from the temp dir.\n *\n * The ~700-LOC studio client used to live here as a template-literal STRING\n * (no typecheck, no lint, no tests — which is exactly why the autosave\n * save-race and slider-flood bugs hid in it). It now lives as a real,\n * typechecked module at `src/web/inline-app.ts` (see #studio-inline-app),\n * exported as `bootStudioApp(config)`.\n *\n * This shim imports that module by ABSOLUTE path under the CLI package and\n * calls it with the injected `{ initialFile }`. The CLI ships `src/web/**` as\n * raw source (package `files` glob), so the path resolves identically in dev\n * (`packages/cli`) and prod (`dist/cli.js` → package root) — both via\n * `resolveCliPackageDir()`. Vite's dev server transpiles + serves the TS\n * module at request time (`server.fs.strict: false` lets it read source\n * outside the temp root).\n *\n * Resolution choice: a direct absolute-path import is the lowest-risk option —\n * no virtual-module plugin, no extra build step. Do NOT reintroduce a string.\n */\nfunction getInlineApp(initialFile: string | null, cliPackageDir: string): string {\n const initialFileStr = initialFile ? JSON.stringify(initialFile) : \"null\";\n // Absolute, POSIX-style path so the generated import specifier is valid on\n // all platforms (Vite/ESM specifiers use forward slashes even on Windows).\n const appModulePath = join(cliPackageDir, \"src\", \"web\", \"inline-app.ts\").split(sep).join(\"/\");\n return `import { bootStudioApp } from ${JSON.stringify(appModulePath)};\nbootStudioApp({ initialFile: ${initialFileStr} });\n`;\n}\n\n/**\n * Locate the CLI package directory regardless of whether this module is\n * running from `dist/cli.js` (production) or `src/commands/studio.ts` (dev).\n *\n * In production: this file is `dist/cli.js`, so `..` from its dirname\n * resolves to the package root.\n * In dev (vitest, ts-node): this file is `src/commands/studio.ts`, so\n * we need to climb two levels.\n *\n * We probe for a recognisable file (package.json) at each candidate.\n */\nfunction resolveCliPackageDir(): string {\n const here = dirname(new URL(import.meta.url).pathname);\n const candidates = [\n resolve(here, \"..\"),\n resolve(here, \"..\", \"..\"),\n ];\n for (const c of candidates) {\n if (existsSync(join(c, \"package.json\"))) {\n return c;\n }\n }\n // Fall back to first candidate; downstream callers handle missing files.\n return candidates[0];\n}\n\n/**\n * If `cwd` contains zero `.atelier` files, copy the bundled welcome template\n * (and any sibling assets) into `cwd`. Skips files that already exist so a\n * user re-running the command never overwrites their work.\n *\n * Returns the basename of the scaffolded file (or null if nothing was done).\n */\nfunction scaffoldWelcomeIfEmpty(cwd: string, cliPackageDir: string): string | null {\n const existing = findAtelierFiles(cwd);\n if (existing.length > 0) return null;\n\n const templatesDir = join(cliPackageDir, \"templates\");\n const welcomeSrc = join(templatesDir, \"welcome.atelier\");\n if (!existsSync(welcomeSrc)) return null;\n\n const welcomeDest = join(cwd, \"welcome.atelier\");\n if (existsSync(welcomeDest)) return null;\n\n try {\n copyFileSync(welcomeSrc, welcomeDest);\n } catch {\n // Non-fatal: studio still launches with empty-state UI.\n return null;\n }\n\n // Also copy welcome-bg.png if shipped alongside (currently we ship a\n // ShapeVisual placeholder, so this is a no-op; future-proofs the path).\n const bgSrc = join(templatesDir, \"welcome-bg.png\");\n if (existsSync(bgSrc)) {\n const bgDest = join(cwd, \"welcome-bg.png\");\n if (!existsSync(bgDest)) {\n try {\n copyFileSync(bgSrc, bgDest);\n } catch {\n // Non-fatal.\n }\n }\n }\n\n return \"welcome.atelier\";\n}\n\n/** Read the CLI package version for display in the splash banner. */\nfunction readCliVersion(cliPackageDir: string): string {\n try {\n const pkg = JSON.parse(readFileSync(join(cliPackageDir, \"package.json\"), \"utf-8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\n/** Register the `studio` subcommand on the Commander program. */\nexport function studioCommand(program: Command): void {\n program\n .command(\"studio [file]\")\n .description(\"Launch the browser-based Atelier editor\")\n .option(\"-p, --port <number>\", \"Port to serve on\", \"4321\")\n .option(\"--no-open\", \"Don't auto-open browser\")\n .action(\n async (\n file: string | undefined,\n options: { port: string; open: boolean },\n ) => {\n const port = parseInt(options.port, 10);\n if (isNaN(port) || port < 1 || port > 65535) {\n console.error(`Invalid port: ${options.port}`);\n process.exit(1);\n }\n\n const cwd = process.cwd();\n\n // Find the CLI package directory (where node_modules + templates live).\n const cliPackageDir = resolveCliPackageDir();\n const version = readCliVersion(cliPackageDir);\n\n // First-run scaffold: if CWD has zero .atelier files, drop in welcome.atelier\n // so the user lands on something real instead of an empty-state dead-end.\n const scaffolded = scaffoldWelcomeIfEmpty(cwd, cliPackageDir);\n\n console.log(\"\");\n console.log(` Atelier Studio · v${version}`);\n if (scaffolded) {\n console.log(` Scaffolded ${scaffolded} — opening…`);\n }\n\n // Create temp directory with inline app.\n // Use realpathSync to resolve macOS /var -> /private/var symlink,\n // which Vite normalizes internally when resolving file paths.\n const tmpId = randomBytes(4).toString(\"hex\");\n const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);\n mkdirSync(tmpDirRaw, { recursive: true });\n const tmpDir = realpathSync(tmpDirRaw);\n\n writeFileSync(join(tmpDir, \"index.html\"), getInlineHTML());\n writeFileSync(join(tmpDir, \"main.ts\"), getInlineApp(file ?? null, cliPackageDir));\n\n // Symlink node_modules into temp dir so Vite can resolve @a-company/* packages.\n // Works for both monorepo (pnpm workspace links) and npm global install.\n const cliNodeModules = join(cliPackageDir, \"node_modules\");\n if (existsSync(cliNodeModules)) {\n try {\n symlinkSync(cliNodeModules, join(tmpDir, \"node_modules\"), \"dir\");\n } catch {\n // Non-fatal: aliases will handle resolution if symlink fails\n }\n }\n\n console.log(` Working directory: ${cwd}`);\n\n // Dynamically import Vite (it's a peer/optional dep)\n let vite: typeof import(\"vite\");\n try {\n vite = await import(\"vite\");\n } catch {\n console.error(\"Vite is required for `atelier studio`.\");\n console.error(\"Install it: pnpm add -D vite\");\n process.exit(1);\n return;\n }\n\n // ^localhost-only: bind explicitly to the loopback interface so the\n // dev server cannot be reached from other machines on the network.\n // Vite's default is \"localhost\" which already resolves to loopback,\n // but we pin 127.0.0.1 to make the invariant intentional + auditable\n // and to defeat any future change in Vite defaults.\n const HOSTNAME = \"127.0.0.1\";\n\n // Shared session state for the bridge + MCP-over-WS endpoints.\n // The same DocumentStore is fed by:\n // - File reads via /api/file (source:\"system\" — hydrates the cache)\n // - File writes via /api/file POST (source:\"human\" — no broadcast)\n // - Bridge doc:patch (source:\"human\" — no broadcast; also persists)\n // - Per-connection MCP servers over /mcp (source:\"llm\" by default\n // for tools that don't tag — broadcasts to the bridge)\n const bridgeState: BridgeSessionState = {\n store: new DocumentStore(),\n currentDocId: null,\n };\n const bridgeClients = new Set<WsWebSocket>();\n\n const loadDocFromDisk = (docId: string): string | null => {\n if (!isSafePath(docId)) return null;\n try {\n return readFileSync(resolve(cwd, docId), \"utf-8\");\n } catch {\n return null;\n }\n };\n\n const persistHumanPatch = (\n docId: string,\n doc: import(\"@a-company/atelier-types\").AtelierDocument,\n ): void => {\n if (!isSafePath(docId)) return;\n // Create parent dirs first so a doc:patch over the WS bridge for a\n // subfolder doc (e.g. `notes/x.atelier`) doesn't ENOENT and lose the\n // edit — mirrors what the POST /api/file handler already does.\n writeFileEnsuringDir(resolve(cwd, docId), serializeAtelier(doc));\n };\n\n // Persist LLM/system mutations to disk so the on-disk .atelier file\n // stays in lock-step with what the browser shows. Without this, a\n // refresh would lose the LLM's edits.\n bridgeState.store.onChange((id, doc, source) => {\n // Only LLM/agent edits get persisted + broadcast. \"human\" is already\n // persisted by /api/file POST and the browser is current; \"system\" is\n // GET-hydration of a file the user just opened (the bytes are already\n // on disk and in the browser) — broadcasting it would fire a phantom\n // \"agent edited document\" toast on a plain file-open.\n if (!shouldBroadcastMutation(source)) return;\n if (doc === null) return; // deletions don't touch disk in v1\n try {\n persistHumanPatch(id, doc);\n } catch {\n // Non-fatal: in-memory state is authoritative for the browser session.\n }\n broadcastToBridge(bridgeClients, {\n type: \"llm:mutation\",\n documentId: id,\n doc,\n source,\n });\n });\n\n const server = await vite.createServer({\n root: tmpDir,\n server: {\n host: HOSTNAME,\n port,\n strictPort: false,\n fs: {\n strict: false,\n },\n },\n plugins: [\n {\n name: \"atelier-api\",\n configureServer(server) {\n // ^localhost-only: Origin check on mutating endpoints.\n // Without this, any browser tab on any site can fire\n // fetch('http://localhost:4321/api/file', { method: 'POST', ... })\n // and write .atelier files into the user's CWD.\n // Only allow Origins that match this server's own loopback host.\n const allowedOrigins = new Set([\n `http://localhost:${port}`,\n `http://127.0.0.1:${port}`,\n ]);\n const MUTATING = new Set([\"POST\", \"PUT\", \"DELETE\", \"PATCH\"]);\n\n server.middlewares.use((req, res, next) => {\n const url = new URL(req.url ?? \"/\", `http://${HOSTNAME}:${port}`);\n\n if (req.method && MUTATING.has(req.method) && url.pathname.startsWith(\"/api/\")) {\n const origin = req.headers.origin;\n if (!origin || !allowedOrigins.has(origin)) {\n res.statusCode = 403;\n res.end(\"Forbidden: cross-origin mutating request rejected\");\n return;\n }\n }\n\n if (url.pathname === \"/api/files\") {\n const atelierFiles = findAtelierFiles(cwd);\n const entries = atelierFiles.map((p) => {\n const parts = p.split(\"/\");\n return {\n path: p,\n name: parts[parts.length - 1].replace(\".atelier\", \"\"),\n folder: parts.length > 1 ? parts.slice(0, -1).join(\"/\") : \"\",\n };\n });\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(entries));\n return;\n }\n\n if (url.pathname === \"/api/file\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n\n if (req.method === \"GET\") {\n try {\n const content = readFileSync(absPath, \"utf-8\");\n // Mirror into the shared store so MCP clients connecting\n // over /mcp see the same active document and the bridge\n // can broadcast it to late-joining browser clients.\n const parsed = parseAtelier(content);\n if (parsed.success) {\n bridgeState.store.set(filePath, parsed.data, \"system\");\n bridgeState.currentDocId = filePath;\n }\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(content);\n } catch {\n res.statusCode = 404;\n res.end(\"File not found\");\n }\n return;\n }\n\n if (req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => { body += chunk.toString(); });\n req.on(\"end\", () => {\n try {\n // Creates parent dirs first so a new file in a\n // subfolder (e.g. `notes/foo.atelier`) doesn't ENOENT.\n writeFileEnsuringDir(absPath, body);\n // Mirror human writes into the store with source:\"human\"\n // so onChange skips re-broadcasting (the browser already\n // has the latest — no echo needed).\n const parsed = parseAtelier(body);\n if (parsed.success) {\n bridgeState.store.set(filePath, parsed.data, \"human\");\n bridgeState.currentDocId = filePath;\n }\n res.end(\"OK\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n res.statusCode = 500;\n res.end(msg);\n }\n });\n return;\n }\n }\n\n if (url.pathname === \"/api/export\" && req.method === \"POST\") {\n const filePath = url.searchParams.get(\"path\");\n if (!filePath || !isSafePath(filePath)) {\n res.statusCode = 400;\n res.end(\"Invalid path\");\n return;\n }\n\n const absPath = resolve(cwd, filePath);\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => { chunks.push(chunk); });\n req.on(\"end\", () => {\n try {\n mkdirSync(dirname(absPath), { recursive: true });\n writeFileSync(absPath, Buffer.concat(chunks));\n res.end(\"OK\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n res.statusCode = 500;\n res.end(msg);\n }\n });\n return;\n }\n\n if (url.pathname === \"/api/cwd\") {\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ cwd }));\n return;\n }\n\n next();\n });\n },\n },\n ],\n logLevel: \"warn\",\n });\n\n await server.listen();\n\n // Mount WebSocket bridge + MCP transport on Vite's HTTP server.\n // ^localhost-only: the connect middleware does NOT run on WS upgrades\n // (Node fires 'upgrade' before connect), so we re-run the Origin\n // check here. Without this the bridge would be cross-origin-reachable\n // from any browser tab.\n const httpServer = server.httpServer;\n if (httpServer) {\n const wssBridge = new WebSocketServer({ noServer: true });\n const wssMcp = new WebSocketServer({ noServer: true });\n\n httpServer.on(\"upgrade\", (req, socket, head) => {\n const url = new URL(req.url ?? \"/\", `http://${HOSTNAME}:${port}`);\n if (url.pathname !== \"/bridge\" && url.pathname !== \"/mcp\") return;\n\n // ^localhost-only Origin check for WS upgrades. /bridge is a real\n // browser origin (strict same-origin), but /mcp serves non-browser\n // MCP clients that send NO Origin header — so /mcp tolerates a\n // missing Origin while still rejecting a present-but-foreign one.\n const origin = req.headers.origin;\n const originOk =\n url.pathname === \"/mcp\"\n ? isAllowedMcpOrigin(origin, port)\n : isAllowedOrigin(origin, port);\n if (!originOk) {\n socket.write(\"HTTP/1.1 403 Forbidden\\r\\n\\r\\n\");\n socket.destroy();\n return;\n }\n\n if (url.pathname === \"/bridge\") {\n wssBridge.handleUpgrade(req, socket, head, (ws) => {\n wssBridge.emit(\"connection\", ws, req);\n });\n return;\n }\n\n // url.pathname === \"/mcp\"\n wssMcp.handleUpgrade(req, socket, head, (ws) => {\n wssMcp.emit(\"connection\", ws, req);\n });\n });\n\n wssBridge.on(\"connection\", (ws) => {\n attachBridgeClient(\n ws,\n bridgeState,\n bridgeClients,\n loadDocFromDisk,\n persistHumanPatch,\n );\n });\n\n wssMcp.on(\"connection\", (ws) => {\n // Per-connection MCP server, shared store. Each external client\n // gets its own handler context but writes flow through the same\n // emitter the bridge subscribes to.\n const { server: mcpServer } = createMcpServer(bridgeState.store);\n const transport = new WebSocketServerTransport(ws);\n mcpServer.connect(transport).catch((err) => {\n // Connection-time failures: log and close. Per-call failures\n // surface as JSON-RPC errors via the SDK.\n console.error(\"MCP-over-WS connect failed:\", err);\n try { ws.close(); } catch { /* ignore */ }\n });\n });\n }\n\n const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;\n const url = resolvedUrl;\n\n console.log(` Server running at: ${url}`);\n\n const atelierFiles = findAtelierFiles(cwd);\n console.log(` Found ${atelierFiles.length} .atelier file(s)`);\n\n if (file) {\n console.log(` Opening: ${file}`);\n }\n\n console.log(` Press Ctrl+C to stop\\n`);\n\n if (options.open) {\n exec(`open \"${url}\"`);\n }\n\n // Keep alive and handle cleanup\n const cleanup = () => {\n console.log(\"\\nShutting down...\");\n server.close();\n try {\n rmSync(tmpDir, { recursive: true, force: true });\n } catch {\n // ignore cleanup errors\n }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n },\n );\n}\n"],"mappings":"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","names":["program","resolve","readFileSync","resolve","readFileSync","program","server","url","atelierFiles"]}
@@ -9,7 +9,7 @@ import {
9
9
  renderRef,
10
10
  renderShape,
11
11
  renderText
12
- } from "./chunk-C5DBTHXB.js";
12
+ } from "./chunk-JPZ4F4PW.js";
13
13
  export {
14
14
  ImageCache,
15
15
  applyFill,
@@ -22,4 +22,4 @@ export {
22
22
  renderShape,
23
23
  renderText
24
24
  };
25
- //# sourceMappingURL=dist-6IHF7WA7.js.map
25
+ //# sourceMappingURL=dist-M67UZGFQ.js.map