@a-company/atelier 0.28.2 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5QQESXI6.js +4432 -0
- package/dist/chunk-5QQESXI6.js.map +1 -0
- package/dist/{chunk-C5DBTHXB.js → chunk-JPZ4F4PW.js} +44 -2
- package/dist/chunk-JPZ4F4PW.js.map +1 -0
- package/dist/cli.cjs +2577 -529
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +405 -433
- package/dist/cli.js.map +1 -1
- package/dist/{dist-6IHF7WA7.js → dist-M67UZGFQ.js} +2 -2
- package/dist/index.cjs +2296 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -2
- package/dist/index.d.ts +584 -2
- package/dist/index.js +112 -4
- package/dist/mcp.cjs +1247 -367
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1241 -367
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -9
- package/src/web/inline-app.ts +867 -0
- package/src/web/tsconfig.json +9 -0
- package/templates/welcome.atelier +67 -0
- package/dist/chunk-C5DBTHXB.js.map +0 -1
- package/dist/chunk-LC7ICNMN.js +0 -2242
- 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
|
@@ -1,26 +1,142 @@
|
|
|
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,
|
|
11
|
+
parseAtelier,
|
|
12
|
+
recipeCommand,
|
|
7
13
|
renderCommand,
|
|
14
|
+
serializeAtelier,
|
|
8
15
|
stillCommand,
|
|
16
|
+
transcribeCommand,
|
|
17
|
+
transcriptCommand,
|
|
18
|
+
trimCommand,
|
|
9
19
|
validateCommand,
|
|
20
|
+
validateVideoLayer,
|
|
10
21
|
variablesCommand
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import
|
|
22
|
+
} from "./chunk-5QQESXI6.js";
|
|
23
|
+
import {
|
|
24
|
+
validateAllDeltas
|
|
25
|
+
} from "./chunk-JPZ4F4PW.js";
|
|
13
26
|
|
|
14
27
|
// src/cli.ts
|
|
15
28
|
import { createRequire } from "module";
|
|
16
29
|
import { Command } from "commander";
|
|
17
30
|
|
|
31
|
+
// src/commands/lint.ts
|
|
32
|
+
import { readFileSync } from "fs";
|
|
33
|
+
import { resolve } from "path";
|
|
34
|
+
function lintFile(filePath) {
|
|
35
|
+
const absPath = resolve(filePath);
|
|
36
|
+
let content;
|
|
37
|
+
try {
|
|
38
|
+
content = readFileSync(absPath, "utf-8");
|
|
39
|
+
} catch {
|
|
40
|
+
return {
|
|
41
|
+
file: absPath,
|
|
42
|
+
valid: false,
|
|
43
|
+
gates: [
|
|
44
|
+
{
|
|
45
|
+
gate: "^valid-document",
|
|
46
|
+
pass: false,
|
|
47
|
+
errors: [`Cannot read file: ${absPath}`]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const gates = [];
|
|
53
|
+
const parseResult = parseAtelier(content);
|
|
54
|
+
if (!parseResult.success) {
|
|
55
|
+
gates.push({
|
|
56
|
+
gate: "^valid-document",
|
|
57
|
+
pass: false,
|
|
58
|
+
errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`)
|
|
59
|
+
});
|
|
60
|
+
return { file: absPath, valid: false, gates };
|
|
61
|
+
}
|
|
62
|
+
gates.push({ gate: "^valid-document", pass: true, errors: [] });
|
|
63
|
+
const doc = parseResult.data;
|
|
64
|
+
const deltaErrors = [];
|
|
65
|
+
for (const [stateName, state] of Object.entries(doc.states)) {
|
|
66
|
+
const overlaps = validateAllDeltas(state.deltas);
|
|
67
|
+
for (const overlap of overlaps) {
|
|
68
|
+
deltaErrors.push(`State "${stateName}": ${overlap.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
gates.push({
|
|
72
|
+
gate: "^valid-delta",
|
|
73
|
+
pass: deltaErrors.length === 0,
|
|
74
|
+
errors: deltaErrors
|
|
75
|
+
});
|
|
76
|
+
const videoErrors = [];
|
|
77
|
+
for (const layer of doc.layers) {
|
|
78
|
+
if (layer.visual.type !== "video") continue;
|
|
79
|
+
const visual = layer.visual;
|
|
80
|
+
const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;
|
|
81
|
+
const result = validateVideoLayer(visual, duration);
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
for (const err of result.errors) {
|
|
84
|
+
videoErrors.push(`Layer "${layer.id}" (${err.path}): ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
gates.push({
|
|
89
|
+
gate: "^valid-video-layer",
|
|
90
|
+
pass: videoErrors.length === 0,
|
|
91
|
+
errors: videoErrors
|
|
92
|
+
});
|
|
93
|
+
const valid = gates.every((g) => g.pass);
|
|
94
|
+
return { file: absPath, valid, gates };
|
|
95
|
+
}
|
|
96
|
+
function formatResult(result) {
|
|
97
|
+
const lines = [];
|
|
98
|
+
const status = result.valid ? "PASS" : "FAIL";
|
|
99
|
+
lines.push(`${status} ${result.file}`);
|
|
100
|
+
for (const gate of result.gates) {
|
|
101
|
+
const gateStatus = gate.pass ? " \u2713" : " \u2717";
|
|
102
|
+
lines.push(`${gateStatus} ${gate.gate}`);
|
|
103
|
+
for (const err of gate.errors) {
|
|
104
|
+
lines.push(` ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
function lintCommand(program2) {
|
|
110
|
+
program2.command("lint <files...>").description(
|
|
111
|
+
"Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)"
|
|
112
|
+
).option("--json", "Output results as JSON array").action((files, opts) => {
|
|
113
|
+
const results = files.map(lintFile);
|
|
114
|
+
if (opts.json) {
|
|
115
|
+
console.log(JSON.stringify(results, null, 2));
|
|
116
|
+
} else {
|
|
117
|
+
for (const result of results) {
|
|
118
|
+
console.log(formatResult(result));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const allValid = results.every((r) => r.valid);
|
|
122
|
+
if (!allValid) process.exit(1);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
18
126
|
// 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";
|
|
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";
|
|
21
129
|
import { tmpdir } from "os";
|
|
22
130
|
import { randomBytes } from "crypto";
|
|
23
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";
|
|
24
140
|
function findAtelierFiles(dir, base = dir) {
|
|
25
141
|
const results = [];
|
|
26
142
|
let entries;
|
|
@@ -47,9 +163,119 @@ function findAtelierFiles(dir, base = dir) {
|
|
|
47
163
|
return results.sort();
|
|
48
164
|
}
|
|
49
165
|
function isSafePath(filePath) {
|
|
50
|
-
if (!filePath || filePath.
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
+
}
|
|
53
279
|
}
|
|
54
280
|
function getInlineHTML() {
|
|
55
281
|
return `<!DOCTYPE html>
|
|
@@ -68,431 +294,59 @@ function getInlineHTML() {
|
|
|
68
294
|
</body>
|
|
69
295
|
</html>`;
|
|
70
296
|
}
|
|
71
|
-
function getInlineApp(initialFile) {
|
|
297
|
+
function getInlineApp(initialFile, cliPackageDir) {
|
|
72
298
|
const initialFileStr = initialFile ? JSON.stringify(initialFile) : "null";
|
|
73
|
-
|
|
74
|
-
import
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// \u2500\u2500 Types \u2500\u2500
|
|
78
|
-
interface FileEntry {
|
|
79
|
-
path: string;
|
|
80
|
-
name: string;
|
|
81
|
-
folder: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// \u2500\u2500 State \u2500\u2500
|
|
85
|
-
let studio: AtelierStudio | null = null;
|
|
86
|
-
let currentFile: string | null = null;
|
|
87
|
-
let files: FileEntry[] = [];
|
|
88
|
-
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
89
|
-
|
|
90
|
-
// \u2500\u2500 API helpers \u2500\u2500
|
|
91
|
-
async function fetchFiles(): Promise<FileEntry[]> {
|
|
92
|
-
const res = await fetch("/api/files");
|
|
93
|
-
return res.json();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function fetchFileContent(path: string): Promise<string> {
|
|
97
|
-
const res = await fetch("/api/file?path=" + encodeURIComponent(path));
|
|
98
|
-
return res.text();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function saveFileContent(path: string, content: string): Promise<void> {
|
|
102
|
-
await fetch("/api/file?path=" + encodeURIComponent(path), {
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers: { "Content-Type": "text/plain" },
|
|
105
|
-
body: content,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function saveExportBlob(path: string, blob: Blob): Promise<void> {
|
|
110
|
-
const buf = await blob.arrayBuffer();
|
|
111
|
-
await fetch("/api/export?path=" + encodeURIComponent(path), {
|
|
112
|
-
method: "POST",
|
|
113
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
114
|
-
body: buf,
|
|
115
|
-
});
|
|
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
|
+
`;
|
|
116
303
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
overlay.appendChild(card);
|
|
127
|
-
document.body.appendChild(overlay);
|
|
128
|
-
|
|
129
|
-
const title = document.createElement("div");
|
|
130
|
-
title.style.cssText = "font-size:18px;margin-bottom:16px;font-weight:600";
|
|
131
|
-
title.textContent = "Exporting All Files\u2026";
|
|
132
|
-
card.appendChild(title);
|
|
133
|
-
|
|
134
|
-
const fileLabel = document.createElement("div");
|
|
135
|
-
fileLabel.style.cssText = "font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace";
|
|
136
|
-
card.appendChild(fileLabel);
|
|
137
|
-
|
|
138
|
-
const progress = document.createElement("progress");
|
|
139
|
-
progress.style.cssText = "width:100%;height:6px;appearance:none;-webkit-appearance:none";
|
|
140
|
-
progress.max = files.length;
|
|
141
|
-
progress.value = 0;
|
|
142
|
-
card.appendChild(progress);
|
|
143
|
-
|
|
144
|
-
const statusText = document.createElement("div");
|
|
145
|
-
statusText.style.cssText = "font-size:12px;color:#A89F95;margin-top:8px";
|
|
146
|
-
card.appendChild(statusText);
|
|
147
|
-
|
|
148
|
-
let exported = 0;
|
|
149
|
-
let errors = 0;
|
|
150
|
-
|
|
151
|
-
for (const file of files) {
|
|
152
|
-
fileLabel.textContent = file.path;
|
|
153
|
-
statusText.textContent = (exported + errors + 1) + " / " + files.length;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const content = await fetchFileContent(file.path);
|
|
157
|
-
const result = parseAtelier(content);
|
|
158
|
-
if (!result.success) {
|
|
159
|
-
errors++;
|
|
160
|
-
progress.value = exported + errors;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const doc = result.data;
|
|
165
|
-
const w = doc.canvas.width;
|
|
166
|
-
const h = doc.canvas.height;
|
|
167
|
-
const canvas = document.createElement("canvas");
|
|
168
|
-
canvas.width = w;
|
|
169
|
-
canvas.height = h;
|
|
170
|
-
const imageCache = new ImageCache();
|
|
171
|
-
|
|
172
|
-
const exportResult = await exportDocument(doc, canvas, imageCache, {
|
|
173
|
-
format,
|
|
174
|
-
onProgress: ({ percent }) => {
|
|
175
|
-
statusText.textContent = (exported + errors + 1) + " / " + files.length + " \u2014 " + percent + "%";
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Save alongside the source file: e.g. "dir/my-anim.atelier" \u2192 "dir/my-anim.gif"
|
|
180
|
-
const outPath = file.path.replace(/\\.atelier$/, "." + format);
|
|
181
|
-
await saveExportBlob(outPath, exportResult.blob);
|
|
182
|
-
exported++;
|
|
183
|
-
} catch (e) {
|
|
184
|
-
console.error("Export failed:", file.path, e);
|
|
185
|
-
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;
|
|
186
313
|
}
|
|
187
|
-
progress.value = exported + errors;
|
|
188
314
|
}
|
|
189
|
-
|
|
190
|
-
// Done
|
|
191
|
-
title.textContent = "Export Complete";
|
|
192
|
-
fileLabel.textContent = "";
|
|
193
|
-
statusText.textContent = exported + " exported" + (errors > 0 ? ", " + errors + " failed" : "");
|
|
194
|
-
if (errors > 0) console.warn("Export All finished with " + errors + " error(s). Check console for details.");
|
|
195
|
-
|
|
196
|
-
const closeBtn = document.createElement("button");
|
|
197
|
-
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";
|
|
198
|
-
closeBtn.textContent = "Close";
|
|
199
|
-
closeBtn.addEventListener("click", () => document.body.removeChild(overlay));
|
|
200
|
-
card.appendChild(closeBtn);
|
|
315
|
+
return candidates[0];
|
|
201
316
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
buttonActive: "#555555",
|
|
215
|
-
accent: "#C75B39",
|
|
216
|
-
accentHover: "#D4724E",
|
|
217
|
-
sliderTrack: "#4A4A4A",
|
|
218
|
-
sliderThumb: "#C75B39",
|
|
219
|
-
fontFamily: "'Cormorant Garamond', Georgia, serif",
|
|
220
|
-
fontMono: "'SF Mono', 'Fira Code', monospace",
|
|
221
|
-
canvasShadow: "0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)",
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// \u2500\u2500 Styles \u2500\u2500
|
|
225
|
-
const style = document.createElement("style");
|
|
226
|
-
style.textContent = \`
|
|
227
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
228
|
-
html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }
|
|
229
|
-
body { font-family: 'Cormorant Garamond', Georgia, serif; }
|
|
230
|
-
#studio { display: flex; height: 100vh; width: 100vw; }
|
|
231
|
-
|
|
232
|
-
.sidebar {
|
|
233
|
-
width: 260px;
|
|
234
|
-
min-width: 260px;
|
|
235
|
-
background: #333333;
|
|
236
|
-
border-right: 1px solid #4A4A4A;
|
|
237
|
-
display: flex;
|
|
238
|
-
flex-direction: column;
|
|
239
|
-
overflow: hidden;
|
|
240
|
-
}
|
|
241
|
-
.sidebar__header {
|
|
242
|
-
padding: 16px 20px;
|
|
243
|
-
border-bottom: 1px solid #4A4A4A;
|
|
244
|
-
font-size: 11px;
|
|
245
|
-
font-weight: 600;
|
|
246
|
-
letter-spacing: 2px;
|
|
247
|
-
text-transform: uppercase;
|
|
248
|
-
color: #A89F95;
|
|
249
|
-
display: flex;
|
|
250
|
-
align-items: center;
|
|
251
|
-
gap: 8px;
|
|
252
|
-
}
|
|
253
|
-
.sidebar__header span {
|
|
254
|
-
color: #C75B39;
|
|
255
|
-
font-size: 13px;
|
|
256
|
-
}
|
|
257
|
-
.sidebar__list {
|
|
258
|
-
flex: 1;
|
|
259
|
-
overflow-y: auto;
|
|
260
|
-
padding: 8px 0;
|
|
261
|
-
}
|
|
262
|
-
.sidebar__list::-webkit-scrollbar { width: 6px; }
|
|
263
|
-
.sidebar__list::-webkit-scrollbar-track { background: transparent; }
|
|
264
|
-
.sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }
|
|
265
|
-
|
|
266
|
-
.sidebar__folder {
|
|
267
|
-
padding: 10px 20px 4px;
|
|
268
|
-
font-size: 10px;
|
|
269
|
-
font-weight: 600;
|
|
270
|
-
letter-spacing: 1.5px;
|
|
271
|
-
text-transform: uppercase;
|
|
272
|
-
color: #A89F95;
|
|
273
|
-
}
|
|
274
|
-
.sidebar__item {
|
|
275
|
-
padding: 8px 20px 8px 28px;
|
|
276
|
-
font-size: 13px;
|
|
277
|
-
cursor: pointer;
|
|
278
|
-
color: #A89F95;
|
|
279
|
-
transition: background 0.15s, color 0.15s;
|
|
280
|
-
white-space: nowrap;
|
|
281
|
-
overflow: hidden;
|
|
282
|
-
text-overflow: ellipsis;
|
|
283
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
284
|
-
font-size: 11.5px;
|
|
285
|
-
}
|
|
286
|
-
.sidebar__item:hover { background: #363636; color: #F5F0EB; }
|
|
287
|
-
.sidebar__item--active {
|
|
288
|
-
background: rgba(199, 91, 57, 0.12) !important;
|
|
289
|
-
color: #C75B39 !important;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
.main {
|
|
293
|
-
flex: 1;
|
|
294
|
-
display: flex;
|
|
295
|
-
flex-direction: column;
|
|
296
|
-
overflow: hidden;
|
|
297
|
-
}
|
|
298
|
-
.main__status {
|
|
299
|
-
height: 32px;
|
|
300
|
-
min-height: 32px;
|
|
301
|
-
display: flex;
|
|
302
|
-
align-items: center;
|
|
303
|
-
padding: 0 16px;
|
|
304
|
-
background: #333333;
|
|
305
|
-
border-bottom: 1px solid #4A4A4A;
|
|
306
|
-
font-size: 11px;
|
|
307
|
-
color: #A89F95;
|
|
308
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
309
|
-
gap: 12px;
|
|
310
|
-
}
|
|
311
|
-
.main__status .save-indicator {
|
|
312
|
-
display: inline-flex;
|
|
313
|
-
align-items: center;
|
|
314
|
-
gap: 4px;
|
|
315
|
-
margin-left: auto;
|
|
316
|
-
transition: opacity 0.3s;
|
|
317
|
-
}
|
|
318
|
-
.main__status .save-indicator--saving { color: #C75B39; }
|
|
319
|
-
.main__status .save-indicator--saved { color: #6B8E6B; }
|
|
320
|
-
.main__editor {
|
|
321
|
-
flex: 1;
|
|
322
|
-
overflow: hidden;
|
|
323
|
-
}
|
|
324
|
-
.main__empty {
|
|
325
|
-
flex: 1;
|
|
326
|
-
display: flex;
|
|
327
|
-
align-items: center;
|
|
328
|
-
justify-content: center;
|
|
329
|
-
color: #A89F95;
|
|
330
|
-
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;
|
|
331
329
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const sidebarHeader = document.createElement("div");
|
|
341
|
-
sidebarHeader.className = "sidebar__header";
|
|
342
|
-
sidebarHeader.innerHTML = '<span>◆</span> ATELIER STUDIO';
|
|
343
|
-
sidebar.appendChild(sidebarHeader);
|
|
344
|
-
|
|
345
|
-
const sidebarList = document.createElement("div");
|
|
346
|
-
sidebarList.className = "sidebar__list";
|
|
347
|
-
sidebar.appendChild(sidebarList);
|
|
348
|
-
|
|
349
|
-
const sidebarFooter = document.createElement("div");
|
|
350
|
-
sidebarFooter.style.cssText = "padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center";
|
|
351
|
-
const exportAllSelect = document.createElement("select");
|
|
352
|
-
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";
|
|
353
|
-
for (const [val, label] of [["gif","GIF"],["mp4","MP4"],["webm","WebM"]] as const) {
|
|
354
|
-
const o = document.createElement("option");
|
|
355
|
-
o.value = val;
|
|
356
|
-
o.textContent = label;
|
|
357
|
-
exportAllSelect.appendChild(o);
|
|
358
|
-
}
|
|
359
|
-
sidebarFooter.appendChild(exportAllSelect);
|
|
360
|
-
const exportAllBtn = document.createElement("button");
|
|
361
|
-
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";
|
|
362
|
-
exportAllBtn.textContent = "Export All";
|
|
363
|
-
exportAllBtn.addEventListener("click", () => {
|
|
364
|
-
exportAll(exportAllSelect.value as "gif" | "mp4" | "webm");
|
|
365
|
-
});
|
|
366
|
-
sidebarFooter.appendChild(exportAllBtn);
|
|
367
|
-
sidebar.appendChild(sidebarFooter);
|
|
368
|
-
|
|
369
|
-
const main = document.createElement("div");
|
|
370
|
-
main.className = "main";
|
|
371
|
-
|
|
372
|
-
const statusBar = document.createElement("div");
|
|
373
|
-
statusBar.className = "main__status";
|
|
374
|
-
main.appendChild(statusBar);
|
|
375
|
-
|
|
376
|
-
const editorContainer = document.createElement("div");
|
|
377
|
-
editorContainer.className = "main__editor";
|
|
378
|
-
main.appendChild(editorContainer);
|
|
379
|
-
|
|
380
|
-
root.appendChild(sidebar);
|
|
381
|
-
root.appendChild(main);
|
|
382
|
-
|
|
383
|
-
// \u2500\u2500 File list rendering \u2500\u2500
|
|
384
|
-
function renderFileList(): void {
|
|
385
|
-
sidebarList.innerHTML = "";
|
|
386
|
-
let lastFolder = "";
|
|
387
|
-
|
|
388
|
-
for (const file of files) {
|
|
389
|
-
if (file.folder && file.folder !== lastFolder) {
|
|
390
|
-
lastFolder = file.folder;
|
|
391
|
-
const folder = document.createElement("div");
|
|
392
|
-
folder.className = "sidebar__folder";
|
|
393
|
-
folder.textContent = file.folder;
|
|
394
|
-
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
|
+
}
|
|
395
338
|
}
|
|
396
|
-
|
|
397
|
-
const item = document.createElement("div");
|
|
398
|
-
item.className = "sidebar__item" + (file.path === currentFile ? " sidebar__item--active" : "");
|
|
399
|
-
item.textContent = file.name;
|
|
400
|
-
item.title = file.path;
|
|
401
|
-
item.addEventListener("click", () => loadFile(file.path));
|
|
402
|
-
sidebarList.appendChild(item);
|
|
403
339
|
}
|
|
340
|
+
return "welcome.atelier";
|
|
404
341
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const content = await fetchFileContent(path);
|
|
412
|
-
const result = parseAtelier(content);
|
|
413
|
-
|
|
414
|
-
if (!result.success) {
|
|
415
|
-
editorContainer.innerHTML = "";
|
|
416
|
-
const err = document.createElement("div");
|
|
417
|
-
err.className = "main__empty";
|
|
418
|
-
err.style.flexDirection = "column";
|
|
419
|
-
err.style.gap = "8px";
|
|
420
|
-
err.innerHTML = '<div style="color:#C75B39">Parse Error</div><div style="font-size:13px;font-family:monospace">' +
|
|
421
|
-
result.errors.map(e => e.path + ": " + e.message).join("<br>") + "</div>";
|
|
422
|
-
editorContainer.appendChild(err);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
statusBar.innerHTML = '<span>' + path + '</span><span class="save-indicator save-indicator--saved">✓ saved</span>';
|
|
427
|
-
|
|
428
|
-
if (studio) {
|
|
429
|
-
studio.destroy();
|
|
430
|
-
studio = null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Set filename for export downloads (strip path and .atelier extension)
|
|
434
|
-
const baseName = path.split("/").pop()?.replace(/\\.atelier$/, "") || null;
|
|
435
|
-
|
|
436
|
-
studio = new AtelierStudio(editorContainer, {
|
|
437
|
-
mode: "full",
|
|
438
|
-
initialTab: "yaml",
|
|
439
|
-
allowSave: true,
|
|
440
|
-
onDocumentChange: (doc) => {
|
|
441
|
-
// Auto-save with debounce
|
|
442
|
-
const indicator = statusBar.querySelector(".save-indicator");
|
|
443
|
-
if (indicator) {
|
|
444
|
-
indicator.className = "save-indicator save-indicator--saving";
|
|
445
|
-
indicator.innerHTML = "● saving...";
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (saveTimeout) clearTimeout(saveTimeout);
|
|
449
|
-
saveTimeout = setTimeout(async () => {
|
|
450
|
-
if (!currentFile) return;
|
|
451
|
-
const yaml = serializeAtelier(doc);
|
|
452
|
-
await saveFileContent(currentFile, yaml);
|
|
453
|
-
const ind = statusBar.querySelector(".save-indicator");
|
|
454
|
-
if (ind) {
|
|
455
|
-
ind.className = "save-indicator save-indicator--saved";
|
|
456
|
-
ind.innerHTML = "✓ saved";
|
|
457
|
-
}
|
|
458
|
-
}, 800);
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
studio.setTheme(theme);
|
|
462
|
-
studio.setFilename(baseName);
|
|
463
|
-
studio.loadDocument(result.data);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// \u2500\u2500 Boot \u2500\u2500
|
|
467
|
-
async function boot(): Promise<void> {
|
|
468
|
-
files = await fetchFiles();
|
|
469
|
-
|
|
470
|
-
if (files.length === 0) {
|
|
471
|
-
editorContainer.innerHTML = "";
|
|
472
|
-
const empty = document.createElement("div");
|
|
473
|
-
empty.className = "main__empty";
|
|
474
|
-
empty.textContent = "No .atelier files found in this directory";
|
|
475
|
-
editorContainer.appendChild(empty);
|
|
476
|
-
statusBar.textContent = "No files";
|
|
477
|
-
renderFileList();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
renderFileList();
|
|
482
|
-
|
|
483
|
-
const initialFile = ${initialFileStr};
|
|
484
|
-
const target = initialFile
|
|
485
|
-
? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))
|
|
486
|
-
: files[0];
|
|
487
|
-
|
|
488
|
-
if (target) {
|
|
489
|
-
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";
|
|
490
348
|
}
|
|
491
349
|
}
|
|
492
|
-
|
|
493
|
-
boot();
|
|
494
|
-
`;
|
|
495
|
-
}
|
|
496
350
|
function studioCommand(program2) {
|
|
497
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(
|
|
498
352
|
async (file, options) => {
|
|
@@ -502,13 +356,20 @@ function studioCommand(program2) {
|
|
|
502
356
|
process.exit(1);
|
|
503
357
|
}
|
|
504
358
|
const cwd = process.cwd();
|
|
505
|
-
const cliPackageDir =
|
|
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
|
+
}
|
|
506
367
|
const tmpId = randomBytes(4).toString("hex");
|
|
507
368
|
const tmpDirRaw = join(tmpdir(), `atelier-studio-${tmpId}`);
|
|
508
369
|
mkdirSync(tmpDirRaw, { recursive: true });
|
|
509
370
|
const tmpDir = realpathSync(tmpDirRaw);
|
|
510
371
|
writeFileSync(join(tmpDir, "index.html"), getInlineHTML());
|
|
511
|
-
writeFileSync(join(tmpDir, "main.ts"), getInlineApp(file ?? null));
|
|
372
|
+
writeFileSync(join(tmpDir, "main.ts"), getInlineApp(file ?? null, cliPackageDir));
|
|
512
373
|
const cliNodeModules = join(cliPackageDir, "node_modules");
|
|
513
374
|
if (existsSync(cliNodeModules)) {
|
|
514
375
|
try {
|
|
@@ -516,7 +377,6 @@ function studioCommand(program2) {
|
|
|
516
377
|
} catch {
|
|
517
378
|
}
|
|
518
379
|
}
|
|
519
|
-
console.log(`Starting Atelier Studio...`);
|
|
520
380
|
console.log(` Working directory: ${cwd}`);
|
|
521
381
|
let vite;
|
|
522
382
|
try {
|
|
@@ -527,9 +387,42 @@ function studioCommand(program2) {
|
|
|
527
387
|
process.exit(1);
|
|
528
388
|
return;
|
|
529
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
|
+
});
|
|
530
422
|
const server = await vite.createServer({
|
|
531
423
|
root: tmpDir,
|
|
532
424
|
server: {
|
|
425
|
+
host: HOSTNAME,
|
|
533
426
|
port,
|
|
534
427
|
strictPort: false,
|
|
535
428
|
fs: {
|
|
@@ -540,8 +433,21 @@ function studioCommand(program2) {
|
|
|
540
433
|
{
|
|
541
434
|
name: "atelier-api",
|
|
542
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"]);
|
|
543
441
|
server2.middlewares.use((req, res, next) => {
|
|
544
|
-
const url2 = new URL(req.url ?? "/", `http
|
|
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
|
+
}
|
|
545
451
|
if (url2.pathname === "/api/files") {
|
|
546
452
|
const atelierFiles2 = findAtelierFiles(cwd);
|
|
547
453
|
const entries = atelierFiles2.map((p) => {
|
|
@@ -563,10 +469,15 @@ function studioCommand(program2) {
|
|
|
563
469
|
res.end("Invalid path");
|
|
564
470
|
return;
|
|
565
471
|
}
|
|
566
|
-
const absPath =
|
|
472
|
+
const absPath = resolve2(cwd, filePath);
|
|
567
473
|
if (req.method === "GET") {
|
|
568
474
|
try {
|
|
569
|
-
const content =
|
|
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
|
+
}
|
|
570
481
|
res.setHeader("Content-Type", "text/plain");
|
|
571
482
|
res.end(content);
|
|
572
483
|
} catch {
|
|
@@ -582,11 +493,17 @@ function studioCommand(program2) {
|
|
|
582
493
|
});
|
|
583
494
|
req.on("end", () => {
|
|
584
495
|
try {
|
|
585
|
-
|
|
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
|
+
}
|
|
586
502
|
res.end("OK");
|
|
587
|
-
} catch {
|
|
503
|
+
} catch (e) {
|
|
504
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
588
505
|
res.statusCode = 500;
|
|
589
|
-
res.end(
|
|
506
|
+
res.end(msg);
|
|
590
507
|
}
|
|
591
508
|
});
|
|
592
509
|
return;
|
|
@@ -599,7 +516,7 @@ function studioCommand(program2) {
|
|
|
599
516
|
res.end("Invalid path");
|
|
600
517
|
return;
|
|
601
518
|
}
|
|
602
|
-
const absPath =
|
|
519
|
+
const absPath = resolve2(cwd, filePath);
|
|
603
520
|
const chunks = [];
|
|
604
521
|
req.on("data", (chunk) => {
|
|
605
522
|
chunks.push(chunk);
|
|
@@ -609,9 +526,10 @@ function studioCommand(program2) {
|
|
|
609
526
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
610
527
|
writeFileSync(absPath, Buffer.concat(chunks));
|
|
611
528
|
res.end("OK");
|
|
612
|
-
} catch {
|
|
529
|
+
} catch (e) {
|
|
530
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
613
531
|
res.statusCode = 500;
|
|
614
|
-
res.end(
|
|
532
|
+
res.end(msg);
|
|
615
533
|
}
|
|
616
534
|
});
|
|
617
535
|
return;
|
|
@@ -629,6 +547,51 @@ function studioCommand(program2) {
|
|
|
629
547
|
logLevel: "warn"
|
|
630
548
|
});
|
|
631
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
|
+
}
|
|
632
595
|
const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;
|
|
633
596
|
const url = resolvedUrl;
|
|
634
597
|
console.log(` Server running at: ${url}`);
|
|
@@ -661,11 +624,20 @@ function studioCommand(program2) {
|
|
|
661
624
|
var program = new Command();
|
|
662
625
|
program.name("atelier").description("Atelier animation CLI").version(createRequire(import.meta.url)("../package.json").version);
|
|
663
626
|
validateCommand(program);
|
|
627
|
+
lintCommand(program);
|
|
628
|
+
trimCommand(program);
|
|
629
|
+
transcribeCommand(program);
|
|
630
|
+
transcriptCommand(program);
|
|
631
|
+
captionsCommand(program);
|
|
632
|
+
recipeCommand(program);
|
|
633
|
+
applyRecipeCommand(program);
|
|
634
|
+
carouselCommand(program);
|
|
664
635
|
infoCommand(program);
|
|
665
636
|
stillCommand(program);
|
|
666
637
|
renderCommand(program);
|
|
667
638
|
exportSvgCommand(program);
|
|
668
639
|
exportLottieCommand(program);
|
|
640
|
+
exportImageCommand(program);
|
|
669
641
|
assetsCommand(program);
|
|
670
642
|
variablesCommand(program);
|
|
671
643
|
studioCommand(program);
|