@a-company/atelier 0.29.0 → 0.37.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.
Files changed (76) hide show
  1. package/dist/chunk-5QQESXI6.js +4432 -0
  2. package/dist/chunk-5QQESXI6.js.map +1 -0
  3. package/dist/cli.cjs +2391 -530
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +301 -429
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +2233 -38
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +584 -2
  10. package/dist/index.d.ts +584 -2
  11. package/dist/index.js +111 -3
  12. package/dist/mcp.cjs +1215 -365
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +1209 -365
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +20 -9
  17. package/src/web/inline-app.ts +867 -0
  18. package/src/web/tsconfig.json +9 -0
  19. package/templates/welcome.atelier +67 -0
  20. package/university/content/notes/N-atel-001-first-render.md +114 -0
  21. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  22. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  23. package/university/content/notes/N-atel-101-easings.md +97 -0
  24. package/university/content/notes/N-atel-101-layers.md +106 -0
  25. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  26. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  27. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  28. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  29. package/university/content/notes/N-atel-201-patterns.md +108 -0
  30. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  31. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  32. package/university/content/notes/N-atel-301-effects.md +136 -0
  33. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  34. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  35. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  36. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  37. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  38. package/university/content/notes/N-atel-401-transitions.md +94 -0
  39. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  40. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  41. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  42. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  43. package/university/content/notes/N-atel-601-carousel.md +71 -0
  44. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  45. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  46. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  47. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  48. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  49. package/university/content/notes/N-atel-701-vector.md +85 -0
  50. package/university/content/notes/N-atel-701-video.md +88 -0
  51. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  52. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  53. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  54. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  55. package/university/content/paths/LP-atel-001.yaml +21 -0
  56. package/university/content/paths/LP-atel-101.yaml +22 -0
  57. package/university/content/paths/LP-atel-201.yaml +23 -0
  58. package/university/content/paths/LP-atel-301.yaml +22 -0
  59. package/university/content/paths/LP-atel-401.yaml +22 -0
  60. package/university/content/paths/LP-atel-501.yaml +22 -0
  61. package/university/content/paths/LP-atel-601.yaml +22 -0
  62. package/university/content/paths/LP-atel-701.yaml +22 -0
  63. package/university/content/paths/LP-atel-801.yaml +22 -0
  64. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  65. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  66. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  67. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  68. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  69. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  70. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  71. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  72. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  73. package/university/index.yaml +720 -0
  74. package/university/pack.yaml +21 -0
  75. package/dist/chunk-JV7RGETS.js +0 -2292
  76. package/dist/chunk-JV7RGETS.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,16 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ applyRecipeCommand,
3
4
  assetsCommand,
5
+ captionsCommand,
6
+ carouselCommand,
7
+ exportImageCommand,
4
8
  exportLottieCommand,
5
9
  exportSvgCommand,
6
10
  infoCommand,
7
11
  parseAtelier,
12
+ recipeCommand,
8
13
  renderCommand,
14
+ serializeAtelier,
9
15
  stillCommand,
16
+ transcribeCommand,
17
+ transcriptCommand,
18
+ trimCommand,
10
19
  validateCommand,
11
20
  validateVideoLayer,
12
21
  variablesCommand
13
- } from "./chunk-JV7RGETS.js";
22
+ } from "./chunk-5QQESXI6.js";
14
23
  import {
15
24
  validateAllDeltas
16
25
  } from "./chunk-JPZ4F4PW.js";
@@ -115,11 +124,19 @@ function lintCommand(program2) {
115
124
  }
116
125
 
117
126
  // src/commands/studio.ts
