@marver-design/marver 0.16.1 → 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 +25 -0
- package/README.md +2 -2
- package/dist/{build-B4yPgFNF.mjs → build-D_g53Bp2.mjs} +2 -2
- package/dist/cli.mjs +5 -5
- package/dist/{dev-DH2W7Ffw.mjs → dev-LnIISva5.mjs} +7 -2
- package/dist/{plugin-BeBGu3gH.mjs → plugin-D2msH1cj.mjs} +142 -24
- package/dist/{poster-CoyobbGW.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/package.json +1 -1
- package/templates/AGENTS-embedded.md +4 -1
- package/templates/AGENTS-studio.md +4 -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-z-d-zMzf.mjs +0 -528
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
|
|
4
4
|
|
|
5
|
+
## 0.17.0 - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Shots in batch.** `npx marver shot a b c`, `--scene <name>` or `--all` render many
|
|
10
|
+
frames as ONE operation: one headless browser, several frames at a time inside it
|
|
11
|
+
(`MARVER_SHOT_CONCURRENCY`, default up to 6 sized to the machine). `POST /api/shots`
|
|
12
|
+
and the jam inbox (`{"scene":...}`, `{"frames":[...]}`, `{"all":true}`) take the same
|
|
13
|
+
ask; every frame answers for itself (`results[]`, in the order asked). Measured on a
|
|
14
|
+
31-frame canvas: 65 s of one-at-a-time calls becomes 8 s; a 5-frame scene, 10 s
|
|
15
|
+
becomes 2 s; a single shot, 2.1 s becomes 1.05 s.
|
|
16
|
+
- A capture that ran out of settle budget says so: `unsettled: true` and a note, instead
|
|
17
|
+
of a silently half-drawn PNG.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **A shot's Chrome could outlive the server** - Ctrl-C, a closed terminal or `kill -9`
|
|
22
|
+
mid-shot left a headless copy of the user's own Chrome running for ever, and on macOS
|
|
23
|
+
such a ghost can swallow every link the machine opens. The browser is now driven over
|
|
24
|
+
Chrome's debugging pipe, so it dies with the server however the server dies, and none
|
|
25
|
+
exists between shots. The first `marver dev` after upgrading kills the ghosts and
|
|
26
|
+
removes the profiles earlier versions left in the temp dir.
|
|
27
|
+
- Chrome starts and opens tabs several times faster: a stale `--disable-gpu` flag was
|
|
28
|
+
costing ~0.5 s per start and per tab.
|
|
29
|
+
|
|
5
30
|
## 0.16.1 - 2026-09-03
|
|
6
31
|
|
|
7
32
|
### Added
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ The trust boundary is hard: only comments written on the owner's machine trigger
|
|
|
84
84
|
|
|
85
85
|
## Working state
|
|
86
86
|
|
|
87
|
-
The same glow, driven from the terminal. When your agent takes a request, it creates the frame files first, pins them on a board, and runs `npx marver work start <scene/frame ...>` - you see the work land on the canvas in seconds, watch it shimmer while subagents build in parallel, and see it settle on `work done`. Marks self-expire, so a crashed agent never leaves a frame glowing. And `npx marver shot <scene/frame
|
|
87
|
+
The same glow, driven from the terminal. When your agent takes a request, it creates the frame files first, pins them on a board, and runs `npx marver work start <scene/frame ...>` - you see the work land on the canvas in seconds, watch it shimmer while subagents build in parallel, and see it settle on `work done`. Marks self-expire, so a crashed agent never leaves a frame glowing. And `npx marver shot --scene <scene>` (or `<scene/frame ...>`, `--all`; `[--scale 4]`) renders frames headless to PNGs - a whole scene in one browser, several frames at a time - so the agent can look at what it built before it says it is done - the same picture you get from the canvas's copy-as-image.
|
|
88
88
|
|
|
89
89
|
## Commands
|
|
90
90
|
|
|
@@ -97,7 +97,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
|
|
|
97
97
|
| `npx marver share …` | The roster (owner): `add <who> [--role]` · `remove` · `block` / `unblock` · `general <mode>` · `list` · `requests` · `explain <who>` · `who` |
|
|
98
98
|
| `npx marver comments …` | The agent's queue: `connect <url>` · `sync` · `list` · `reply` · `resolve` · `invite <email>` · `revoke <email>` |
|
|
99
99
|
| `npx marver work …` | Working glow from the terminal: `start <scene/frame …>` · `done … \| --all` · `list` |
|
|
100
|
-
| `npx marver shot <frame> [--scale 1-4]` | Render
|
|
100
|
+
| `npx marver shot <frame ...> \| --scene <name> \| --all [--scale 1-4] [--json]` | Render frames headless and print the PNG paths (needs `dev` running); a scene is one browser, several frames at a time; 2x by default |
|
|
101
101
|
| `npx marver boards [--json]` | The sidebar as the files say it is: folders, boards in reading order with their title, `order` and description, the landing board |
|
|
102
102
|
|
|
103
103
|
## Shortcuts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME } from "./cli.mjs";
|
|
2
2
|
import { a as listBoardFiles, d as flatten, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
|
|
3
3
|
import { l as detectHost, n as scanFrames, s as loadConfig } from "./manifest-CaslQIAO.mjs";
|
|
4
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
4
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-D2msH1cj.mjs";
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -504,7 +504,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
504
504
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
505
505
|
for (const r of refs) {
|
|
506
506
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
507
|
-
const { ensurePoster } = await import("./poster-
|
|
507
|
+
const { ensurePoster } = await import("./poster-BEjUcQP3.mjs");
|
|
508
508
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
509
509
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
510
510
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
package/dist/cli.mjs
CHANGED
|
@@ -71,7 +71,7 @@ cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "st
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
74
|
-
const { dev } = await import("./dev-
|
|
74
|
+
const { dev } = await import("./dev-LnIISva5.mjs");
|
|
75
75
|
let port;
|
|
76
76
|
if (opts.port !== void 0) {
|
|
77
77
|
const n = Number(opts.port);
|
|
@@ -81,7 +81,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
81
81
|
await dev(resolve$1(opts.root), port);
|
|
82
82
|
});
|
|
83
83
|
cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--embed-seeds", "Copy comment history INTO the web root (identifying - every event carries its author's email)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
84
|
-
const { buildSite } = await import("./build-
|
|
84
|
+
const { buildSite } = await import("./build-D_g53Bp2.mjs");
|
|
85
85
|
try {
|
|
86
86
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
87
87
|
await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
|
|
@@ -135,10 +135,10 @@ cli.command("boards", "The sidebar as files: every folder and board in reading o
|
|
|
135
135
|
process.exit(1);
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
|
-
cli.command("shot
|
|
139
|
-
const { shotCommand } = await import("./shot-
|
|
138
|
+
cli.command("shot [...frames]", "Render frames headless and print the PNG paths (needs `dev` running): ids, --scene <name> or --all").option("--root <dir>", "Host repo root", { default: "." }).option("--scene <name>", "Every frame of one scene").option("--all", "Every frame of the canvas").option("--theme <name>", "Theme to render (default: light)").option("--scale <n>", "Device pixels per CSS px, 1-4 (default: 2; 4 for print-quality stills)").option("--json", "Print the results as JSON ({ results: [...] }) instead of paths").action(async (frames, opts) => {
|
|
139
|
+
const { shotCommand } = await import("./shot-BzQ0PXKH.mjs");
|
|
140
140
|
try {
|
|
141
|
-
await shotCommand(resolve$1(opts.root),
|
|
141
|
+
await shotCommand(resolve$1(opts.root), frames, opts);
|
|
142
142
|
} catch (err) {
|
|
143
143
|
console.error(`[${NAME}] ${err.message}`);
|
|
144
144
|
process.exit(1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
2
|
import { l as detectHost, s as loadConfig } from "./manifest-CaslQIAO.mjs";
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-D2msH1cj.mjs";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -255,9 +255,14 @@ async function dev(root, portFlag) {
|
|
|
255
255
|
const port = typeof addr === "object" && addr ? addr.port : picked.port;
|
|
256
256
|
if (picked.fellBack) console.log(`\n port ${desiredPort} is in use - serving "${projectName}" on ${port} instead`);
|
|
257
257
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
258
|
+
setTimeout(async () => {
|
|
259
|
+
try {
|
|
260
|
+
await (await import("./shot-DlmTO8AF.mjs")).sweepGhosts((m) => console.log(` ${m}`));
|
|
261
|
+
} catch {}
|
|
262
|
+
}, 0);
|
|
258
263
|
try {
|
|
259
264
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
260
|
-
const { installedVersion } = await import("./plugin-
|
|
265
|
+
const { installedVersion } = await import("./plugin-D2msH1cj.mjs").then((n) => n.i);
|
|
261
266
|
const stale = staleManagedInstructions(root);
|
|
262
267
|
if (stale.length) {
|
|
263
268
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -422,7 +422,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
422
422
|
};
|
|
423
423
|
const asPng = url.searchParams.get("format") === "png";
|
|
424
424
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
425
|
-
const { shootFrame } = await import("./shot-
|
|
425
|
+
const { shootFrame } = await import("./shot-DlmTO8AF.mjs");
|
|
426
426
|
const r = await shootFrame({
|
|
427
427
|
root,
|
|
428
428
|
viewports: opts.viewports ?? {},
|
|
@@ -441,6 +441,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
441
441
|
height: r.height,
|
|
442
442
|
scale: r.scale,
|
|
443
443
|
...r.truncated ? { truncated: true } : {},
|
|
444
|
+
...r.unsettled ? { unsettled: true } : {},
|
|
444
445
|
...r.note ? { note: r.note } : {}
|
|
445
446
|
};
|
|
446
447
|
if (!asPng) return json(res, 200, summary);
|
|
@@ -453,11 +454,46 @@ function apiMiddleware(root, opts = {}) {
|
|
|
453
454
|
});
|
|
454
455
|
return res.end(png);
|
|
455
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
|
+
}
|
|
456
492
|
if (path === "poster" && req.method === "GET") {
|
|
457
493
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
458
494
|
const src = url.searchParams.get("src") ?? "";
|
|
459
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
460
|
-
const { isLocalAssetRef } = await import("./build-
|
|
495
|
+
const { ensurePoster, isLocalClip } = await import("./poster-BEjUcQP3.mjs");
|
|
496
|
+
const { isLocalAssetRef } = await import("./build-D_g53Bp2.mjs");
|
|
461
497
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
462
498
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
463
499
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -982,23 +1018,68 @@ function marverPlugin(ctx) {
|
|
|
982
1018
|
if (!existsSync(reqPath)) return;
|
|
983
1019
|
inFlight.add(file);
|
|
984
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
|
+
}
|
|
985
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
|
+
};
|
|
986
1042
|
let spec;
|
|
987
1043
|
try {
|
|
988
|
-
spec = JSON.parse(readFileSync(
|
|
989
|
-
|
|
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
|
+
});
|
|
990
1080
|
return;
|
|
991
1081
|
}
|
|
992
|
-
const base = file.replace(/\.request\.json$/, "");
|
|
993
|
-
const resultPath = join(inbox, `${base}.result.json`);
|
|
994
|
-
const frameId = typeof spec.frame === "string" ? spec.frame : "";
|
|
995
|
-
const theme = typeof spec.theme === "string" ? spec.theme : "light";
|
|
996
1082
|
const o = origin();
|
|
997
|
-
const write = (r) => {
|
|
998
|
-
try {
|
|
999
|
-
writeFileSync(resultPath, JSON.stringify(r, null, 2));
|
|
1000
|
-
} catch {}
|
|
1001
|
-
};
|
|
1002
1083
|
if (!o) {
|
|
1003
1084
|
write({
|
|
1004
1085
|
ok: false,
|
|
@@ -1006,18 +1087,55 @@ function marverPlugin(ctx) {
|
|
|
1006
1087
|
});
|
|
1007
1088
|
return;
|
|
1008
1089
|
}
|
|
1009
|
-
const { shootFrame } = await import("./shot-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
}
|
|
1017
1117
|
try {
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
+
}
|
|
1020
1135
|
} finally {
|
|
1136
|
+
try {
|
|
1137
|
+
rmSync(claimed, { force: true });
|
|
1138
|
+
} catch {}
|
|
1021
1139
|
inFlight.delete(file);
|
|
1022
1140
|
}
|
|
1023
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 };
|