@marver-design/marver 0.16.0 → 0.17.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/CHANGELOG.md +51 -0
- package/README.md +4 -4
- package/dist/{boards-BdG1TJwU.mjs → boards-BmxcT3Lc.mjs} +32 -9
- package/dist/{boards-6hKVW42a.mjs → boards-PuVzw5Wp.mjs} +9 -4
- package/dist/{build-Ct0iMoSi.mjs → build-D_g53Bp2.mjs} +12 -8
- package/dist/cli.mjs +27 -16
- package/dist/{daemon-Cb4JjpgL.mjs → daemon-Bbh_jmui.mjs} +1 -1
- package/dist/{dev-CdcIuhZJ.mjs → dev-LnIISva5.mjs} +9 -4
- package/dist/{init-BQIpIpIv.mjs → init-B7YhcN2o.mjs} +1 -1
- package/dist/{manifest-CpbsqQ_v.mjs → manifest-CaslQIAO.mjs} +112 -15
- package/dist/{plugin-BXyezwfN.mjs → plugin-D2msH1cj.mjs} +223 -43
- package/dist/{poster-DOY7pax8.mjs → poster-BEjUcQP3.mjs} +13 -5
- package/dist/shot-BzQ0PXKH.mjs +86 -0
- package/dist/shot-DlmTO8AF.mjs +926 -0
- package/docs/live-jam.md +20 -8
- package/docs/publish.md +2 -2
- package/package.json +1 -1
- package/src/client/shell/App.tsx +42 -14
- package/src/client/shell/BoardList.tsx +88 -58
- package/src/client/shell/LockedApp.tsx +2 -1
- package/src/client/shell/Play.tsx +1 -0
- package/src/client/shell/labels.ts +4 -4
- package/src/client/shell/store.ts +76 -29
- package/src/client/shell/styles.css +5 -1
- package/src/shared/board-tree.ts +104 -46
- package/templates/AGENTS-embedded.md +13 -3
- package/templates/AGENTS-studio.md +13 -3
- package/templates/instructions/boards.md +27 -9
- package/templates/instructions/discover.md +13 -1
- package/templates/instructions/jam.md +15 -3
- package/templates/instructions/shape.md +2 -1
- package/dist/shot-By1AItpD.mjs +0 -30
- package/dist/shot-CwmHO5T4.mjs +0 -528
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as listBoardFiles, c as BOARD_NAME, g as hash, h as validateWire, i as isRegularFile, l as FOLDERS_FILE, m as readTitle, n as checkBoardsDir, o as nodeExists, p as readDescription, s as readRegistry, t as boardFields } from "./boards-BmxcT3Lc.mjs";
|
|
4
4
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { a as writeManifest, n as scanFrames, r as setSceneTitle, s as loadConfig, t as affectedFrameIds } from "./manifest-CaslQIAO.mjs";
|
|
6
6
|
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -155,9 +155,13 @@ function apiMiddleware(root, opts = {}) {
|
|
|
155
155
|
return json(res, 400, { error: "malformed JSON" });
|
|
156
156
|
}
|
|
157
157
|
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
158
|
-
const { from, to } = parsed;
|
|
158
|
+
const { from, to: toRaw, title: titleRaw, baseHash } = parsed;
|
|
159
|
+
const to = toRaw === void 0 ? from : toRaw;
|
|
159
160
|
if (!validName(from) || !validName(to)) return json(res, 400, { error: "invalid board name" });
|
|
160
|
-
if (
|
|
161
|
+
if (titleRaw !== void 0 && (typeof titleRaw !== "string" || Array.from(titleRaw).length > 120)) return json(res, 400, { error: "invalid title" });
|
|
162
|
+
if (baseHash !== void 0 && typeof baseHash !== "string") return json(res, 400, { error: "invalid baseHash" });
|
|
163
|
+
if (to === "all-scenes" || from === "all-scenes") return json(res, 400, { error: "invalid rename" });
|
|
164
|
+
if (to === from && titleRaw === void 0) return json(res, 400, { error: "invalid rename" });
|
|
161
165
|
{
|
|
162
166
|
const de = dirError();
|
|
163
167
|
if (de) return json(res, 400, { error: de });
|
|
@@ -166,21 +170,74 @@ function apiMiddleware(root, opts = {}) {
|
|
|
166
170
|
if (!fromPath || !toPath) return json(res, 400, { error: "invalid board name" });
|
|
167
171
|
if (!existsSync(fromPath)) return json(res, 404, { error: `board "${from}" does not exist` });
|
|
168
172
|
if (!notSymlink(fromPath)) return json(res, 400, { error: "refusing to rename a symlinked board file" });
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
const current = readFileSync(fromPath, "utf8");
|
|
174
|
+
if (baseHash !== void 0 && baseHash !== hash(current)) return json(res, 409, {
|
|
175
|
+
error: "board changed on disk",
|
|
176
|
+
sha256: hash(current)
|
|
177
|
+
});
|
|
178
|
+
let obj = null;
|
|
179
|
+
if (titleRaw !== void 0) {
|
|
180
|
+
let parsedBoard;
|
|
181
|
+
try {
|
|
182
|
+
parsedBoard = JSON.parse(current);
|
|
183
|
+
} catch {
|
|
184
|
+
return json(res, 422, { error: `board "${from}" is not valid JSON - fix the file` });
|
|
185
|
+
}
|
|
186
|
+
if (!parsedBoard || typeof parsedBoard !== "object" || Array.isArray(parsedBoard)) return json(res, 422, { error: `board "${from}" is not a JSON object - fix the file` });
|
|
187
|
+
obj = parsedBoard;
|
|
188
|
+
}
|
|
189
|
+
let next;
|
|
190
|
+
if (obj) {
|
|
191
|
+
const title = readTitle(titleRaw);
|
|
192
|
+
if (title) obj.title = title;
|
|
193
|
+
else delete obj.title;
|
|
194
|
+
next = JSON.stringify(obj, null, 2) + "\n";
|
|
195
|
+
}
|
|
196
|
+
if (to !== from) {
|
|
197
|
+
if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
|
|
198
|
+
if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
|
|
178
199
|
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
try {
|
|
201
|
+
if (next !== void 0) writeFileSync(toPath, next, { flag: "wx" });
|
|
202
|
+
else linkSync(fromPath, toPath);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const code = err.code;
|
|
205
|
+
if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
206
|
+
if (next !== void 0 || code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
|
|
207
|
+
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
208
|
+
renameSync(fromPath, toPath);
|
|
209
|
+
return json(res, 200, { name: to });
|
|
210
|
+
}
|
|
211
|
+
rmSync(fromPath);
|
|
212
|
+
return json(res, 200, {
|
|
213
|
+
name: to,
|
|
214
|
+
...next !== void 0 ? { sha256: hash(next) } : {}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
atomicWrite(toPath, next);
|
|
218
|
+
return json(res, 200, {
|
|
219
|
+
name: to,
|
|
220
|
+
sha256: hash(next)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (path === "scenes/rename" && req.method === "POST") {
|
|
224
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
225
|
+
const raw = await readBody(req);
|
|
226
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
181
232
|
}
|
|
182
|
-
|
|
183
|
-
|
|
233
|
+
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
234
|
+
const { scene, title } = parsed;
|
|
235
|
+
if (typeof scene !== "string" || !scene || scene.length > 128 || /[\\/]/.test(scene) || scene === "." || scene === "..") return json(res, 400, { error: "invalid scene name" });
|
|
236
|
+
if (typeof title !== "string" || Array.from(title).length > 120) return json(res, 400, { error: "invalid title" });
|
|
237
|
+
if (!scanFrames(root).scenes.some((sc) => sc.name === scene)) return json(res, 400, { error: `scene "${scene}" does not exist (no frames under design/scenes/${scene}/)` });
|
|
238
|
+
const e = setSceneTitle(root, scene, readTitle(title) ?? "");
|
|
239
|
+
if (e) return json(res, 400, { error: e });
|
|
240
|
+
return json(res, 200, { ok: true });
|
|
184
241
|
}
|
|
185
242
|
if (path === "boards/reorder" && req.method === "POST") {
|
|
186
243
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
@@ -249,10 +306,11 @@ function apiMiddleware(root, opts = {}) {
|
|
|
249
306
|
if (e) return json(res, 422, { error: e });
|
|
250
307
|
continue;
|
|
251
308
|
}
|
|
252
|
-
const description = readDescription(it.description);
|
|
309
|
+
const title = readTitle(it.title), description = readDescription(it.description);
|
|
253
310
|
folders.push({
|
|
254
311
|
name: it.folder,
|
|
255
312
|
order: i,
|
|
313
|
+
...title ? { title } : {},
|
|
256
314
|
...description ? { description } : {}
|
|
257
315
|
});
|
|
258
316
|
for (const [j, kid] of it.boards.entries()) {
|
|
@@ -339,6 +397,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
339
397
|
const disk = JSON.parse(current);
|
|
340
398
|
if (incoming.order === void 0 && typeof disk.order === "number" && Number.isFinite(disk.order)) incoming.order = disk.order;
|
|
341
399
|
if (incoming.folder === void 0 && validName(disk.folder)) incoming.folder = disk.folder;
|
|
400
|
+
if (incoming.title === void 0 && readTitle(disk.title)) incoming.title = disk.title;
|
|
342
401
|
if (incoming.description === void 0 && readDescription(disk.description)) incoming.description = disk.description;
|
|
343
402
|
} catch {}
|
|
344
403
|
const next2 = JSON.stringify(body.board, null, 2) + "\n";
|
|
@@ -363,7 +422,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
363
422
|
};
|
|
364
423
|
const asPng = url.searchParams.get("format") === "png";
|
|
365
424
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
366
|
-
const { shootFrame } = await import("./shot-
|
|
425
|
+
const { shootFrame } = await import("./shot-DlmTO8AF.mjs");
|
|
367
426
|
const r = await shootFrame({
|
|
368
427
|
root,
|
|
369
428
|
viewports: opts.viewports ?? {},
|
|
@@ -382,6 +441,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
382
441
|
height: r.height,
|
|
383
442
|
scale: r.scale,
|
|
384
443
|
...r.truncated ? { truncated: true } : {},
|
|
444
|
+
...r.unsettled ? { unsettled: true } : {},
|
|
385
445
|
...r.note ? { note: r.note } : {}
|
|
386
446
|
};
|
|
387
447
|
if (!asPng) return json(res, 200, summary);
|
|
@@ -394,11 +454,46 @@ function apiMiddleware(root, opts = {}) {
|
|
|
394
454
|
});
|
|
395
455
|
return res.end(png);
|
|
396
456
|
}
|
|
457
|
+
if (path === "shots" && req.method === "POST") {
|
|
458
|
+
const { readDevInfo } = await import("./work-CLrmY-vQ.mjs");
|
|
459
|
+
const token = readDevInfo(root)?.token;
|
|
460
|
+
if (!ownerGated(req) && !(token && req.headers["x-mv-work"] === token)) return json(res, 403, { error: "forbidden" });
|
|
461
|
+
const raw = await readBody(req);
|
|
462
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
463
|
+
let parsed;
|
|
464
|
+
try {
|
|
465
|
+
parsed = JSON.parse(raw);
|
|
466
|
+
} catch {
|
|
467
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
468
|
+
}
|
|
469
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return json(res, 400, { error: "expected an object" });
|
|
470
|
+
const body = parsed;
|
|
471
|
+
const theme = body.theme === void 0 ? "light" : body.theme;
|
|
472
|
+
if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
|
|
473
|
+
const scale = body.scale === void 0 ? void 0 : body.scale;
|
|
474
|
+
if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) return json(res, 400, { error: "invalid scale" });
|
|
475
|
+
const { resolveFrames, shootBatch } = await import("./shot-DlmTO8AF.mjs");
|
|
476
|
+
const sel = resolveFrames(root, body);
|
|
477
|
+
if (!sel.ok) return json(res, sel.status, { error: sel.error });
|
|
478
|
+
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
479
|
+
try {
|
|
480
|
+
return json(res, 200, { results: await shootBatch({
|
|
481
|
+
root,
|
|
482
|
+
viewports: opts.viewports ?? {},
|
|
483
|
+
frames: sel.frames,
|
|
484
|
+
theme,
|
|
485
|
+
origin,
|
|
486
|
+
scale
|
|
487
|
+
}) });
|
|
488
|
+
} catch (err) {
|
|
489
|
+
return json(res, 503, { error: err.message });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
397
492
|
if (path === "poster" && req.method === "GET") {
|
|
398
493
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
399
494
|
const src = url.searchParams.get("src") ?? "";
|
|
400
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
401
|
-
const { isLocalAssetRef } = await import("./build-
|
|
495
|
+
const { ensurePoster, isLocalClip } = await import("./poster-BEjUcQP3.mjs");
|
|
496
|
+
const { isLocalAssetRef } = await import("./build-D_g53Bp2.mjs");
|
|
402
497
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
403
498
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
404
499
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -827,12 +922,15 @@ function marverPlugin(ctx) {
|
|
|
827
922
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
828
923
|
devServer = server;
|
|
829
924
|
let framesKey = JSON.stringify(manifest.frames);
|
|
925
|
+
let scenesKey = JSON.stringify(manifest.scenes);
|
|
830
926
|
const regen = debounce(() => {
|
|
831
927
|
manifest = scanFrames(root, projectInfo());
|
|
832
928
|
const changed = writeManifest(root, manifest);
|
|
833
|
-
const key = JSON.stringify(manifest.frames);
|
|
929
|
+
const key = JSON.stringify(manifest.frames), sKey = JSON.stringify(manifest.scenes);
|
|
834
930
|
if (changed && key !== framesKey) server.ws.send("sh:manifest", manifest);
|
|
931
|
+
else if (changed && sKey !== scenesKey) server.ws.send("sh:scenes", { scenes: manifest.scenes });
|
|
835
932
|
framesKey = key;
|
|
933
|
+
scenesKey = sKey;
|
|
836
934
|
}, 150);
|
|
837
935
|
const watched = [join(root, "design", "scenes"), join(root, "design", "components")];
|
|
838
936
|
const inScope = (file) => watched.some((w) => file.startsWith(w));
|
|
@@ -920,23 +1018,68 @@ function marverPlugin(ctx) {
|
|
|
920
1018
|
if (!existsSync(reqPath)) return;
|
|
921
1019
|
inFlight.add(file);
|
|
922
1020
|
setTimeout(async () => {
|
|
1021
|
+
const base = file.replace(/\.request\.json$/, "");
|
|
1022
|
+
const resultPath = join(inbox, `${base}.result.json`);
|
|
1023
|
+
const claimed = join(inbox, `.${base}.${process.pid}.${Date.now().toString(36)}.claimed`);
|
|
1024
|
+
try {
|
|
1025
|
+
renameSync(reqPath, claimed);
|
|
1026
|
+
} catch {
|
|
1027
|
+
inFlight.delete(file);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
923
1030
|
try {
|
|
1031
|
+
const write = (r) => {
|
|
1032
|
+
const tmp = join(inbox, `.${base}.${process.pid}.tmp`);
|
|
1033
|
+
try {
|
|
1034
|
+
writeFileSync(tmp, JSON.stringify(r, null, 2));
|
|
1035
|
+
renameSync(tmp, resultPath);
|
|
1036
|
+
} catch {
|
|
1037
|
+
try {
|
|
1038
|
+
rmSync(tmp, { force: true });
|
|
1039
|
+
} catch {}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
924
1042
|
let spec;
|
|
925
1043
|
try {
|
|
926
|
-
spec = JSON.parse(readFileSync(
|
|
927
|
-
|
|
1044
|
+
spec = JSON.parse(readFileSync(claimed, "utf8"));
|
|
1045
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) throw new Error("expected an object");
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
write({
|
|
1048
|
+
ok: false,
|
|
1049
|
+
error: `malformed request - ${err.message}`
|
|
1050
|
+
});
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const theme = spec.theme === void 0 ? "light" : spec.theme;
|
|
1054
|
+
if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) {
|
|
1055
|
+
write({
|
|
1056
|
+
ok: false,
|
|
1057
|
+
error: "invalid theme"
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const scale = spec.scale === void 0 ? void 0 : spec.scale;
|
|
1062
|
+
if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) {
|
|
1063
|
+
write({
|
|
1064
|
+
ok: false,
|
|
1065
|
+
error: "invalid scale"
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const ways = [
|
|
1070
|
+
"frame",
|
|
1071
|
+
"frames",
|
|
1072
|
+
"scene",
|
|
1073
|
+
"all"
|
|
1074
|
+
].filter((k) => spec[k] !== void 0);
|
|
1075
|
+
if (ways.length !== 1) {
|
|
1076
|
+
write({
|
|
1077
|
+
ok: false,
|
|
1078
|
+
error: "name the frames one way: frame (an id), frames (an array of ids), scene (a name) or all (true)"
|
|
1079
|
+
});
|
|
928
1080
|
return;
|
|
929
1081
|
}
|
|
930
|
-
const base = file.replace(/\.request\.json$/, "");
|
|
931
|
-
const resultPath = join(inbox, `${base}.result.json`);
|
|
932
|
-
const frameId = typeof spec.frame === "string" ? spec.frame : "";
|
|
933
|
-
const theme = typeof spec.theme === "string" ? spec.theme : "light";
|
|
934
1082
|
const o = origin();
|
|
935
|
-
const write = (r) => {
|
|
936
|
-
try {
|
|
937
|
-
writeFileSync(resultPath, JSON.stringify(r, null, 2));
|
|
938
|
-
} catch {}
|
|
939
|
-
};
|
|
940
1083
|
if (!o) {
|
|
941
1084
|
write({
|
|
942
1085
|
ok: false,
|
|
@@ -944,18 +1087,55 @@ function marverPlugin(ctx) {
|
|
|
944
1087
|
});
|
|
945
1088
|
return;
|
|
946
1089
|
}
|
|
947
|
-
const { shootFrame } = await import("./shot-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1090
|
+
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-DlmTO8AF.mjs");
|
|
1091
|
+
if (ways[0] === "frame") {
|
|
1092
|
+
if (typeof spec.frame !== "string") {
|
|
1093
|
+
write({
|
|
1094
|
+
ok: false,
|
|
1095
|
+
error: "frame must be a frame id"
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
write(await shootFrame({
|
|
1100
|
+
root,
|
|
1101
|
+
viewports: config.viewports,
|
|
1102
|
+
frameId: spec.frame,
|
|
1103
|
+
theme,
|
|
1104
|
+
origin: o,
|
|
1105
|
+
scale
|
|
1106
|
+
}));
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const sel = resolveFrames(root, spec);
|
|
1110
|
+
if (!sel.ok) {
|
|
1111
|
+
write({
|
|
1112
|
+
ok: false,
|
|
1113
|
+
error: sel.error
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
955
1117
|
try {
|
|
956
|
-
|
|
957
|
-
|
|
1118
|
+
write({
|
|
1119
|
+
ok: true,
|
|
1120
|
+
results: await shootBatch({
|
|
1121
|
+
root,
|
|
1122
|
+
viewports: config.viewports,
|
|
1123
|
+
frames: sel.frames,
|
|
1124
|
+
theme,
|
|
1125
|
+
origin: o,
|
|
1126
|
+
scale
|
|
1127
|
+
})
|
|
1128
|
+
});
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
write({
|
|
1131
|
+
ok: false,
|
|
1132
|
+
error: err.message
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
958
1135
|
} finally {
|
|
1136
|
+
try {
|
|
1137
|
+
rmSync(claimed, { force: true });
|
|
1138
|
+
} catch {}
|
|
959
1139
|
inFlight.delete(file);
|
|
960
1140
|
}
|
|
961
1141
|
}, 60);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capture, findChrome } from "./shot-
|
|
1
|
+
import { capture, t as findChrome } from "./shot-DlmTO8AF.mjs";
|
|
2
2
|
import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -52,15 +52,23 @@ html,body{margin:0;background:#000}#root video{display:block}
|
|
|
52
52
|
setTimeout(function(){ if (!done && v.readyState >= 2) show() }, 4000);
|
|
53
53
|
})();
|
|
54
54
|
<\/script></body></html>`;
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
55
|
+
/** Single-flight by output path: N frames of one batch referencing the same clip ask at the
|
|
56
|
+
* same moment, and each would otherwise pass the "exists" check and render the same poster. */
|
|
57
|
+
const rendering = /* @__PURE__ */ new Map();
|
|
58
|
+
function ensurePoster(assetsDir, src) {
|
|
59
|
+
const out = join(assetsDir, posterNameFor(src));
|
|
60
|
+
const live = rendering.get(out);
|
|
61
|
+
if (live) return live;
|
|
62
|
+
const run = ensurePosterNow(assetsDir, src, out).finally(() => rendering.delete(out));
|
|
63
|
+
rendering.set(out, run);
|
|
64
|
+
return run;
|
|
65
|
+
}
|
|
66
|
+
async function ensurePosterNow(assetsDir, src, out) {
|
|
58
67
|
if (!isLocalClip(src)) return {
|
|
59
68
|
ok: false,
|
|
60
69
|
error: `not a local clip: ${src}`
|
|
61
70
|
};
|
|
62
71
|
const clip = join(assetsDir, src);
|
|
63
|
-
const out = join(assetsDir, posterNameFor(src));
|
|
64
72
|
if (existsSync(out)) return {
|
|
65
73
|
ok: true,
|
|
66
74
|
path: out,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { n as NAME } from "./cli.mjs";
|
|
2
|
+
import { readDevInfo } from "./work-CLrmY-vQ.mjs";
|
|
3
|
+
//#region src/cli/shot.ts
|
|
4
|
+
/**
|
|
5
|
+
* `marver shot <scene/frame> [...]`, `--scene <name>`, `--all` - render frames headless and
|
|
6
|
+
* print the PNG paths, one per line, in the order asked.
|
|
7
|
+
*
|
|
8
|
+
* The shell-ful agents' door into the same verify loop jam teaches: build, shoot, LOOK. One
|
|
9
|
+
* frame goes through /api/shot; several, a scene or everything go through /api/shots as ONE
|
|
10
|
+
* operation (one browser, several frames at a time), so shooting a scene costs about what
|
|
11
|
+
* shooting a frame did. shot.ts (server) has the capture story.
|
|
12
|
+
*/
|
|
13
|
+
async function shotCommand(root, frames, opts) {
|
|
14
|
+
const usage = `${NAME} shot <scene/frame ...> | --scene <name> | --all [--theme <name>] [--scale 1-4] [--json]`;
|
|
15
|
+
if ([
|
|
16
|
+
frames.length > 0,
|
|
17
|
+
!!opts.scene,
|
|
18
|
+
!!opts.all
|
|
19
|
+
].filter(Boolean).length !== 1) throw new Error(`name the frames, one way: ${usage}`);
|
|
20
|
+
const info = readDevInfo(root);
|
|
21
|
+
if (!info) throw new Error(`\`${NAME} dev\` is not running in this repo (design/.local/dev.json not found) - start it first.`);
|
|
22
|
+
const scale = opts.scale != null ? Number(opts.scale) : void 0;
|
|
23
|
+
const theme = opts.theme;
|
|
24
|
+
const single = frames.length === 1 && !opts.scene && !opts.all;
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
if (single) {
|
|
28
|
+
const qs = new URLSearchParams({
|
|
29
|
+
frame: frames[0],
|
|
30
|
+
...theme ? { theme } : {},
|
|
31
|
+
...scale != null ? { scale: String(scale) } : {}
|
|
32
|
+
});
|
|
33
|
+
res = await fetch(`http://localhost:${info.port}/__mv/api/shot?${qs}`, { headers: { "x-mv-work": info.token } });
|
|
34
|
+
} else {
|
|
35
|
+
const sel = opts.all ? { all: true } : opts.scene ? { scene: opts.scene } : { frames };
|
|
36
|
+
res = await fetch(`http://localhost:${info.port}/__mv/api/shots`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"x-mv-work": info.token,
|
|
40
|
+
"content-type": "application/json"
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
...sel,
|
|
44
|
+
...theme ? { theme } : {},
|
|
45
|
+
...scale != null ? { scale } : {}
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(`could not reach \`${NAME} dev\` on port ${info.port} - is it still running?`);
|
|
51
|
+
}
|
|
52
|
+
const data = await res.json().catch(() => ({}));
|
|
53
|
+
if (!res.ok) throw new Error(data.error ?? `shot failed (${res.status})`);
|
|
54
|
+
const one = data;
|
|
55
|
+
const results = single ? [one.path ? {
|
|
56
|
+
frame: frames[0],
|
|
57
|
+
ok: true,
|
|
58
|
+
path: one.path,
|
|
59
|
+
width: one.width,
|
|
60
|
+
height: one.height,
|
|
61
|
+
scale: one.scale,
|
|
62
|
+
...one.truncated ? { truncated: true } : {},
|
|
63
|
+
...one.unsettled ? { unsettled: true } : {},
|
|
64
|
+
...one.note ? { note: one.note } : {}
|
|
65
|
+
} : {
|
|
66
|
+
frame: frames[0],
|
|
67
|
+
ok: false,
|
|
68
|
+
error: one.error ?? "shot failed"
|
|
69
|
+
}] : data.results ?? [];
|
|
70
|
+
if (opts.json) {
|
|
71
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
72
|
+
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let failed = false;
|
|
76
|
+
for (const r of results) if (r.ok) {
|
|
77
|
+
console.log(r.path);
|
|
78
|
+
if (r.note) console.error(`${r.frame}: ${r.note}`);
|
|
79
|
+
} else {
|
|
80
|
+
failed = true;
|
|
81
|
+
console.error(`${r.frame}: ${r.error}`);
|
|
82
|
+
}
|
|
83
|
+
if (failed) process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { shotCommand };
|