@neta-art/cohub-cli 8.4.1 → 9.0.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.
Files changed (68) hide show
  1. package/README.md +25 -2
  2. package/dist/board-export.d.ts +21 -36
  3. package/dist/board-export.js +163 -88
  4. package/dist/board-kernel.d.ts +2 -3
  5. package/dist/board-kernel.js +3428 -3770
  6. package/dist/board-time.d.ts +3 -0
  7. package/dist/board-time.js +28 -0
  8. package/dist/commands/apps.js +2 -2
  9. package/dist/commands/auth.js +2 -2
  10. package/dist/commands/boards/examples.d.ts +8 -2
  11. package/dist/commands/boards/examples.js +79 -411
  12. package/dist/commands/boards.d.ts +4 -9
  13. package/dist/commands/boards.js +346 -286
  14. package/dist/commands/desktop.d.ts +1 -5
  15. package/dist/commands/desktop.js +5 -16
  16. package/dist/commands/generations.js +1 -1
  17. package/dist/commands/me.js +1 -1
  18. package/dist/commands/public.js +3 -3
  19. package/dist/commands/references.js +3 -0
  20. package/dist/commands/referrals.js +2 -2
  21. package/dist/commands/run.js +3 -3
  22. package/dist/commands/runtime.d.ts +1 -1
  23. package/dist/commands/runtime.js +12 -8
  24. package/dist/commands/search.js +15 -7
  25. package/dist/commands/space-activity.js +3 -5
  26. package/dist/commands/space-displays.d.ts +2 -0
  27. package/dist/commands/space-displays.js +347 -0
  28. package/dist/commands/space-files.d.ts +5 -0
  29. package/dist/commands/space-files.js +215 -0
  30. package/dist/commands/space-invitations.js +3 -3
  31. package/dist/commands/spaces.d.ts +3 -11
  32. package/dist/commands/spaces.js +146 -75
  33. package/dist/index.js +7 -4
  34. package/dist/runtime/launch.d.ts +2 -0
  35. package/dist/runtime/launch.js +35 -8
  36. package/dist/runtime/presentation.d.ts +1 -0
  37. package/dist/runtime/presentation.js +3 -2
  38. package/dist/runtime/supervisor.d.ts +1 -0
  39. package/dist/runtime/supervisor.js +2 -1
  40. package/dist/space-files/limit.d.ts +2 -0
  41. package/dist/space-files/limit.js +16 -0
  42. package/dist/space-files/operands.d.ts +16 -0
  43. package/dist/space-files/operands.js +33 -0
  44. package/dist/space-files/transfer.d.ts +53 -0
  45. package/dist/space-files/transfer.js +296 -0
  46. package/dist/space-files/upload.d.ts +14 -0
  47. package/dist/space-files/upload.js +53 -0
  48. package/dist/space.d.ts +18 -16
  49. package/dist/space.js +86 -39
  50. package/dist/web.d.ts +1 -0
  51. package/dist/web.js +6 -0
  52. package/package.json +2 -2
  53. package/dist/commands/board-domain.d.ts +0 -2
  54. package/dist/commands/board-domain.js +0 -16
  55. package/dist/commands/boards/animation.d.ts +0 -2
  56. package/dist/commands/boards/animation.js +0 -171
  57. package/dist/commands/boards/appearance.d.ts +0 -2
  58. package/dist/commands/boards/appearance.js +0 -127
  59. package/dist/commands/boards/batch.d.ts +0 -4
  60. package/dist/commands/boards/batch.js +0 -62
  61. package/dist/commands/boards/connections.d.ts +0 -2
  62. package/dist/commands/boards/connections.js +0 -95
  63. package/dist/commands/boards/context.d.ts +0 -31
  64. package/dist/commands/boards/context.js +0 -71
  65. package/dist/commands/boards/items.d.ts +0 -2
  66. package/dist/commands/boards/items.js +0 -184
  67. package/dist/commands/boards/transactions.d.ts +0 -12
  68. 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
