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