@a-company/atelier 0.28.2 → 0.29.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/{chunk-C5DBTHXB.js → chunk-JPZ4F4PW.js} +44 -2
- package/dist/chunk-JPZ4F4PW.js.map +1 -0
- package/dist/{chunk-LC7ICNMN.js → chunk-JV7RGETS.js} +54 -4
- package/dist/chunk-JV7RGETS.js.map +1 -0
- package/dist/cli.cjs +253 -66
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +109 -9
- package/dist/cli.js.map +1 -1
- package/dist/{dist-6IHF7WA7.js → dist-M67UZGFQ.js} +2 -2
- package/dist/index.cjs +63 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp.cjs +32 -2
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +32 -2
- package/dist/mcp.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-C5DBTHXB.js.map +0 -1
- package/dist/chunk-LC7ICNMN.js.map +0 -1
- /package/dist/{dist-6IHF7WA7.js.map → dist-M67UZGFQ.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,20 +4,119 @@ import {
|
|
|
4
4
|
exportLottieCommand,
|
|
5
5
|
exportSvgCommand,
|
|
6
6
|
infoCommand,
|
|
7
|
+
parseAtelier,
|
|
7
8
|
renderCommand,
|
|
8
9
|
stillCommand,
|
|
9
10
|
validateCommand,
|
|
11
|
+
validateVideoLayer,
|
|
10
12
|
variablesCommand
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import
|
|
13
|
+
} from "./chunk-JV7RGETS.js";
|
|
14
|
+
import {
|
|
15
|
+
validateAllDeltas
|
|
16
|
+
} from "./chunk-JPZ4F4PW.js";
|
|
13
17
|
|
|
14
18
|
// src/cli.ts
|
|
15
19
|
import { createRequire } from "module";
|
|
16
20
|
import { Command } from "commander";
|
|
17
21
|
|
|
22
|
+
// src/commands/lint.ts
|
|
23
|
+
import { readFileSync } from "fs";
|
|
24
|
+
import { resolve } from "path";
|
|
25
|
+
function lintFile(filePath) {
|
|
26
|
+
const absPath = resolve(filePath);
|
|
27
|
+
let content;
|
|
28
|
+
try {
|
|
29
|
+
content = readFileSync(absPath, "utf-8");
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
file: absPath,
|
|
33
|
+
valid: false,
|
|
34
|
+
gates: [
|
|
35
|
+
{
|
|
36
|
+
gate: "^valid-document",
|
|
37
|
+
pass: false,
|
|
38
|
+
errors: [`Cannot read file: ${absPath}`]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const gates = [];
|
|
44
|
+
const parseResult = parseAtelier(content);
|
|
45
|
+
if (!parseResult.success) {
|
|
46
|
+
gates.push({
|
|
47
|
+
gate: "^valid-document",
|
|
48
|
+
pass: false,
|
|
49
|
+
errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`)
|
|
50
|
+
});
|
|
51
|
+
return { file: absPath, valid: false, gates };
|
|
52
|
+
}
|
|
53
|
+
gates.push({ gate: "^valid-document", pass: true, errors: [] });
|
|
54
|
+
const doc = parseResult.data;
|
|
55
|
+
const deltaErrors = [];
|
|
56
|
+
for (const [stateName, state] of Object.entries(doc.states)) {
|
|
57
|
+
const overlaps = validateAllDeltas(state.deltas);
|
|
58
|
+
for (const overlap of overlaps) {
|
|
59
|
+
deltaErrors.push(`State "${stateName}": ${overlap.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
gates.push({
|
|
63
|
+
gate: "^valid-delta",
|
|
64
|
+
pass: deltaErrors.length === 0,
|
|
65
|
+
errors: deltaErrors
|
|
66
|
+
});
|
|
67
|
+
const videoErrors = [];
|
|
68
|
+
for (const layer of doc.layers) {
|
|
69
|
+
if (layer.visual.type !== "video") continue;
|
|
70
|
+
const visual = layer.visual;
|
|
71
|
+
const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;
|
|
72
|
+
const result = validateVideoLayer(visual, duration);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
for (const err of result.errors) {
|
|
75
|
+
videoErrors.push(`Layer "${layer.id}" (${err.path}): ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
gates.push({
|
|
80
|
+
gate: "^valid-video-layer",
|
|
81
|
+
pass: videoErrors.length === 0,
|
|
82
|
+
errors: videoErrors
|
|
83
|
+
});
|
|
84
|
+
const valid = gates.every((g) => g.pass);
|
|
85
|
+
return { file: absPath, valid, gates };
|
|
86
|
+
}
|
|
87
|
+
function formatResult(result) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const status = result.valid ? "PASS" : "FAIL";
|
|
90
|
+
lines.push(`${status} ${result.file}`);
|
|
91
|
+
for (const gate of result.gates) {
|
|
92
|
+
const gateStatus = gate.pass ? " \u2713" : " \u2717";
|
|
93
|
+
lines.push(`${gateStatus} ${gate.gate}`);
|
|
94
|
+
for (const err of gate.errors) {
|
|
95
|
+
lines.push(` ${err}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
function lintCommand(program2) {
|
|
101
|
+
program2.command("lint <files...>").description(
|
|
102
|
+
"Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)"
|
|
103
|
+
).option("--json", "Output results as JSON array").action((files, opts) => {
|
|
104
|
+
const results = files.map(lintFile);
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
console.log(JSON.stringify(results, null, 2));
|
|
107
|
+
} else {
|
|
108
|
+
for (const result of results) {
|
|
109
|
+
console.log(formatResult(result));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const allValid = results.every((r) => r.valid);
|
|
113
|
+
if (!allValid) process.exit(1);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
18
117
|
// src/commands/studio.ts
|
|
19
|
-
import { resolve, join, relative, dirname } from "path";
|
|
20
|
-
import { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, realpathSync, symlinkSync, existsSync } from "fs";
|
|
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";
|
|
21
120
|
import { tmpdir } from "os";
|
|
22
121
|
import { randomBytes } from "crypto";
|
|
23
122
|
import { exec } from "child_process";
|
|
@@ -48,7 +147,7 @@ function findAtelierFiles(dir, base = dir) {
|
|
|
48
147
|
}
|
|
49
148
|
function isSafePath(filePath) {
|
|
50
149
|
if (!filePath || filePath.includes("..") || filePath.startsWith("/")) return false;
|
|
51
|
-
const resolved =
|
|
150
|
+
const resolved = resolve2(process.cwd(), filePath);
|
|
52
151
|
return resolved.startsWith(process.cwd());
|
|
53
152
|
}
|
|
54
153
|
function getInlineHTML() {
|
|
@@ -502,7 +601,7 @@ function studioCommand(program2) {
|
|
|
502
601
|
process.exit(1);
|
|
503
602
|
}
|
|
504
603
|
const cwd = process.cwd();
|
|
505
|
-
const cliPackageDir =
|
|
604
|
+
const cliPackageDir = resolve2(dirname(new URL(import.meta.url).pathname), "..");
|
|
506
605
|
const tmpId = randomBytes(4).toString("hex");
|
|
507
606
|
const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);
|
|
508
607
|
mkdirSync(tmpDirRaw, { recursive: true });
|
|
@@ -563,10 +662,10 @@ function studioCommand(program2) {
|
|
|
563
662
|
res.end("Invalid path");
|
|
564
663
|
return;
|
|
565
664
|
}
|
|
566
|
-
const absPath =
|
|
665
|
+
const absPath = resolve2(cwd, filePath);
|
|
567
666
|
if (req.method === "GET") {
|
|
568
667
|
try {
|
|
569
|
-
const content =
|
|
668
|
+
const content = readFileSync2(absPath, "utf-8");
|
|
570
669
|
res.setHeader("Content-Type", "text/plain");
|
|
571
670
|
res.end(content);
|
|
572
671
|
} catch {
|
|
@@ -599,7 +698,7 @@ function studioCommand(program2) {
|
|
|
599
698
|
res.end("Invalid path");
|
|
600
699
|
return;
|
|
601
700
|
}
|
|
602
|
-
const absPath =
|
|
701
|
+
const absPath = resolve2(cwd, filePath);
|
|
603
702
|
const chunks = [];
|
|
604
703
|
req.on("data", (chunk) => {
|
|
605
704
|
chunks.push(chunk);
|
|
@@ -661,6 +760,7 @@ function studioCommand(program2) {
|
|
|
661
760
|
var program = new Command();
|
|
662
761
|
program.name("atelier").description("Atelier animation CLI").version(createRequire(import.meta.url)("../package.json").version);
|
|
663
762
|
validateCommand(program);
|
|
763
|
+
lintCommand(program);
|
|
664
764
|
infoCommand(program);
|
|
665
765
|
stillCommand(program);
|
|
666
766
|
renderCommand(program);
|
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>◆</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\">✓ 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 = \"● 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 = \"✓ 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 { 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);\nlintCommand(program);\ninfoCommand(program);\nstillCommand(program);\nrenderCommand(program);\nexportSvgCommand(program);\nexportLottieCommand(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 } 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>◆</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\">✓ 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 = \"● 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 = \"✓ 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;;;ACJxB,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAyBjB,SAAS,SAAS,UAA8B;AACrD,QAAM,UAAU,QAAQ,QAAQ;AAEhC,MAAI;AACJ,MAAI;AACF,cAAU,aAAa,SAAS,OAAO;AAAA,EACzC,QAAQ;AACN,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,UACN,QAAQ,CAAC,qBAAqB,OAAO,EAAE;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAsB,CAAC;AAG7B,QAAM,cAAc,aAAa,OAAO;AACxC,MAAI,CAAC,YAAY,SAAS;AACxB,UAAM,KAAK;AAAA,MACT,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ,YAAY,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE;AAAA,IACjE,CAAC;AAED,WAAO,EAAE,MAAM,SAAS,OAAO,OAAO,MAAM;AAAA,EAC9C;AAEA,QAAM,KAAK,EAAE,MAAM,mBAAmB,MAAM,MAAM,QAAQ,CAAC,EAAE,CAAC;AAC9D,QAAM,MAAM,YAAY;AAGxB,QAAM,cAAwB,CAAC;AAC/B,aAAW,CAAC,WAAW,KAAK,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAC3D,UAAM,WAAW,kBAAkB,MAAM,MAAM;AAC/C,eAAW,WAAW,UAAU;AAC9B,kBAAY,KAAK,UAAU,SAAS,MAAM,QAAQ,OAAO,EAAE;AAAA,IAC7D;AAAA,EACF;AACA,QAAM,KAAK;AAAA,IACT,MAAM;AAAA,IACN,MAAM,YAAY,WAAW;AAAA,IAC7B,QAAQ;AAAA,EACV,CAAC;AAGD,QAAM,cAAwB,CAAC;AAC/B,aAAW,SAAS,IAAI,QAAQ;AAC9B,QAAI,MAAM,OAAO,SAAS,QAAS;AACnC,UAAM,SAAS,MAAM;AACrB,UAAM,WAAW,IAAI,SAAS,OAAO,OAAO,GAAG,WAAW;AAC1D,UAAM,SAAS,mBAAmB,QAAQ,QAAQ;AAClD,QAAI,CAAC,OAAO,SAAS;AACnB,iBAAW,OAAO,OAAO,QAAQ;AAC/B,oBAAY,KAAK,UAAU,MAAM,EAAE,MAAM,IAAI,IAAI,MAAM,IAAI,OAAO,EAAE;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK;AAAA,IACT,MAAM;AAAA,IACN,MAAM,YAAY,WAAW;AAAA,IAC7B,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI;AACvC,SAAO,EAAE,MAAM,SAAS,OAAO,MAAM;AACvC;AAGA,SAAS,aAAa,QAA4B;AAChD,QAAM,QAAkB,CAAC;AACzB,QAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,QAAM,KAAK,GAAG,MAAM,KAAK,OAAO,IAAI,EAAE;AAEtC,aAAW,QAAQ,OAAO,OAAO;AAC/B,UAAM,aAAa,KAAK,OAAO,aAAQ;AACvC,UAAM,KAAK,GAAG,UAAU,IAAI,KAAK,IAAI,EAAE;AACvC,eAAW,OAAO,KAAK,QAAQ;AAC7B,YAAM,KAAK,UAAU,GAAG,EAAE;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,YAAYA,UAAwB;AAClD,EAAAA,SACG,QAAQ,iBAAiB,EACzB;AAAA,IACC;AAAA,EACF,EACC,OAAO,UAAU,8BAA8B,EAC/C,OAAO,CAAC,OAAiB,SAA6B;AACrD,UAAM,UAAU,MAAM,IAAI,QAAQ;AAElC,QAAI,KAAK,MAAM;AACb,cAAQ,IAAI,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,IAC9C,OAAO;AACL,iBAAW,UAAU,SAAS;AAC5B,gBAAQ,IAAI,aAAa,MAAM,CAAC;AAAA,MAClC;AAAA,IACF;AAEA,UAAM,WAAW,QAAQ,MAAM,CAAC,MAAM,EAAE,KAAK;AAC7C,QAAI,CAAC,SAAU,SAAQ,KAAK,CAAC;AAAA,EAC/B,CAAC;AACL;;;AC/HA,SAAS,WAAAC,UAAS,MAAM,UAAU,eAAe;AACjD,SAAS,WAAW,eAAe,QAAQ,gBAAAC,eAAc,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,WAAWD,SAAQ,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;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;A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","names":["program","resolve","readFileSync","program","server","url","atelierFiles"]}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
renderRef,
|
|
10
10
|
renderShape,
|
|
11
11
|
renderText
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
25
|
+
//# sourceMappingURL=dist-M67UZGFQ.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -420,7 +420,18 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
-
|
|
423
|
+
const resolvedLayer = { id: layer.id, layer, computedProperties };
|
|
424
|
+
if (layer.visual.type === "video") {
|
|
425
|
+
const video = layer.visual;
|
|
426
|
+
const fps = doc.canvas.fps;
|
|
427
|
+
const startFrame = video.startFrame ?? 0;
|
|
428
|
+
const sourceOffset = video.sourceOffset ?? 0;
|
|
429
|
+
const playbackRate = video.playbackRate ?? 1;
|
|
430
|
+
const relativeFrame = Math.max(0, frame - startFrame);
|
|
431
|
+
const sourceTime = relativeFrame / fps * playbackRate + sourceOffset;
|
|
432
|
+
resolvedLayer.videoSourceTime = video.sourceEnd !== void 0 ? Math.min(sourceTime, video.sourceEnd) : sourceTime;
|
|
433
|
+
}
|
|
434
|
+
return resolvedLayer;
|
|
424
435
|
});
|
|
425
436
|
return { frame, stateName, layers: resolvedLayers };
|
|
426
437
|
}
|
|
@@ -1027,6 +1038,14 @@ function renderImage(ctx, eff, imageCache) {
|
|
|
1027
1038
|
}
|
|
1028
1039
|
ctx.drawImage(img, 0, 0, eff.width, eff.height);
|
|
1029
1040
|
}
|
|
1041
|
+
function renderVideo(ctx, eff, sourceTime, provider) {
|
|
1042
|
+
const visual = eff.visual;
|
|
1043
|
+
const src = visual.src;
|
|
1044
|
+
if (!src) return;
|
|
1045
|
+
const frame = provider(src, sourceTime, eff.width, eff.height);
|
|
1046
|
+
if (!frame) return;
|
|
1047
|
+
ctx.drawImage(frame, 0, 0, eff.width, eff.height);
|
|
1048
|
+
}
|
|
1030
1049
|
function renderRef(ctx, eff, opts, _parentDoc) {
|
|
1031
1050
|
const visual = eff.visual;
|
|
1032
1051
|
const resolver = opts?.documentResolver;
|
|
@@ -1124,6 +1143,7 @@ function renderPlaceholder(ctx, eff, label) {
|
|
|
1124
1143
|
function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
1125
1144
|
let imageCache;
|
|
1126
1145
|
let documentResolver;
|
|
1146
|
+
let videoFrameProvider;
|
|
1127
1147
|
let maxRefDepth = 4;
|
|
1128
1148
|
if (optsOrCache && typeof optsOrCache.get === "function") {
|
|
1129
1149
|
imageCache = optsOrCache;
|
|
@@ -1131,6 +1151,7 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1131
1151
|
const opts = optsOrCache;
|
|
1132
1152
|
imageCache = opts.imageCache;
|
|
1133
1153
|
documentResolver = opts.documentResolver;
|
|
1154
|
+
videoFrameProvider = opts.videoFrameProvider;
|
|
1134
1155
|
maxRefDepth = opts.maxRefDepth ?? 4;
|
|
1135
1156
|
}
|
|
1136
1157
|
const { width, height } = doc.canvas;
|
|
@@ -1138,10 +1159,14 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1138
1159
|
ctx.fillRect(0, 0, width, height);
|
|
1139
1160
|
const effMap = /* @__PURE__ */ new Map();
|
|
1140
1161
|
const effList = [];
|
|
1162
|
+
const videoSourceTimeMap = /* @__PURE__ */ new Map();
|
|
1141
1163
|
for (const resolvedLayer of resolvedFrame.layers) {
|
|
1142
1164
|
const eff = buildEffectiveLayer(resolvedLayer, width, height);
|
|
1143
1165
|
effMap.set(resolvedLayer.layer.id, eff);
|
|
1144
1166
|
effList.push(eff);
|
|
1167
|
+
if (resolvedLayer.videoSourceTime !== void 0) {
|
|
1168
|
+
videoSourceTimeMap.set(resolvedLayer.layer.id, resolvedLayer.videoSourceTime);
|
|
1169
|
+
}
|
|
1145
1170
|
}
|
|
1146
1171
|
for (const eff of effList) {
|
|
1147
1172
|
const { layer } = eff;
|
|
@@ -1153,6 +1178,12 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1153
1178
|
iv.src = doc.assets[iv.assetId].src;
|
|
1154
1179
|
}
|
|
1155
1180
|
}
|
|
1181
|
+
if (layer.visual.type === "video") {
|
|
1182
|
+
const vv = eff.visual;
|
|
1183
|
+
if (!vv.src && vv.assetId && doc.assets?.[vv.assetId]) {
|
|
1184
|
+
vv.src = doc.assets[vv.assetId].src;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1156
1187
|
ctx.save();
|
|
1157
1188
|
applyAncestorTransforms(ctx, layer.id, effMap, doc);
|
|
1158
1189
|
ctx.globalAlpha = eff.opacity;
|
|
@@ -1200,6 +1231,11 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1200
1231
|
case "image":
|
|
1201
1232
|
if (imageCache) renderImage(renderCtx, eff, imageCache);
|
|
1202
1233
|
break;
|
|
1234
|
+
case "video":
|
|
1235
|
+
if (videoFrameProvider) {
|
|
1236
|
+
renderVideo(renderCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1203
1239
|
case "group":
|
|
1204
1240
|
break;
|
|
1205
1241
|
case "ref":
|
|
@@ -1225,6 +1261,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1225
1261
|
case "image":
|
|
1226
1262
|
if (imageCache) renderImage(offCtx, eff, imageCache);
|
|
1227
1263
|
break;
|
|
1264
|
+
case "video":
|
|
1265
|
+
if (videoFrameProvider) renderVideo(offCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1266
|
+
break;
|
|
1228
1267
|
case "ref":
|
|
1229
1268
|
renderRef(offCtx, eff, refOpts, doc);
|
|
1230
1269
|
break;
|
|
@@ -1240,6 +1279,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1240
1279
|
case "image":
|
|
1241
1280
|
if (imageCache) renderImage(ctx, eff, imageCache);
|
|
1242
1281
|
break;
|
|
1282
|
+
case "video":
|
|
1283
|
+
if (videoFrameProvider) renderVideo(ctx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1284
|
+
break;
|
|
1243
1285
|
case "ref":
|
|
1244
1286
|
renderRef(ctx, eff, refOpts, doc);
|
|
1245
1287
|
break;
|
|
@@ -1660,6 +1702,18 @@ var ImageVisualSchema = import_zod7.z.object({
|
|
|
1660
1702
|
spritesheet: SpritesheetConfigSchema.optional(),
|
|
1661
1703
|
frameIndex: import_zod7.z.number().int().min(0).optional()
|
|
1662
1704
|
});
|
|
1705
|
+
var VideoVisualSchema = import_zod7.z.object({
|
|
1706
|
+
type: import_zod7.z.literal("video"),
|
|
1707
|
+
assetId: import_zod7.z.string().min(1, "assetId is required"),
|
|
1708
|
+
src: import_zod7.z.string().optional(),
|
|
1709
|
+
startFrame: import_zod7.z.number().int().min(0).optional(),
|
|
1710
|
+
sourceOffset: import_zod7.z.number().min(0).optional(),
|
|
1711
|
+
sourceEnd: import_zod7.z.number().positive().optional(),
|
|
1712
|
+
playbackRate: import_zod7.z.number().positive().optional(),
|
|
1713
|
+
volume: import_zod7.z.number().min(0).max(1).optional(),
|
|
1714
|
+
muted: import_zod7.z.boolean().optional(),
|
|
1715
|
+
objectFit: import_zod7.z.enum(["contain", "cover", "fill"]).optional()
|
|
1716
|
+
});
|
|
1663
1717
|
var GroupVisualSchema = import_zod7.z.object({
|
|
1664
1718
|
type: import_zod7.z.literal("group")
|
|
1665
1719
|
});
|
|
@@ -1673,6 +1727,7 @@ var VisualSchema = import_zod7.z.discriminatedUnion("type", [
|
|
|
1673
1727
|
ShapeVisualSchema,
|
|
1674
1728
|
TextVisualSchema,
|
|
1675
1729
|
ImageVisualSchema,
|
|
1730
|
+
VideoVisualSchema,
|
|
1676
1731
|
GroupVisualSchema,
|
|
1677
1732
|
RefVisualSchema
|
|
1678
1733
|
]);
|
|
@@ -1792,7 +1847,7 @@ var VariableSchema = import_zod12.z.object({
|
|
|
1792
1847
|
default: import_zod12.z.unknown().optional(),
|
|
1793
1848
|
description: import_zod12.z.string().optional()
|
|
1794
1849
|
});
|
|
1795
|
-
var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio"]);
|
|
1850
|
+
var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio", "video"]);
|
|
1796
1851
|
var AssetSchema = import_zod13.z.object({
|
|
1797
1852
|
type: AssetTypeSchema,
|
|
1798
1853
|
src: import_zod13.z.string().min(1, "Asset src is required"),
|
|
@@ -1803,6 +1858,12 @@ var AssetSchema = import_zod13.z.object({
|
|
|
1803
1858
|
frameCount: import_zod13.z.number().int().positive().optional(),
|
|
1804
1859
|
frameWidth: import_zod13.z.number().positive(),
|
|
1805
1860
|
frameHeight: import_zod13.z.number().positive()
|
|
1861
|
+
}).optional(),
|
|
1862
|
+
videoMeta: import_zod13.z.object({
|
|
1863
|
+
duration: import_zod13.z.number().positive("videoMeta.duration must be positive"),
|
|
1864
|
+
fps: import_zod13.z.number().positive("videoMeta.fps must be positive"),
|
|
1865
|
+
width: import_zod13.z.number().int().positive(),
|
|
1866
|
+
height: import_zod13.z.number().int().positive()
|
|
1806
1867
|
}).optional()
|
|
1807
1868
|
});
|
|
1808
1869
|
var CanvasSchema = import_zod14.z.object({
|