@neta-art/cohub-cli 8.4.1 → 9.1.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/README.md +66 -30
- package/dist/board-export.d.ts +21 -36
- package/dist/board-export.js +163 -88
- package/dist/board-kernel.d.ts +2 -3
- package/dist/board-kernel.js +3428 -3770
- package/dist/board-time.d.ts +3 -0
- package/dist/board-time.js +28 -0
- package/dist/commands/apps.js +2 -2
- package/dist/commands/auth.js +2 -2
- package/dist/commands/boards/examples.d.ts +8 -2
- package/dist/commands/boards/examples.js +79 -411
- package/dist/commands/boards.d.ts +4 -9
- package/dist/commands/boards.js +365 -286
- package/dist/commands/desktop.d.ts +1 -5
- package/dist/commands/desktop.js +5 -16
- package/dist/commands/generations.js +1 -1
- package/dist/commands/me.js +1 -1
- package/dist/commands/public.js +3 -3
- package/dist/commands/references.js +3 -0
- package/dist/commands/referrals.js +2 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/runtime.js +14 -9
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +2 -23
- package/dist/commands/search.js +15 -7
- package/dist/commands/space-activity.js +3 -5
- package/dist/commands/space-displays.d.ts +2 -0
- package/dist/commands/space-displays.js +347 -0
- package/dist/commands/space-files.d.ts +5 -0
- package/dist/commands/space-files.js +215 -0
- package/dist/commands/space-invitations.js +3 -3
- package/dist/commands/spaces.d.ts +3 -11
- package/dist/commands/spaces.js +146 -75
- package/dist/index.js +8 -5
- package/dist/runtime/launch.d.ts +2 -0
- package/dist/runtime/launch.js +38 -8
- package/dist/runtime/presentation.d.ts +1 -0
- package/dist/runtime/presentation.js +3 -2
- package/dist/runtime/supervisor.d.ts +1 -0
- package/dist/runtime/supervisor.js +2 -1
- package/dist/space-files/limit.d.ts +2 -0
- package/dist/space-files/limit.js +16 -0
- package/dist/space-files/operands.d.ts +16 -0
- package/dist/space-files/operands.js +33 -0
- package/dist/space-files/transfer.d.ts +53 -0
- package/dist/space-files/transfer.js +296 -0
- package/dist/space-files/upload.d.ts +14 -0
- package/dist/space-files/upload.js +53 -0
- package/dist/space.d.ts +18 -16
- package/dist/space.js +86 -39
- package/dist/web.d.ts +1 -0
- package/dist/web.js +6 -0
- package/package.json +2 -2
- package/dist/commands/board-domain.d.ts +0 -2
- package/dist/commands/board-domain.js +0 -16
- package/dist/commands/boards/animation.d.ts +0 -2
- package/dist/commands/boards/animation.js +0 -171
- package/dist/commands/boards/appearance.d.ts +0 -2
- package/dist/commands/boards/appearance.js +0 -127
- package/dist/commands/boards/batch.d.ts +0 -4
- package/dist/commands/boards/batch.js +0 -62
- package/dist/commands/boards/connections.d.ts +0 -2
- package/dist/commands/boards/connections.js +0 -95
- package/dist/commands/boards/context.d.ts +0 -31
- package/dist/commands/boards/context.js +0 -71
- package/dist/commands/boards/items.d.ts +0 -2
- package/dist/commands/boards/items.js +0 -184
- package/dist/commands/boards/transactions.d.ts +0 -12
- package/dist/commands/boards/transactions.js +0 -69
package/README.md
CHANGED
|
@@ -72,12 +72,15 @@ cohub -s <spaceId> spaces invites revoke <code> --yes
|
|
|
72
72
|
cohub -s <spaceId> run -- git status
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
Space-scoped commands use `-s`, then `COHUB_SPACE_ID`, then the Space remembered for
|
|
76
|
+
the current directory. `<space>` is a Space ID, a slug you own such as `home`, or
|
|
77
|
+
`username/slug`. Only `prompt`, `completion`, `generate`, `apps`, and `public` fall
|
|
78
|
+
back to your Home Space; other commands exit with an error when no target is given:
|
|
77
79
|
|
|
78
80
|
```bash
|
|
79
81
|
cohub -s <spaceId> spaces prompt "message" --json
|
|
80
82
|
COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
83
|
+
cohub -s home spaces files ls
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
## Local Runtime
|
|
@@ -112,6 +115,13 @@ cohub runtime down
|
|
|
112
115
|
cohub runtime up -n --name another-project
|
|
113
116
|
```
|
|
114
117
|
|
|
118
|
+
On macOS and Linux X11, a new Runtime offers screen sharing by default. The prompt
|
|
119
|
+
explains that collaborators and agents can see and control the screen; Enter accepts,
|
|
120
|
+
and `n` declines. Use `--no-display` to skip sharing, or `--display xvfb[:WIDTHxHEIGHT]`
|
|
121
|
+
for a virtual screen. `--yes` also authorizes default screen sharing. Headless and
|
|
122
|
+
Wayland-only Linux sessions skip the default offer; an existing Runtime keeps its
|
|
123
|
+
screen configuration unless explicitly overridden (configuration changes require a restart).
|
|
124
|
+
|
|
115
125
|
Runtime diagnostics stay as redacted JSONL under the local Runtime state directory and
|
|
116
126
|
are never uploaded automatically. Use `runtime logs --json` to export a failure report;
|
|
117
127
|
events carry the local `runtimeId` plus server `requestId` / `traceparent` when a turn has
|
|
@@ -222,57 +232,63 @@ cohub -s <spaceId> spaces activity 365 --json
|
|
|
222
232
|
|
|
223
233
|
## Boards
|
|
224
234
|
|
|
225
|
-
Board
|
|
235
|
+
A Board is one JSON document with three collections — `board` settings, `items`,
|
|
236
|
+
and `animations`. Board targets accept a Board ID or a `.board` path. Every
|
|
237
|
+
command supports `-h`, and the global `--json` makes the output machine-readable:
|
|
226
238
|
|
|
227
239
|
```bash
|
|
228
240
|
cohub boards -h
|
|
229
|
-
cohub -s <spaceId> boards
|
|
230
|
-
cohub -s <spaceId> boards
|
|
231
|
-
cohub -s <spaceId> boards
|
|
232
|
-
cohub -s <spaceId> boards
|
|
241
|
+
cohub -s <spaceId> boards get boards/plan.board --json
|
|
242
|
+
cohub -s <spaceId> boards get <boardId> --only items
|
|
243
|
+
cohub -s <spaceId> boards get <boardId> --only board
|
|
244
|
+
cohub -s <spaceId> boards schema
|
|
245
|
+
cohub -s <spaceId> boards history <boardId> --json
|
|
233
246
|
cohub -s <spaceId> boards watch <boardId> --json
|
|
234
247
|
```
|
|
235
248
|
|
|
236
|
-
Use
|
|
249
|
+
Use the filters so a large Board is not read whole:
|
|
237
250
|
|
|
238
251
|
```bash
|
|
239
|
-
cohub -s <spaceId> boards
|
|
240
|
-
cohub -s <spaceId> boards
|
|
241
|
-
cohub -s <spaceId> boards
|
|
242
|
-
cohub -s <spaceId> boards compositions get <boardId> <compositionId> --json
|
|
252
|
+
cohub -s <spaceId> boards get <boardId> --items title,note
|
|
253
|
+
cohub -s <spaceId> boards get <boardId> --within s1
|
|
254
|
+
cohub -s <spaceId> boards get <boardId> --rect 0,0,1600,900
|
|
243
255
|
```
|
|
244
256
|
|
|
245
|
-
|
|
257
|
+
Start from an example and write it with `apply`, a JSON Merge Patch where fields
|
|
258
|
+
merge, `null` deletes, and unmentioned fields stay:
|
|
246
259
|
|
|
247
260
|
```bash
|
|
248
|
-
cohub boards examples
|
|
249
|
-
cohub -s <spaceId> boards
|
|
261
|
+
cohub boards examples workflow > seed.json
|
|
262
|
+
cohub -s <spaceId> boards create boards/plan.board --title "Plan" -i seed.json
|
|
250
263
|
|
|
251
|
-
cohub
|
|
252
|
-
cohub -s <spaceId> boards
|
|
264
|
+
cohub -s <spaceId> boards apply <boardId> '{"items":{"title":{"props":{"text":"Updated"}}}}'
|
|
265
|
+
cohub -s <spaceId> boards apply <boardId> -i changes.json
|
|
253
266
|
```
|
|
254
267
|
|
|
255
|
-
|
|
268
|
+
Group a whole change into one patch so it lands as one version. `--replace` makes
|
|
269
|
+
the document equal to the patch instead of merging; `--cascade` also deletes
|
|
270
|
+
children and the tracks targeting a deleted item:
|
|
256
271
|
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
{"type":"connection.create","connection":{"id":"title-agent","source":{"itemId":"title"},"target":{"itemId":"agent"}}}
|
|
261
|
-
]}
|
|
272
|
+
```bash
|
|
273
|
+
cohub -s <spaceId> boards apply <boardId> -i changes.json --dry-run
|
|
274
|
+
cohub -s <spaceId> boards apply <boardId> -i changes.json --base-version 12 --mutation-id deploy-1
|
|
262
275
|
```
|
|
263
276
|
|
|
277
|
+
Use `--base-version` and `--mutation-id` for controlled, retry-safe scripts, and
|
|
278
|
+
`--dry-run` to validate without writing. `boards preset` prints a preset motion as
|
|
279
|
+
tracks — an apply-ready patch with `--animation`, bare tracks without it:
|
|
280
|
+
|
|
264
281
|
```bash
|
|
265
|
-
cohub boards
|
|
266
|
-
cohub -s <spaceId> boards
|
|
267
|
-
cohub -s <spaceId> boards batch <boardId> --input changes.json
|
|
282
|
+
cohub -s <spaceId> boards preset rise --targets a,b --animation intro | cohub boards apply <boardId> -i -
|
|
283
|
+
cohub -s <spaceId> boards preset float --targets ship > idle-tracks.json
|
|
268
284
|
```
|
|
269
285
|
|
|
270
|
-
|
|
286
|
+
Playback commands sit on `boards` directly:
|
|
271
287
|
|
|
272
288
|
```bash
|
|
273
|
-
cohub -s <spaceId> boards
|
|
274
|
-
cohub -s <spaceId> boards
|
|
275
|
-
cohub -s <spaceId> boards
|
|
289
|
+
cohub -s <spaceId> boards play <boardId> <animationId> --at 2.5s
|
|
290
|
+
cohub -s <spaceId> boards seek <boardId> 12.5s
|
|
291
|
+
cohub -s <spaceId> boards stop <boardId>
|
|
276
292
|
```
|
|
277
293
|
|
|
278
294
|
## Search
|
|
@@ -349,12 +365,32 @@ Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
|
|
|
349
365
|
```bash
|
|
350
366
|
cohub -s <spaceId> spaces files ls [path] --json
|
|
351
367
|
cohub -s <spaceId> spaces files cat <path>
|
|
368
|
+
cohub -s <spaceId> spaces files cat <path> > <local-file>
|
|
352
369
|
cohub -s <spaceId> spaces files write <path> -c "<content>"
|
|
353
370
|
cohub -s <spaceId> spaces files upload <files...> --dir <dir>
|
|
371
|
+
cohub spaces files cp [-r] [-n] [-p] <source>... <destination>
|
|
354
372
|
cohub -s <spaceId> spaces files mv <from> <to>
|
|
355
373
|
cohub -s <spaceId> spaces files rm <path>
|
|
356
374
|
```
|
|
357
375
|
|
|
376
|
+
`cat` streams raw bytes, so redirecting it saves binary files intact.
|
|
377
|
+
|
|
378
|
+
`cp` works like `scp` with `cp` semantics: the last path is the destination,
|
|
379
|
+
directories need `-r`, existing files are overwritten and directories merged
|
|
380
|
+
(`-n` keeps existing files, `-p` keeps modification times). `<space>:<path>`
|
|
381
|
+
addresses a Space by id, `username/slug`, or the slug of a Space you own. Bare
|
|
382
|
+
paths address the current Space when one is declared with `-s` or
|
|
383
|
+
`COHUB_SPACE_ID` (as inside a Cohub sandbox) and local files otherwise;
|
|
384
|
+
absolute paths outside `/workspace` are always local:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
cohub spaces files cp -r alice/templates:starter ./starter # download
|
|
388
|
+
cohub spaces files cp report.pdf <spaceId>:inbox/ # upload
|
|
389
|
+
cohub -s <spaceId> spaces files cp -r other:docs docs # Space to Space
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Copies between Spaces run server-side and never pass file content through the CLI.
|
|
393
|
+
|
|
358
394
|
`upload` places each file under `--dir`; a directory argument contributes its
|
|
359
395
|
contents directly (like `aws s3 cp dir remote:path`), so `upload dist --dir apps/demo`
|
|
360
396
|
lands at `apps/demo/index.html`, not `apps/demo/dist/index.html`. Upload adds or
|
package/dist/board-export.d.ts
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
* Board image export for the CLI.
|
|
3
|
-
*
|
|
4
|
-
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
-
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
-
* Canvas2D. Everything platform-specific
|
|
7
|
-
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
8
|
-
* space, and locating fonts on disk.
|
|
9
|
-
*/
|
|
10
|
-
import type { BoardDocument, BoardExportRegion, BoardItem } from "@neta-art/cohub/board";
|
|
1
|
+
import type { BoardDocument, BoardExportRegion, BoardSceneItem } from "@neta-art/cohub/board";
|
|
11
2
|
import type { BoardHeadlessExportFormat, BoardHeadlessFont, BoardHeadlessRenderer, BoardHeadlessTexture } from "@neta-art/cohub/board/headless";
|
|
12
|
-
/**
|
|
13
|
-
* Geist, as shipped to the browser.
|
|
14
|
-
*
|
|
15
|
-
* The board asks for the "Geist" family; registering the exact same font files
|
|
16
|
-
* the web app loads is what makes CLI output match the editor rather than
|
|
17
|
-
* substituting whatever sans-serif the host happens to have. A source checkout
|
|
18
|
-
* falls back to the dev packages.
|
|
19
|
-
*/
|
|
20
3
|
export declare function resolveBundledFonts(): BoardHeadlessFont[];
|
|
21
4
|
export type BoardExportSource = {
|
|
22
5
|
document: BoardDocument;
|
|
23
6
|
boardId: string;
|
|
24
7
|
title: string | null;
|
|
25
8
|
};
|
|
26
|
-
/**
|
|
27
|
-
* Load a board document by board id or by the path of its `.board` file.
|
|
28
|
-
*
|
|
29
|
-
* A `.board` file is a manifest holding a board id, so both forms converge on
|
|
30
|
-
* the same inspect call — which is also what the web client does.
|
|
31
|
-
*/
|
|
32
9
|
export declare function loadBoardDocument(spaceId: string, target: string): Promise<BoardExportSource>;
|
|
33
|
-
|
|
34
|
-
* Fetch every image in `items`, keyed the way the renderers ask for it.
|
|
35
|
-
*
|
|
36
|
-
* Takes the planned items rather than the whole document so a partial export
|
|
37
|
-
* does not download the rest of the board. Failures are collected rather than
|
|
38
|
-
* thrown: one unreadable image should cost a placeholder and a warning, not the
|
|
39
|
-
* whole export. Downloads run concurrently but bounded, so a board with hundreds
|
|
40
|
-
* of images does not open hundreds of sockets.
|
|
41
|
-
*/
|
|
42
|
-
export declare function loadBoardTextures(headless: BoardHeadlessRenderer, spaceId: string, items: BoardItem[], options?: {
|
|
10
|
+
export declare function loadBoardTextures(headless: BoardHeadlessRenderer, spaceId: string, items: readonly BoardSceneItem[], options?: {
|
|
43
11
|
concurrency?: number;
|
|
44
12
|
}): Promise<{
|
|
45
13
|
textures: Map<string, BoardHeadlessTexture>;
|
|
@@ -50,6 +18,8 @@ export type BoardExportRunOptions = {
|
|
|
50
18
|
spaceId: string;
|
|
51
19
|
target: string;
|
|
52
20
|
region: BoardExportRegion;
|
|
21
|
+
times?: number[];
|
|
22
|
+
animation?: string;
|
|
53
23
|
scale: number;
|
|
54
24
|
padding?: number;
|
|
55
25
|
colorScheme: "dark" | "light";
|
|
@@ -58,14 +28,29 @@ export type BoardExportRunOptions = {
|
|
|
58
28
|
quality?: number;
|
|
59
29
|
withImages: boolean;
|
|
60
30
|
};
|
|
61
|
-
export type
|
|
31
|
+
export type BoardExportFrame = {
|
|
32
|
+
time: number | null;
|
|
62
33
|
bytes: Uint8Array;
|
|
63
34
|
width: number;
|
|
64
35
|
height: number;
|
|
65
36
|
scale: number;
|
|
66
37
|
itemCount: number;
|
|
67
38
|
format: BoardHeadlessExportFormat;
|
|
39
|
+
};
|
|
40
|
+
export type BoardExportRunResult = {
|
|
41
|
+
frames: BoardExportFrame[];
|
|
68
42
|
warnings: string[];
|
|
69
43
|
};
|
|
70
|
-
|
|
44
|
+
export type BoardVideoExportFormat = "mp4" | "webm";
|
|
45
|
+
export type BoardVideoExportOptions = Omit<BoardExportRunOptions, "format"> & {
|
|
46
|
+
format: BoardVideoExportFormat;
|
|
47
|
+
fps: number;
|
|
48
|
+
output: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function runBoardVideoExport(options: BoardVideoExportOptions): Promise<{
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
frames: number;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}>;
|
|
71
56
|
export declare function runBoardExport(options: BoardExportRunOptions): Promise<BoardExportRunResult | null>;
|
package/dist/board-export.js
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board image export for the CLI.
|
|
3
|
-
*
|
|
4
|
-
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
-
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
-
* Canvas2D. Everything platform-specific
|
|
7
|
-
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
8
|
-
* space, and locating fonts on disk.
|
|
9
|
-
*/
|
|
10
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
11
5
|
import { createRequire } from "node:module";
|
|
12
6
|
import { dirname, join } from "node:path";
|
|
13
7
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import {
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
const execFile = promisify(execFileCallback);
|
|
10
|
+
import { boardDocumentAt, boardImageKeySource, buildBoardScene, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
|
|
15
11
|
import { resolveBoardId } from "./board-command-support.js";
|
|
16
12
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
17
13
|
import { createClient } from "./client.js";
|
|
18
14
|
import { downloadPublicImage } from "./safe-remote-image.js";
|
|
19
|
-
/**
|
|
20
|
-
* Geist, as shipped to the browser.
|
|
21
|
-
*
|
|
22
|
-
* The board asks for the "Geist" family; registering the exact same font files
|
|
23
|
-
* the web app loads is what makes CLI output match the editor rather than
|
|
24
|
-
* substituting whatever sans-serif the host happens to have. A source checkout
|
|
25
|
-
* falls back to the dev packages.
|
|
26
|
-
*/
|
|
27
15
|
export function resolveBundledFonts() {
|
|
28
16
|
const require = createRequire(import.meta.url);
|
|
29
17
|
const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
|
|
@@ -44,33 +32,21 @@ export function resolveBundledFonts() {
|
|
|
44
32
|
return path ? [{ path, family: font.family }] : [];
|
|
45
33
|
});
|
|
46
34
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Load a board document by board id or by the path of its `.board` file.
|
|
49
|
-
*
|
|
50
|
-
* A `.board` file is a manifest holding a board id, so both forms converge on
|
|
51
|
-
* the same inspect call — which is also what the web client does.
|
|
52
|
-
*/
|
|
53
35
|
export async function loadBoardDocument(spaceId, target) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
36
|
+
const board = createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
|
|
37
|
+
let result = await board.get();
|
|
38
|
+
const items = { ...result.items };
|
|
39
|
+
while (result.next) {
|
|
40
|
+
result = await board.get({ only: ["items"], cursor: result.next });
|
|
41
|
+
Object.assign(items, result.items);
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseBoardDocument({ board: result.board, items, animations: result.animations ?? {} });
|
|
44
|
+
if (!parsed.ok)
|
|
45
|
+
throw new Error(`Board ${result.id} could not be read: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
|
|
46
|
+
return { document: parsed.document, boardId: result.id, title: result.title };
|
|
62
47
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Fetch every image in `items`, keyed the way the renderers ask for it.
|
|
65
|
-
*
|
|
66
|
-
* Takes the planned items rather than the whole document so a partial export
|
|
67
|
-
* does not download the rest of the board. Failures are collected rather than
|
|
68
|
-
* thrown: one unreadable image should cost a placeholder and a warning, not the
|
|
69
|
-
* whole export. Downloads run concurrently but bounded, so a board with hundreds
|
|
70
|
-
* of images does not open hundreds of sockets.
|
|
71
|
-
*/
|
|
72
48
|
export async function loadBoardTextures(headless, spaceId, items, options = {}) {
|
|
73
|
-
const selection = selectBoardExportAssets(items, imageAssetKey);
|
|
49
|
+
const selection = selectBoardExportAssets([...items], imageAssetKey);
|
|
74
50
|
const textures = new Map();
|
|
75
51
|
const failed = [];
|
|
76
52
|
const pending = [...selection.keys];
|
|
@@ -105,13 +81,80 @@ async function readSpaceFileBytes(client, spaceId, path) {
|
|
|
105
81
|
const { blob, mimeType } = await client.space(spaceId).files.download(path);
|
|
106
82
|
return { bytes: new Uint8Array(await blob.arrayBuffer()), mimeType };
|
|
107
83
|
}
|
|
108
|
-
|
|
84
|
+
export async function runBoardVideoExport(options) {
|
|
85
|
+
if (!options.animation)
|
|
86
|
+
throw new Error("Video export needs --animation.");
|
|
87
|
+
if (!options.times || options.times.length < 2)
|
|
88
|
+
throw new Error("Video export needs an --at range with at least two frames.");
|
|
89
|
+
const times = options.times;
|
|
90
|
+
const step = times[1] - times[0];
|
|
91
|
+
if (!Number.isFinite(step) || step <= 0)
|
|
92
|
+
throw new Error("Video export needs an increasing --at range.");
|
|
93
|
+
const derivedFps = 1000 / step;
|
|
94
|
+
if (Math.abs(options.fps - derivedFps) > 0.01)
|
|
95
|
+
throw new Error(`--fps must match the --at step (${derivedFps.toFixed(3)} fps).`);
|
|
96
|
+
const result = await runBoardExport({ ...options, format: "png" });
|
|
97
|
+
if (!result)
|
|
98
|
+
throw new Error("Nothing to export: the region contains no items.");
|
|
99
|
+
const tempDir = await mkdtemp(join(tmpdir(), "cohub-board-export-"));
|
|
100
|
+
try {
|
|
101
|
+
for (const [index, frame] of result.frames.entries())
|
|
102
|
+
await writeFile(join(tempDir, `${String(index + 1).padStart(6, "0")}.png`), frame.bytes);
|
|
103
|
+
const document = (await loadBoardDocument(options.spaceId, options.target)).document;
|
|
104
|
+
const audioDocument = boardDocumentAt(document, options.animation, times[0]).document;
|
|
105
|
+
const audio = Object.values(audioDocument.items).filter((item) => item.type === "audio");
|
|
106
|
+
const audioInputs = [];
|
|
107
|
+
for (const [index, item] of audio.entries()) {
|
|
108
|
+
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, item.props.src);
|
|
109
|
+
const path = join(tempDir, `audio-${index}-${item.props.src.split("/").pop() ?? "track"}`);
|
|
110
|
+
await writeFile(path, bytes);
|
|
111
|
+
audioInputs.push({ path, offset: Math.max(0, item.props.time ?? 0) / 1000 });
|
|
112
|
+
}
|
|
113
|
+
const duration = Math.max(0.001, result.frames.length / options.fps);
|
|
114
|
+
const args = ["-y", "-f", "image2", "-framerate", String(options.fps), "-i", join(tempDir, "%06d.png")];
|
|
115
|
+
for (const input of audioInputs) {
|
|
116
|
+
if (input.offset > 0)
|
|
117
|
+
args.push("-ss", input.offset.toFixed(3));
|
|
118
|
+
args.push("-i", input.path);
|
|
119
|
+
}
|
|
120
|
+
if (audioInputs.length > 1) {
|
|
121
|
+
const inputs = audioInputs.map((_, index) => `[${index + 1}:a]`).join("");
|
|
122
|
+
args.push("-filter_complex", `${inputs}amix=inputs=${audioInputs.length}:duration=longest:dropout_transition=0[aout]`, "-map", "0:v:0", "-map", "[aout]");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
args.push("-map", "0:v:0");
|
|
126
|
+
if (audioInputs.length === 1)
|
|
127
|
+
args.push("-map", "1:a:0");
|
|
128
|
+
}
|
|
129
|
+
if (options.format === "mp4")
|
|
130
|
+
args.push("-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", "-f", "mp4");
|
|
131
|
+
else
|
|
132
|
+
args.push("-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0", "-c:a", "libopus", "-f", "webm");
|
|
133
|
+
args.push("-t", duration.toFixed(3), options.output);
|
|
134
|
+
try {
|
|
135
|
+
await execFile("ffmpeg", args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024 });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const cause = error;
|
|
139
|
+
if (cause.code === "ENOENT")
|
|
140
|
+
throw new Error("Video export needs ffmpeg. Install ffmpeg and try again.");
|
|
141
|
+
throw new Error(`ffmpeg could not encode the video: ${(cause.stderr ?? cause.message ?? "unknown error").trim().split("\n").slice(-6).join("\n")}`);
|
|
142
|
+
}
|
|
143
|
+
return { width: result.frames[0]?.width ?? 0, height: result.frames[0]?.height ?? 0, frames: result.frames.length, warnings: result.warnings };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
109
149
|
export async function runBoardExport(options) {
|
|
110
150
|
const { document } = await loadBoardDocument(options.spaceId, options.target);
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
if (options.animation && !document.animations[options.animation]) {
|
|
152
|
+
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
153
|
+
}
|
|
154
|
+
const times = options.times?.length ? options.times : [null];
|
|
155
|
+
const documentAt = (time) => time === null ? document : boardDocumentAt(document, options.animation ?? null, time).document;
|
|
113
156
|
const plan = planBoardExport({
|
|
114
|
-
|
|
157
|
+
scene: buildBoardScene(documentAt(times[0] ?? null)),
|
|
115
158
|
region: options.region,
|
|
116
159
|
scale: options.scale,
|
|
117
160
|
...(options.padding === undefined ? {} : { padding: options.padding }),
|
|
@@ -119,6 +162,7 @@ export async function runBoardExport(options) {
|
|
|
119
162
|
if (!plan)
|
|
120
163
|
return null;
|
|
121
164
|
const headless = await createBoardHeadlessRenderer({ fonts: resolveBundledFonts() });
|
|
165
|
+
let sketchHost = null;
|
|
122
166
|
try {
|
|
123
167
|
const warnings = [];
|
|
124
168
|
let textures;
|
|
@@ -135,7 +179,7 @@ export async function runBoardExport(options) {
|
|
|
135
179
|
warnings.push(`${loaded.omitted.length} previews were drawn as placeholders to stay within the export texture limit.`);
|
|
136
180
|
}
|
|
137
181
|
}
|
|
138
|
-
const declaredBackground = document.
|
|
182
|
+
const declaredBackground = document.board.background;
|
|
139
183
|
if (options.withImages &&
|
|
140
184
|
options.background === "paper" &&
|
|
141
185
|
declaredBackground.kind === "image" &&
|
|
@@ -149,54 +193,85 @@ export async function runBoardExport(options) {
|
|
|
149
193
|
}
|
|
150
194
|
}
|
|
151
195
|
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
196
|
+
sketchHost = createBoardHeadlessSketchHost(headless, {
|
|
197
|
+
readModule: async (src) => {
|
|
198
|
+
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, src);
|
|
199
|
+
return new TextDecoder().decode(bytes);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const region = times.length > 1 ? { kind: "rect", rect: plan.world } : options.region;
|
|
203
|
+
const host = sketchHost;
|
|
204
|
+
if (!host)
|
|
205
|
+
throw new Error("Headless sketch host could not be created.");
|
|
206
|
+
const sketchVariants = new Map();
|
|
207
|
+
for (const time of times) {
|
|
208
|
+
const sketchPlan = planBoardExport({
|
|
209
|
+
scene: buildBoardScene(documentAt(time)),
|
|
210
|
+
region,
|
|
211
|
+
scale: options.scale,
|
|
212
|
+
...(times.length > 1 ? { padding: 0 } : options.padding === undefined ? {} : { padding: options.padding }),
|
|
213
|
+
});
|
|
214
|
+
for (const item of sketchPlan?.items.filter((entry) => entry.type === "sketch") ?? []) {
|
|
215
|
+
const key = `${item.id}:${item.frame.width}:${item.frame.height}:${item.props.src}:${item.props.seed ?? ""}:${JSON.stringify(item.props.params)}`;
|
|
216
|
+
const variant = sketchVariants.get(key) ?? { item, times: [] };
|
|
217
|
+
variant.times.push(time ?? 0);
|
|
218
|
+
sketchVariants.set(key, variant);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await Promise.all([...sketchVariants.values()].map((variant) => host.prepare([variant.item], variant.times, plan.scale)));
|
|
152
222
|
if (videoCount > 0) {
|
|
153
223
|
warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; headless video decoding is unavailable.`);
|
|
154
224
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
?
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
: {}),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return null;
|
|
183
|
-
for (const warning of result.warnings) {
|
|
184
|
-
// Missing-image warnings are already reported above with their paths.
|
|
185
|
-
if (warning.kind === "images-missing" && options.withImages)
|
|
225
|
+
const backgroundImage = backgroundTexture
|
|
226
|
+
? { texture: backgroundTexture, fit: declaredBackground.fit ?? "cover", position: "center", opacity: declaredBackground.opacity ?? 1 }
|
|
227
|
+
: undefined;
|
|
228
|
+
const assetKey = options.withImages
|
|
229
|
+
? (item) => {
|
|
230
|
+
const key = imageAssetKey(item);
|
|
231
|
+
return key && omittedKeys.has(key) ? null : key;
|
|
232
|
+
}
|
|
233
|
+
: undefined;
|
|
234
|
+
const frames = [];
|
|
235
|
+
const reported = new Set();
|
|
236
|
+
for (const time of times) {
|
|
237
|
+
const result = exportBoardImageBytes(headless, document, {
|
|
238
|
+
...(time === null ? {} : { at: { ...(options.animation ? { animation: options.animation } : {}), time } }),
|
|
239
|
+
region,
|
|
240
|
+
scale: options.scale,
|
|
241
|
+
padding: times.length > 1 ? 0 : options.padding,
|
|
242
|
+
colorScheme: options.colorScheme,
|
|
243
|
+
background: options.background,
|
|
244
|
+
textures,
|
|
245
|
+
sketches: sketchHost ?? undefined,
|
|
246
|
+
backgroundImage,
|
|
247
|
+
...(assetKey ? { assetKey } : {}),
|
|
248
|
+
format: options.format,
|
|
249
|
+
quality: options.quality,
|
|
250
|
+
});
|
|
251
|
+
if (!result)
|
|
186
252
|
continue;
|
|
187
|
-
|
|
253
|
+
for (const warning of result.warnings) {
|
|
254
|
+
if (warning.kind === "images-missing" && options.withImages)
|
|
255
|
+
continue;
|
|
256
|
+
const text = describeWarning(warning);
|
|
257
|
+
if (!reported.has(text))
|
|
258
|
+
warnings.push(text);
|
|
259
|
+
reported.add(text);
|
|
260
|
+
}
|
|
261
|
+
frames.push({
|
|
262
|
+
time,
|
|
263
|
+
bytes: result.bytes,
|
|
264
|
+
width: result.plan.width,
|
|
265
|
+
height: result.plan.height,
|
|
266
|
+
scale: result.plan.scale,
|
|
267
|
+
itemCount: result.plan.items.length,
|
|
268
|
+
format: result.format,
|
|
269
|
+
});
|
|
188
270
|
}
|
|
189
|
-
return {
|
|
190
|
-
bytes: result.bytes,
|
|
191
|
-
width: result.plan.width,
|
|
192
|
-
height: result.plan.height,
|
|
193
|
-
scale: result.plan.scale,
|
|
194
|
-
itemCount: result.plan.items.length,
|
|
195
|
-
format: result.format,
|
|
196
|
-
warnings,
|
|
197
|
-
};
|
|
271
|
+
return frames.length ? { frames, warnings } : null;
|
|
198
272
|
}
|
|
199
273
|
finally {
|
|
274
|
+
sketchHost?.destroy();
|
|
200
275
|
headless.destroy();
|
|
201
276
|
}
|
|
202
277
|
}
|
package/dist/board-kernel.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
export { createBoardHeadlessRenderer, exportBoardImageBytes } from "@neta-art/cohub/board/headless";
|
|
1
|
+
export { boardDocumentAt, boardImageKeySource, buildBoardScene, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
2
|
+
export { createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes } from "@neta-art/cohub/board/headless";
|