- Many space-scoped commands need a target Space. Without an explicit target, commands
76
- also use the Space remembered for the current directory before falling back to Home:
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
@@ -349,12 +352,32 @@ Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
349
352
  ```bash
350
353
  cohub -s <spaceId> spaces files ls [path] --json
351
354
  cohub -s <spaceId> spaces files cat <path>
355
+ cohub -s <spaceId> spaces files cat <path> > <local-file>
352
356
  cohub -s <spaceId> spaces files write <path> -c "<content>"
353
357
  cohub -s <spaceId> spaces files upload <files...> --dir <dir>
358
+ cohub spaces files cp [-r] [-n] [-p] <source>... <destination>
354
359
  cohub -s <spaceId> spaces files mv <from> <to>
355
360
  cohub -s <spaceId> spaces files rm <path>
356
361
  ```
357
362
 
363
+ `cat` streams raw bytes, so redirecting it saves binary files intact.
364
+
365
+ `cp` works like `scp` with `cp` semantics: the last path is the destination,
366
+ directories need `-r`, existing files are overwritten and directories merged
367
+ (`-n` keeps existing files, `-p` keeps modification times). `<space>:<path>`
368
+ addresses a Space by id, `username/slug`, or the slug of a Space you own. Bare
369
+ paths address the current Space when one is declared with `-s` or
370
+ `COHUB_SPACE_ID` (as inside a Cohub sandbox) and local files otherwise;
371
+ absolute paths outside `/workspace` are always local:
372
+
373
+ ```bash
374
+ cohub spaces files cp -r alice/templates:starter ./starter # download
375
+ cohub spaces files cp report.pdf <spaceId>:inbox/ # upload
376
+ cohub -s <spaceId> spaces files cp -r other:docs docs # Space to Space
377
+ ```
378
+
379
+ Copies between Spaces run server-side and never pass file content through the CLI.
380
+
358
381
  `upload` places each file under `--dir`; a directory argument contributes its
359
382
  contents directly (like `aws s3 cp dir remote:path`), so `upload dist --dir apps/demo`
360
383
  lands at `apps/demo/index.html`, not `apps/demo/dist/index.html`. Upload adds or
@@ -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 BoardExportRunResult = {
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
- /** Render a board to image bytes. Returns null when the region is empty. */
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>;
@@ -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 { boardAuthoringSnapshotToDocument, boardImageKeySource, createBoardHeadlessRenderer, exportBoardImageBytes, imageAssetKey, planBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
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 client = createClient();
55
- const boardId = await resolveBoardId(spaceId, target);
56
- const snapshot = await client.space(spaceId).board(boardId).authoring({ include: ["items", "connections"] });
57
- return {
58
- document: boardAuthoringSnapshotToDocument(snapshot),
59
- boardId: snapshot.board.id,
60
- title: snapshot.board.title ?? null,
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
- /** Render a board to image bytes. Returns null when the region is empty. */
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
- // Plan before fetching: a --frame / --items / --rect export should only pull
112
- // the images it will actually draw, and an empty region should pull none.
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
- document,
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.appearance.background;
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 result = exportBoardImageBytes(headless, document, {
156
- region: options.region,
157
- scale: options.scale,
158
- padding: options.padding,
159
- colorScheme: options.colorScheme,
160
- background: options.background,
161
- textures,
162
- backgroundImage: backgroundTexture
163
- ? {
164
- texture: backgroundTexture,
165
- fit: declaredBackground.fit ?? "cover",
166
- position: declaredBackground.position ?? "center",
167
- opacity: declaredBackground.opacity ?? 1,
168
- }
169
- : undefined,
170
- ...(options.withImages
171
- ? {
172
- assetKey: (item) => {
173
- const key = imageAssetKey(item);
174
- return key && omittedKeys.has(key) ? null : key;
175
- },
176
- }
177
- : {}),
178
- format: options.format,
179
- quality: options.quality,
180
- });
181
- if (!result)
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
- warnings.push(describeWarning(warning));
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
  }
@@ -1,3 +1,2 @@
1
- /** The Board renderer for `boards export`, bundled with PixiJS by `tsdown.config.ts`. */
2
- export { boardAuthoringSnapshotToDocument, boardImageKeySource, imageAssetKey, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
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";