118
- import { resolve as resolve2, join, relative, dirname } from "path";
119
- import { mkdirSync, writeFileSync, rmSync, readFileSync as readFileSync2, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from "fs";
127
+ import { resolve as resolve2, join, relative, dirname, sep } from "path";
128
+ import { mkdirSync, writeFileSync, rmSync, readFileSync as readFileSync2, readdirSync, statSync, realpathSync, symlinkSync, existsSync, copyFileSync } from "fs";
120
129
  import { tmpdir } from "os";
121
130
  import { randomBytes } from "crypto";
122
131
  import { exec } from "child_process";
132
+ import {
133
+ DocumentStore,
134
+ WebSocketServerTransport,
135
+ createServer as createMcpServer,
136
+ BRIDGE_PROTOCOL_VERSION,
137
+ isBridgeEnvelope
138
+ } from "@a-company/atelier-mcp";
139
+ import { WebSocketServer } from "ws";
123
140
  function findAtelierFiles(dir, base = dir) {
124
141
  const results = [];
125
142
  let entries;
@@ -146,9 +163,119 @@ function findAtelierFiles(dir, base = dir) {
146
163
  return results.sort();
147
164
  }
148
165
  function isSafePath(filePath) {
149
- if (!filePath || filePath.includes("..") || filePath.startsWith("/")) return false;
150
- const resolved = resolve2(process.cwd(), filePath);
151
- return resolved.startsWith(process.cwd());
166
+ if (!filePath || filePath.startsWith("/")) return false;
167
+ const cwd = process.cwd();
168
+ const resolved = resolve2(cwd, filePath);
169
+ return resolved === cwd || resolved.startsWith(cwd + sep);
170
+ }
171
+ function writeFileEnsuringDir(absPath, body) {
172
+ mkdirSync(dirname(absPath), { recursive: true });
173
+ writeFileSync(absPath, body, "utf-8");
174
+ }
175
+ function isAllowedOrigin(origin, port) {
176
+ if (!origin) return false;
177
+ return origin === `http://localhost:${port}` || origin === `http://127.0.0.1:${port}`;
178
+ }
179
+ function isAllowedMcpOrigin(origin, port) {
180
+ return origin === void 0 || isAllowedOrigin(origin, port);
181
+ }
182
+ function shouldBroadcastMutation(source) {
183
+ return source === "llm";
184
+ }
185
+ function attachBridgeClient(ws, state, clients, loadDocFromDisk, persistHumanPatch) {
186
+ clients.add(ws);
187
+ const clientId = randomBytes(6).toString("hex");
188
+ const send = (env) => {
189
+ if (ws.readyState !== ws.OPEN) return;
190
+ ws.send(JSON.stringify(env));
191
+ };
192
+ send({ type: "hello", clientId, protocolVersion: BRIDGE_PROTOCOL_VERSION });
193
+ if (state.currentDocId) {
194
+ const existing = state.store.get(state.currentDocId);
195
+ if (existing) {
196
+ send({ type: "doc:loaded", documentId: state.currentDocId, doc: existing });
197
+ } else {
198
+ const raw = loadDocFromDisk(state.currentDocId);
199
+ if (raw) {
200
+ const parsed = parseAtelier(raw);
201
+ if (parsed.success) {
202
+ state.store.set(state.currentDocId, parsed.data, "system");
203
+ send({ type: "doc:loaded", documentId: state.currentDocId, doc: parsed.data });
204
+ }
205
+ }
206
+ }
207
+ }
208
+ const onMessage = (data) => {
209
+ let text;
210
+ if (typeof data === "string") text = data;
211
+ else if (data instanceof Buffer) text = data.toString("utf-8");
212
+ else if (Array.isArray(data)) text = Buffer.concat(data).toString("utf-8");
213
+ else text = String(data);
214
+ let env;
215
+ try {
216
+ env = JSON.parse(text);
217
+ } catch {
218
+ send({ type: "error", code: "parse_error", message: "invalid JSON" });
219
+ return;
220
+ }
221
+ if (!isBridgeEnvelope(env)) {
222
+ send({ type: "error", code: "invalid_envelope", message: "unknown envelope shape" });
223
+ return;
224
+ }
225
+ if (env.type === "doc:patch") {
226
+ try {
227
+ state.store.set(env.documentId, env.doc, "human");
228
+ state.currentDocId = env.documentId;
229
+ persistHumanPatch(env.documentId, env.doc);
230
+ } catch (err) {
231
+ const msg = err instanceof Error ? err.message : String(err);
232
+ send({ type: "error", code: "persist_failed", message: msg, opId: env.opId });
233
+ }
234
+ return;
235
+ }
236
+ if (env.type === "doc:load") {
237
+ const existing = state.store.get(env.documentId);
238
+ if (existing) {
239
+ send({ type: "doc:loaded", documentId: env.documentId, doc: existing });
240
+ return;
241
+ }
242
+ const raw = loadDocFromDisk(env.documentId);
243
+ if (raw) {
244
+ const parsed = parseAtelier(raw);
245
+ if (parsed.success) {
246
+ state.store.set(env.documentId, parsed.data, "system");
247
+ send({ type: "doc:loaded", documentId: env.documentId, doc: parsed.data });
248
+ return;
249
+ }
250
+ }
251
+ send({ type: "error", code: "not_found", message: `document ${env.documentId} not found` });
252
+ return;
253
+ }
254
+ };
255
+ const onClose = () => {
256
+ clients.delete(ws);
257
+ };
258
+ ws.on("message", onMessage);
259
+ ws.on("close", onClose);
260
+ ws.on("error", onClose);
261
+ return () => {
262
+ clients.delete(ws);
263
+ try {
264
+ ws.close();
265
+ } catch {
266
+ }
267
+ };
268
+ }
269
+ function broadcastToBridge(clients, envelope) {
270
+ const payload = JSON.stringify(envelope);
271
+ for (const ws of clients) {
272
+ if (ws.readyState === ws.OPEN) {
273
+ try {
274
+ ws.send(payload);
275
+ } catch {
276
+ }
277
+ }
278
+ }
152
279
  }
153
280
  function getInlineHTML() {
154
281
  return `<!DOCTYPE html>
@@ -167,431 +294,59 @@ function getInlineHTML() {
167
294
  </body>
168
295
  </html>`;
169
296
  }
170
- function getInlineApp(initialFile) {
297
+ function getInlineApp(initialFile, cliPackageDir) {
171
298
  const initialFileStr = initialFile ? JSON.stringify(initialFile) : "null";
172
- return `import { AtelierStudio, exportDocument, ImageCache } from "@a-company/atelier-studio";
173
- import "@a-company/atelier-studio/styles.css";
174
- import { parseAtelier, serializeAtelier } from "@a-company/atelier-schema";
175
-
176
- // \u2500\u2500 Types \u2500\u2500
177
- interface FileEntry {
178
- path: string;
179
- name: string;
180
- folder: string;
181
- }
182
-
183
- // \u2500\u2500 State \u2500\u2500
184
- let studio: AtelierStudio | null = null;
185
- let currentFile: string | null = null;
186
- let files: FileEntry[] = [];
187
- let saveTimeout: ReturnType<typeof setTimeout> | null = null;
188
-
189
- // \u2500\u2500 API helpers \u2500\u2500
190
- async function fetchFiles(): Promise<FileEntry[]> {
191
- const res = await fetch("/api/files");
192
- return res.json();
193
- }
194
-
195
- async function fetchFileContent(path: string): Promise<string> {
196
- const res = await fetch("/api/file?path=" + encodeURIComponent(path));
197
- return res.text();
198
- }
199
-
200
- async function saveFileContent(path: string, content: string): Promise<void> {
201
- await fetch("/api/file?path=" + encodeURIComponent(path), {
202
- method: "POST",
203
- headers: { "Content-Type": "text/plain" },
204
- body: content,
205
- });
206
- }
207
-
208
- async function saveExportBlob(path: string, blob: Blob): Promise<void> {
209
- const buf = await blob.arrayBuffer();
210
- await fetch("/api/export?path=" + encodeURIComponent(path), {
211
- method: "POST",
212
- headers: { "Content-Type": "application/octet-stream" },
213
- body: buf,
214
- });
299
+ const appModulePath = join(cliPackageDir, "src", "web", "inline-app.ts").split(sep).join("/");
300
+ return `import { bootStudioApp } from ${JSON.stringify(appModulePath)};
301
+ bootStudioApp({ initialFile: ${initialFileStr} });
302
+ `;
215
303
  }
216
-
217
- async function exportAll(format: "gif" | "mp4" | "webm"): Promise<void> {
218
- if (files.length === 0) return;
219
-
220
- // Create progress overlay
221
- const overlay = document.createElement("div");
222
- 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";
223
- const card = document.createElement("div");
224
- 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";
225
- overlay.appendChild(card);
226
- document.body.appendChild(overlay);
227
-
228
- const title = document.createElement("div");
229
- title.style.cssText = "font-size:18px;margin-bottom:16px;font-weight:600";
230
- title.textContent = "Exporting All Files\u2026";
231
- card.appendChild(title);
232
-
233
- const fileLabel = document.createElement("div");
234
- fileLabel.style.cssText = "font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace";
235
- card.appendChild(fileLabel);
236
-
237
- const progress = document.createElement("progress");
238
- progress.style.cssText = "width:100%;height:6px;appearance:none;-webkit-appearance:none";
239
- progress.max = files.length;
240
- progress.value = 0;
241
- card.appendChild(progress);
242
-
243
- const statusText = document.createElement("div");
244
- statusText.style.cssText = "font-size:12px;color:#A89F95;margin-top:8px";
245
- card.appendChild(statusText);
246
-
247
- let exported = 0;
248
- let errors = 0;
249
-
250
- for (const file of files) {
251
- fileLabel.textContent = file.path;
252
- statusText.textContent = (exported + errors + 1) + " / " + files.length;
253
-
254
- try {
255
- const content = await fetchFileContent(file.path);
256
- const result = parseAtelier(content);
257
- if (!result.success) {
258
- errors++;
259
- progress.value = exported + errors;
260
- continue;
261
- }
262
-
263
- const doc = result.data;
264
- const w = doc.canvas.width;
265
- const h = doc.canvas.height;
266
- const canvas = document.createElement("canvas");
267
- canvas.width = w;
268
- canvas.height = h;
269
- const imageCache = new ImageCache();
270
-
271
- const exportResult = await exportDocument(doc, canvas, imageCache, {
272
- format,
273
- onProgress: ({ percent }) => {
274
- statusText.textContent = (exported + errors + 1) + " / " + files.length + " \u2014 " + percent + "%";
275
- },
276
- });
277
-
278
- // Save alongside the source file: e.g. "dir/my-anim.atelier" \u2192 "dir/my-anim.gif"
279
- const outPath = file.path.replace(/\\.atelier$/, "." + format);
280
- await saveExportBlob(outPath, exportResult.blob);
281
- exported++;
282
- } catch (e) {
283
- console.error("Export failed:", file.path, e);
284
- errors++;
304
+ function resolveCliPackageDir() {
305
+ const here = dirname(new URL(import.meta.url).pathname);
306
+ const candidates = [
307
+ resolve2(here, ".."),
308
+ resolve2(here, "..", "..")
309
+ ];
310
+ for (const c of candidates) {
311
+ if (existsSync(join(c, "package.json"))) {
312
+ return c;
285
313
  }
286
- progress.value = exported + errors;
287
314
  }
288
-
289
- // Done
290
- title.textContent = "Export Complete";
291
- fileLabel.textContent = "";
292
- statusText.textContent = exported + " exported" + (errors > 0 ? ", " + errors + " failed" : "");
293
- if (errors > 0) console.warn("Export All finished with " + errors + " error(s). Check console for details.");
294
-
295
- const closeBtn = document.createElement("button");
296
- 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";
297
- closeBtn.textContent = "Close";
298
- closeBtn.addEventListener("click", () => document.body.removeChild(overlay));
299
- card.appendChild(closeBtn);
315
+ return candidates[0];
300
316
  }
301
-
302
- // \u2500\u2500 Theme (matches branded theme from showcase) \u2500\u2500
303
- const theme = {
304
- bg: "#2C2C2C",
305
- bgSecondary: "#333333",
306
- bgTertiary: "#3D3D3D",
307
- text: "#F5F0EB",
308
- textMuted: "#A89F95",
309
- textAccent: "#F5F0EB",
310
- border: "#4A4A4A",
311
- buttonBg: "#3D3D3D",
312
- buttonHover: "#4A4A4A",
313
- buttonActive: "#555555",
314
- accent: "#C75B39",
315
- accentHover: "#D4724E",
316
- sliderTrack: "#4A4A4A",
317
- sliderThumb: "#C75B39",
318
- fontFamily: "'Cormorant Garamond', Georgia, serif",
319
- fontMono: "'SF Mono', 'Fira Code', monospace",
320
- canvasShadow: "0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)",
321
- };
322
-
323
- // \u2500\u2500 Styles \u2500\u2500
324
- const style = document.createElement("style");
325
- style.textContent = \`
326
- * { margin: 0; padding: 0; box-sizing: border-box; }
327
- html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }
328
- body { font-family: 'Cormorant Garamond', Georgia, serif; }
329
- #studio { display: flex; height: 100vh; width: 100vw; }
330
-
331
- .sidebar {
332
- width: 260px;
333
- min-width: 260px;
334
- background: #333333;
335
- border-right: 1px solid #4A4A4A;
336
- display: flex;
337
- flex-direction: column;
338
- overflow: hidden;
339
- }
340
- .sidebar__header {
341
- padding: 16px 20px;
342
- border-bottom: 1px solid #4A4A4A;
343
- font-size: 11px;
344
- font-weight: 600;
345
- letter-spacing: 2px;
346
- text-transform: uppercase;
347
- color: #A89F95;
348
- display: flex;
349
- align-items: center;
350
- gap: 8px;
351
- }
352
- .sidebar__header span {
353
- color: #C75B39;
354
- font-size: 13px;
355
- }
356
- .sidebar__list {
357
- flex: 1;
358
- overflow-y: auto;
359
- padding: 8px 0;
360
- }
361
- .sidebar__list::-webkit-scrollbar { width: 6px; }
362
- .sidebar__list::-webkit-scrollbar-track { background: transparent; }
363
- .sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }
364
-
365
- .sidebar__folder {
366
- padding: 10px 20px 4px;
367
- font-size: 10px;
368
- font-weight: 600;
369
- letter-spacing: 1.5px;
370
- text-transform: uppercase;
371
- color: #A89F95;
372
- }
373
- .sidebar__item {
374
- padding: 8px 20px 8px 28px;
375
- font-size: 13px;
376
- cursor: pointer;
377
- color: #A89F95;
378
- transition: background 0.15s, color 0.15s;
379
- white-space: nowrap;
380
- overflow: hidden;
381
- text-overflow: ellipsis;
382
- font-family: 'SF Mono', 'Fira Code', monospace;
383
- font-size: 11.5px;
384
- }
385
- .sidebar__item:hover { background: #363636; color: #F5F0EB; }
386
- .sidebar__item--active {
387
- background: rgba(199, 91, 57, 0.12) !important;
388
- color: #C75B39 !important;
389
- }
390
-
391
- .main {
392
- flex: 1;
393
- display: flex;
394
- flex-direction: column;
395
- overflow: hidden;
396
- }
397
- .main__status {
398
- height: 32px;
399
- min-height: 32px;
400
- display: flex;
401
- align-items: center;
402
- padding: 0 16px;
403
- background: #333333;
404
- border-bottom: 1px solid #4A4A4A;
405
- font-size: 11px;
406
- color: #A89F95;
407
- font-family: 'SF Mono', 'Fira Code', monospace;
408
- gap: 12px;
409
- }
410
- .main__status .save-indicator {
411
- display: inline-flex;
412
- align-items: center;
413
- gap: 4px;
414
- margin-left: auto;
415
- transition: opacity 0.3s;
416
- }
417
- .main__status .save-indicator--saving { color: #C75B39; }
418
- .main__status .save-indicator--saved { color: #6B8E6B; }
419
- .main__editor {
420
- flex: 1;
421
- overflow: hidden;
422
- }
423
- .main__empty {
424
- flex: 1;
425
- display: flex;
426
- align-items: center;
427
- justify-content: center;
428
- color: #A89F95;
429
- font-size: 18px;
317
+ function scaffoldWelcomeIfEmpty(cwd, cliPackageDir) {
318
+ const existing = findAtelierFiles(cwd);
319
+ if (existing.length > 0) return null;
320
+ const templatesDir = join(cliPackageDir, "templates");
321
+ const welcomeSrc = join(templatesDir, "welcome.atelier");
322
+ if (!existsSync(welcomeSrc)) return null;
323
+ const welcomeDest = join(cwd, "welcome.atelier");
324
+ if (existsSync(welcomeDest)) return null;
325
+ try {
326
+ copyFileSync(welcomeSrc, welcomeDest);
327
+ } catch {
328
+ return null;
430
329
  }
431
- \`;
432
- document.head.appendChild(style);
433
-
434
- // \u2500\u2500 Build UI \u2500\u2500
435
- const root = document.getElementById("studio")!;
436
- const sidebar = document.createElement("div");
437
- sidebar.className = "sidebar";
438
-
439
- const sidebarHeader = document.createElement("div");
440
- sidebarHeader.className = "sidebar__header";
441
- sidebarHeader.innerHTML = '<span>&#9670;</span> ATELIER STUDIO';
442
- sidebar.appendChild(sidebarHeader);
443
-
444
- const sidebarList = document.createElement("div");
445
- sidebarList.className = "sidebar__list";
446
- sidebar.appendChild(sidebarList);
447
-
448
- const sidebarFooter = document.createElement("div");
449
- sidebarFooter.style.cssText = "padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center";
450
- const exportAllSelect = document.createElement("select");
451
- exportAllSelect.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";
452
- for (const [val, label] of [["gif","GIF"],["mp4","MP4"],["webm","WebM"]] as const) {
453
- const o = document.createElement("option");
454
- o.value = val;
455
- o.textContent = label;
456
- exportAllSelect.appendChild(o);
457
- }
458
- sidebarFooter.appendChild(exportAllSelect);
459
- const exportAllBtn = document.createElement("button");
460
- exportAllBtn.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";
461
- exportAllBtn.textContent = "Export All";
462
- exportAllBtn.addEventListener("click", () => {
463
- exportAll(exportAllSelect.value as "gif" | "mp4" | "webm");
464
- });
465
- sidebarFooter.appendChild(exportAllBtn);
466
- sidebar.appendChild(sidebarFooter);
467
-
468
- const main = document.createElement("div");
469
- main.className = "main";
470
-
471
- const statusBar = document.createElement("div");
472
- statusBar.className = "main__status";
473
- main.appendChild(statusBar);
474
-
475
- const editorContainer = document.createElement("div");
476
- editorContainer.className = "main__editor";
477
- main.appendChild(editorContainer);
478
-
479
- root.appendChild(sidebar);
480
- root.appendChild(main);
481
-
482
- // \u2500\u2500 File list rendering \u2500\u2500
483
- function renderFileList(): void {
484
- sidebarList.innerHTML = "";
485
- let lastFolder = "";
486
-
487
- for (const file of files) {
488
- if (file.folder && file.folder !== lastFolder) {
489
- lastFolder = file.folder;
490
- const folder = document.createElement("div");
491
- folder.className = "sidebar__folder";
492
- folder.textContent = file.folder;
493
- sidebarList.appendChild(folder);
330
+ const bgSrc = join(templatesDir, "welcome-bg.png");
331
+ if (existsSync(bgSrc)) {
332
+ const bgDest = join(cwd, "welcome-bg.png");
333
+ if (!existsSync(bgDest)) {
334
+ try {
335
+ copyFileSync(bgSrc, bgDest);
336
+ } catch {
337
+ }
494
338
  }
495
-
496
- const item = document.createElement("div");
497
- item.className = "sidebar__item" + (file.path === currentFile ? " sidebar__item--active" : "");
498
- item.textContent = file.name;
499
- item.title = file.path;
500
- item.addEventListener("click", () => loadFile(file.path));
501
- sidebarList.appendChild(item);
502
339
  }
340
+ return "welcome.atelier";
503
341
  }
504
-
505
- // \u2500\u2500 Load a file into the studio \u2500\u2500
506
- async function loadFile(path: string): Promise<void> {
507
- currentFile = path;
508
- renderFileList();
509
-
510
- const content = await fetchFileContent(path);
511
- const result = parseAtelier(content);
512
-
513
- if (!result.success) {
514
- editorContainer.innerHTML = "";
515
- const err = document.createElement("div");
516
- err.className = "main__empty";
517
- err.style.flexDirection = "column";
518
- err.style.gap = "8px";
519
- err.innerHTML = '<div style="color:#C75B39">Parse Error</div><div style="font-size:13px;font-family:monospace">' +
520
- result.errors.map(e => e.path + ": " + e.message).join("<br>") + "</div>";
521
- editorContainer.appendChild(err);
522
- return;
523
- }
524
-
525
- statusBar.innerHTML = '<span>' + path + '</span><span class="save-indicator save-indicator--saved">&#10003; saved</span>';
526
-
527
- if (studio) {
528
- studio.destroy();
529
- studio = null;
530
- }
531
-
532
- // Set filename for export downloads (strip path and .atelier extension)
533
- const baseName = path.split("/").pop()?.replace(/\\.atelier$/, "") || null;
534
-
535
- studio = new AtelierStudio(editorContainer, {
536
- mode: "full",
537
- initialTab: "yaml",
538
- allowSave: true,
539
- onDocumentChange: (doc) => {
540
- // Auto-save with debounce
541
- const indicator = statusBar.querySelector(".save-indicator");
542
- if (indicator) {
543
- indicator.className = "save-indicator save-indicator--saving";
544
- indicator.innerHTML = "&#9679; saving...";
545
- }
546
-
547
- if (saveTimeout) clearTimeout(saveTimeout);
548
- saveTimeout = setTimeout(async () => {
549
- if (!currentFile) return;
550
- const yaml = serializeAtelier(doc);
551
- await saveFileContent(currentFile, yaml);
552
- const ind = statusBar.querySelector(".save-indicator");
553
- if (ind) {
554
- ind.className = "save-indicator save-indicator--saved";
555
- ind.innerHTML = "&#10003; saved";
556
- }
557
- }, 800);
558
- },
559
- });
560
- studio.setTheme(theme);
561
- studio.setFilename(baseName);
562
- studio.loadDocument(result.data);
563
- }
564
-
565
- // \u2500\u2500 Boot \u2500\u2500
566
- async function boot(): Promise<void> {
567
- files = await fetchFiles();
568
-
569
- if (files.length === 0) {
570
- editorContainer.innerHTML = "";
571
- const empty = document.createElement("div");
572
- empty.className = "main__empty";
573
- empty.textContent = "No .atelier files found in this directory";
574
- editorContainer.appendChild(empty);
575
- statusBar.textContent = "No files";
576
- renderFileList();
577
- return;
578
- }
579
-
580
- renderFileList();
581
-
582
- const initialFile = ${initialFileStr};
583
- const target = initialFile
584
- ? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))
585
- : files[0];
586
-
587
- if (target) {
588
- await loadFile(target.path);
342
+ function readCliVersion(cliPackageDir) {
343
+ try {
344
+ const pkg = JSON.parse(readFileSync2(join(cliPackageDir, "package.json"), "utf-8"));
345
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
346
+ } catch {
347
+ return "unknown";
589
348
  }
590
349
  }
591
-
592
- boot();
593
- `;
594
- }
595
350
  function studioCommand(program2) {
596
351
  program2.command("studio [file]").description("Launch the browser-based Atelier editor").option("-p, --port <number>", "Port to serve on", "4321").option("--no-open", "Don't auto-open browser").action(
597
352
  async (file, options) => {
@@ -601,13 +356,20 @@ function studioCommand(program2) {
601
356
  process.exit(1);
602
357
  }
603
358
  const cwd = process.cwd();
604
- const cliPackageDir = resolve2(dirname(new URL(import.meta.url).pathname), "..");
359
+ const cliPackageDir = resolveCliPackageDir();
360
+ const version = readCliVersion(cliPackageDir);
361
+ const scaffolded = scaffoldWelcomeIfEmpty(cwd, cliPackageDir);
362
+ console.log("");
363
+ console.log(` Atelier Studio \xB7 v${version}`);
364
+ if (scaffolded) {
365
+ console.log(` Scaffolded ${scaffolded} \u2014 opening\u2026`);
366
+ }
605
367
  const tmpId = randomBytes(4).toString("hex");
606
368
  const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);
607
369
  mkdirSync(tmpDirRaw, { recursive: true });
608
370
  const tmpDir = realpathSync(tmpDirRaw);
609
371
  writeFileSync(join(tmpDir, "index.html"), getInlineHTML());
610
- writeFileSync(join(tmpDir, "main.ts"), getInlineApp(file ?? null));
372
+ writeFileSync(join(tmpDir, "main.ts"), getInlineApp(file ?? null, cliPackageDir));
611
373
  const cliNodeModules = join(cliPackageDir, "node_modules");
612
374
  if (existsSync(cliNodeModules)) {
613
375
  try {
@@ -615,7 +377,6 @@ function studioCommand(program2) {
615
377
  } catch {
616
378
  }
617
379
  }
618
- console.log(`Starting Atelier Studio...`);
619
380
  console.log(` Working directory: ${cwd}`);
620
381
  let vite;
621
382
  try {
@@ -626,9 +387,42 @@ function studioCommand(program2) {
626
387
  process.exit(1);
627
388
  return;
628
389
  }
390
+ const HOSTNAME = "127.0.0.1";
391
+ const bridgeState = {
392
+ store: new DocumentStore(),
393
+ currentDocId: null
394
+ };
395
+ const bridgeClients = /* @__PURE__ */ new Set();
396
+ const loadDocFromDisk = (docId) => {
397
+ if (!isSafePath(docId)) return null;
398
+ try {
399
+ return readFileSync2(resolve2(cwd, docId), "utf-8");
400
+ } catch {
401
+ return null;
402
+ }
403
+ };
404
+ const persistHumanPatch = (docId, doc) => {
405
+ if (!isSafePath(docId)) return;
406
+ writeFileEnsuringDir(resolve2(cwd, docId), serializeAtelier(doc));
407
+ };
408
+ bridgeState.store.onChange((id, doc, source) => {
409
+ if (!shouldBroadcastMutation(source)) return;
410
+ if (doc === null) return;
411
+ try {
412
+ persistHumanPatch(id, doc);
413
+ } catch {
414
+ }
415
+ broadcastToBridge(bridgeClients, {
416
+ type: "llm:mutation",
417
+ documentId: id,
418
+ doc,
419
+ source
420
+ });
421
+ });
629
422
  const server = await vite.createServer({
630
423
  root: tmpDir,
631
424
  server: {
425
+ host: HOSTNAME,
632
426
  port,
633
427
  strictPort: false,
634
428
  fs: {
@@ -639,8 +433,21 @@ function studioCommand(program2) {
639
433
  {
640
434
  name: "atelier-api",
641
435
  configureServer(server2) {
436
+ const allowedOrigins = /* @__PURE__ */ new Set([
437
+ `http://localhost:${port}`,
438
+ `http://127.0.0.1:${port}`
439
+ ]);
440
+ const MUTATING = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
642
441
  server2.middlewares.use((req, res, next) => {
643
- const url2 = new URL(req.url ?? "/", `http://localhost:${port}`);
442
+ const url2 = new URL(req.url ?? "/", `http://${HOSTNAME}:${port}`);
443
+ if (req.method && MUTATING.has(req.method) && url2.pathname.startsWith("/api/")) {
444
+ const origin = req.headers.origin;
445
+ if (!origin || !allowedOrigins.has(origin)) {
446
+ res.statusCode = 403;
447
+ res.end("Forbidden: cross-origin mutating request rejected");
448
+ return;
449
+ }
450
+ }
644
451
  if (url2.pathname === "/api/files") {
645
452
  const atelierFiles2 = findAtelierFiles(cwd);
646
453
  const entries = atelierFiles2.map((p) => {
@@ -666,6 +473,11 @@ function studioCommand(program2) {
666
473
  if (req.method === "GET") {
667
474
  try {
668
475
  const content = readFileSync2(absPath, "utf-8");
476
+ const parsed = parseAtelier(content);
477
+ if (parsed.success) {
478
+ bridgeState.store.set(filePath, parsed.data, "system");
479
+ bridgeState.currentDocId = filePath;
480
+ }
669
481
  res.setHeader("Content-Type", "text/plain");
670
482
  res.end(content);
671
483
  } catch {
@@ -681,11 +493,17 @@ function studioCommand(program2) {
681
493
  });
682
494
  req.on("end", () => {
683
495
  try {
684
- writeFileSync(absPath, body, "utf-8");
496
+ writeFileEnsuringDir(absPath, body);
497
+ const parsed = parseAtelier(body);
498
+ if (parsed.success) {
499
+ bridgeState.store.set(filePath, parsed.data, "human");
500
+ bridgeState.currentDocId = filePath;
501
+ }
685
502
  res.end("OK");
686
- } catch {
503
+ } catch (e) {
504
+ const msg = e instanceof Error ? e.message : String(e);
687
505
  res.statusCode = 500;
688
- res.end("Write failed");
506
+ res.end(msg);
689
507
  }
690
508
  });
691
509
  return;
@@ -708,9 +526,10 @@ function studioCommand(program2) {
708
526
  mkdirSync(dirname(absPath), { recursive: true });
709
527
  writeFileSync(absPath, Buffer.concat(chunks));
710
528
  res.end("OK");
711
- } catch {
529
+ } catch (e) {
530
+ const msg = e instanceof Error ? e.message : String(e);
712
531
  res.statusCode = 500;
713
- res.end("Write failed");
532
+ res.end(msg);
714
533
  }
715
534
  });
716
535
  return;
@@ -728,6 +547,51 @@ function studioCommand(program2) {
728
547
  logLevel: "warn"
729
548
  });
730
549
  await server.listen();
550
+ const httpServer = server.httpServer;
551
+ if (httpServer) {
552
+ const wssBridge = new WebSocketServer({ noServer: true });
553
+ const wssMcp = new WebSocketServer({ noServer: true });
554
+ httpServer.on("upgrade", (req, socket, head) => {
555
+ const url2 = new URL(req.url ?? "/", `http://${HOSTNAME}:${port}`);
556
+ if (url2.pathname !== "/bridge" && url2.pathname !== "/mcp") return;
557
+ const origin = req.headers.origin;
558
+ const originOk = url2.pathname === "/mcp" ? isAllowedMcpOrigin(origin, port) : isAllowedOrigin(origin, port);
559
+ if (!originOk) {
560
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
561
+ socket.destroy();
562
+ return;
563
+ }
564
+ if (url2.pathname === "/bridge") {
565
+ wssBridge.handleUpgrade(req, socket, head, (ws) => {
566
+ wssBridge.emit("connection", ws, req);
567
+ });
568
+ return;
569
+ }
570
+ wssMcp.handleUpgrade(req, socket, head, (ws) => {
571
+ wssMcp.emit("connection", ws, req);
572
+ });
573
+ });
574
+ wssBridge.on("connection", (ws) => {
575
+ attachBridgeClient(
576
+ ws,
577
+ bridgeState,
578
+ bridgeClients,
579
+ loadDocFromDisk,
580
+ persistHumanPatch
581
+ );
582
+ });
583
+ wssMcp.on("connection", (ws) => {
584
+ const { server: mcpServer } = createMcpServer(bridgeState.store);
585
+ const transport = new WebSocketServerTransport(ws);
586
+ mcpServer.connect(transport).catch((err) => {
587
+ console.error("MCP-over-WS connect failed:", err);
588
+ try {
589
+ ws.close();
590
+ } catch {
591
+ }
592
+ });
593
+ });
594
+ }
731
595
  const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;
732
596
  const url = resolvedUrl;
733
597
  console.log(` Server running at: ${url}`);
@@ -761,11 +625,19 @@ var program = new Command();
761
625
  program.name("atelier").description("Atelier animation CLI").version(createRequire(import.meta.url)("../package.json").version);
762
626
  validateCommand(program);
763
627
  lintCommand(program);
628
+ trimCommand(program);
629
+ transcribeCommand(program);
630
+ transcriptCommand(program);
631
+ captionsCommand(program);
632
+ recipeCommand(program);
633
+ applyRecipeCommand(program);
634
+ carouselCommand(program);
764
635
  infoCommand(program);
765
636
  stillCommand(program);
766
637
  renderCommand(program);
767
638
  exportSvgCommand(program);
768
639
  exportLottieCommand(program);
640
+ exportImageCommand(program);
769
641
  assetsCommand(program);
770
642
  variablesCommand(program);
771
643
  studioCommand(program);