@neta-art/cohub-cli 8.4.0 → 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.
- package/README.md +25 -2
- package/dist/avatar.js +3 -1
- package/dist/board-export.d.ts +22 -41
- package/dist/board-export.js +178 -115
- package/dist/board-fonts.d.ts +7 -0
- package/dist/board-fonts.js +10 -0
- package/dist/board-kernel/browserAll.js +2760 -0
- package/dist/board-kernel/getPo2TextureFromSource.js +13138 -0
- package/dist/board-kernel/init.js +867 -0
- package/dist/board-kernel/webworkerAll.js +3 -0
- package/dist/board-kernel.d.ts +2 -0
- package/dist/board-kernel.js +28812 -0
- 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 +356 -286
- package/dist/commands/desktop.d.ts +1 -5
- package/dist/commands/desktop.js +10 -19
- 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 +12 -8
- 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/fonts/geist-latin-400-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
- package/dist/index.js +7 -4
- package/dist/runtime/launch.d.ts +2 -0
- package/dist/runtime/launch.js +35 -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/self-update.d.ts +1 -0
- package/dist/self-update.js +52 -16
- 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 +8 -4
- 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/dist/commands/boards.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
|
|
6
|
+
import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
|
|
7
|
+
import { formatBoardTime, parseBoardTime, parseBoardTimes } from "../board-time.js";
|
|
5
8
|
import { createClient, createRealtimeClient } from "../client.js";
|
|
6
9
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
7
10
|
import { resolveSpace } from "../space.js";
|
|
11
|
+
import { registerBoardExampleCommands } from "./boards/examples.js";
|
|
8
12
|
export const parseJsonObject = parseBoardJsonObject;
|
|
13
|
+
function withJson(command) {
|
|
14
|
+
return command.option("--json", "Output as JSON");
|
|
15
|
+
}
|
|
9
16
|
function parseNumber(value, name, options = {}) {
|
|
10
17
|
if (!value.trim())
|
|
11
18
|
throw new Error(`${name} must be a finite number`);
|
|
@@ -20,187 +27,213 @@ function parseNumber(value, name, options = {}) {
|
|
|
20
27
|
throw new Error(`${name} must be at most ${options.max}`);
|
|
21
28
|
return parsed;
|
|
22
29
|
}
|
|
23
|
-
export function
|
|
24
|
-
if (!value)
|
|
25
|
-
return undefined;
|
|
30
|
+
export function parseRect(value, name = "--rect") {
|
|
26
31
|
const parts = value.split(",").map((part) => part.trim());
|
|
27
32
|
if (parts.length !== 4)
|
|
28
|
-
throw new Error(
|
|
29
|
-
const [
|
|
30
|
-
if (xValue === undefined || yValue === undefined || widthValue === undefined || heightValue === undefined) {
|
|
31
|
-
throw new Error("viewport must be x,y,width,height");
|
|
32
|
-
}
|
|
33
|
-
const x = parseNumber(xValue, "viewport x");
|
|
34
|
-
const y = parseNumber(yValue, "viewport y");
|
|
35
|
-
const width = parseNumber(widthValue, "viewport width");
|
|
36
|
-
const height = parseNumber(heightValue, "viewport height");
|
|
33
|
+
throw new Error(`${name} must be x,y,width,height`);
|
|
34
|
+
const [x, y, width, height] = parts.map((part, index) => parseNumber(part, `${name} ${["x", "y", "width", "height"][index]}`));
|
|
37
35
|
if (width <= 0 || height <= 0)
|
|
38
|
-
throw new Error(
|
|
36
|
+
throw new Error(`${name} width and height must be greater than zero`);
|
|
39
37
|
return { x, y, width, height };
|
|
40
38
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{ key: "id", label: "ID" },
|
|
45
|
-
{ key: "title", label: "Title" },
|
|
46
|
-
{ key: "version", label: "Version" },
|
|
47
|
-
]);
|
|
39
|
+
function list(value) {
|
|
40
|
+
const entries = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
41
|
+
return entries?.length ? entries : undefined;
|
|
48
42
|
}
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
id: result.board.id,
|
|
53
|
-
title: result.board.title,
|
|
54
|
-
version: result.board.version,
|
|
55
|
-
...result.counts,
|
|
56
|
-
background: background?.background?.kind ?? "default",
|
|
57
|
-
updatedAt: result.board.updatedAt,
|
|
58
|
-
}], [
|
|
59
|
-
{ key: "id", label: "ID" },
|
|
60
|
-
{ key: "title", label: "TITLE" },
|
|
61
|
-
{ key: "version", label: "VERSION" },
|
|
62
|
-
{ key: "items", label: "ITEMS" },
|
|
63
|
-
{ key: "connections", label: "CONNECTIONS" },
|
|
64
|
-
{ key: "effects", label: "EFFECTS" },
|
|
65
|
-
{ key: "compositions", label: "COMPOSITIONS" },
|
|
66
|
-
{ key: "background", label: "BACKGROUND" },
|
|
67
|
-
{ key: "updatedAt", label: "UPDATED" },
|
|
68
|
-
]);
|
|
43
|
+
async function boardClient(boards, target) {
|
|
44
|
+
const spaceId = await resolveSpace(boards);
|
|
45
|
+
return createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
|
|
69
46
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
47
|
+
/** A patch from an inline argument, a file, or stdin (`-`). */
|
|
48
|
+
async function readPatch(inline, input, maxBytes) {
|
|
49
|
+
if (inline !== undefined && input !== undefined)
|
|
50
|
+
throw new Error("Pass the patch inline or with --input, not both.");
|
|
51
|
+
if (inline !== undefined)
|
|
52
|
+
return parseBoardJsonObject(inline, "patch");
|
|
53
|
+
if (input !== undefined)
|
|
54
|
+
return (await readBoardJsonObject(input, maxBytes));
|
|
55
|
+
throw new Error("Pass a patch inline or with --input <file|->.");
|
|
78
56
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
57
|
+
function describeChanges(result) {
|
|
58
|
+
const parts = [
|
|
59
|
+
result.changed.board ? "board" : null,
|
|
60
|
+
result.changed.items.length ? `${result.changed.items.length} item${result.changed.items.length === 1 ? "" : "s"}` : null,
|
|
61
|
+
result.changed.animations.length ? `${result.changed.animations.length} animation${result.changed.animations.length === 1 ? "" : "s"}` : null,
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
return parts.length ? parts.join(", ") : "nothing";
|
|
81
64
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.filter((item) => typeof item === "string")
|
|
92
|
-
.join("/");
|
|
93
|
-
return detail ? [`${field}:${detail}`] : [];
|
|
94
|
-
}).join(", ");
|
|
65
|
+
function showApplied(result) {
|
|
66
|
+
if (result.status === "validated")
|
|
67
|
+
ok(`Valid: would change ${describeChanges(result)}`);
|
|
68
|
+
else if (result.status === "unchanged")
|
|
69
|
+
ok(`Unchanged at version ${result.version}`);
|
|
70
|
+
else
|
|
71
|
+
ok(`Applied version ${result.version}${result.replayed ? " (replayed)" : ""}: ${describeChanges(result)}`);
|
|
72
|
+
for (const diagnostic of result.diagnostics)
|
|
73
|
+
console.log(` ! ${diagnostic.path}: ${diagnostic.message}`);
|
|
95
74
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
75
|
+
function showPlayback(playback) {
|
|
76
|
+
if (!playback) {
|
|
77
|
+
ok("Stopped");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
table([{ ...playback, position: formatBoardTime(Math.round(playback.position)) }], [
|
|
81
|
+
{ key: "animationId", label: "Animation" },
|
|
82
|
+
{ key: "status", label: "Status" },
|
|
83
|
+
{ key: "position", label: "Position" },
|
|
84
|
+
{ key: "timeScale", label: "Speed" },
|
|
85
|
+
]);
|
|
98
86
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Resolve the mutually exclusive region flags.
|
|
101
|
-
*
|
|
102
|
-
* Selecting more than one is rejected rather than silently ranked: "why did
|
|
103
|
-
* --items win over --frame" is a worse experience than being told to pick one.
|
|
104
|
-
*/
|
|
87
|
+
/** One region flag at most: being told to pick beats guessing which one wins. */
|
|
105
88
|
function parseExportRegion(options) {
|
|
106
|
-
const chosen = [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
options.rect ? "--rect" : null,
|
|
110
|
-
].filter(Boolean);
|
|
111
|
-
if (chosen.length > 1) {
|
|
112
|
-
throw new Error(`Pick one region: ${chosen.join(", ")} cannot be combined`);
|
|
113
|
-
}
|
|
89
|
+
const chosen = [options.frame && "--frame", options.items && "--items", options.rect && "--rect"].filter(Boolean);
|
|
90
|
+
if (chosen.length > 1)
|
|
91
|
+
throw new Error(`Pick one region: ${chosen.join(", ")} cannot be combined.`);
|
|
114
92
|
if (options.frame)
|
|
115
93
|
return { kind: "frame", id: options.frame };
|
|
116
94
|
if (options.items) {
|
|
117
|
-
const ids = options.items
|
|
118
|
-
if (ids
|
|
119
|
-
throw new Error("--items needs at least one
|
|
95
|
+
const ids = list(options.items);
|
|
96
|
+
if (!ids)
|
|
97
|
+
throw new Error("--items needs at least one item id.");
|
|
120
98
|
return { kind: "items", ids };
|
|
121
99
|
}
|
|
122
|
-
if (options.rect)
|
|
123
|
-
|
|
124
|
-
if (!rect)
|
|
125
|
-
throw new Error("--rect must be x,y,width,height");
|
|
126
|
-
return { kind: "rect", rect };
|
|
127
|
-
}
|
|
100
|
+
if (options.rect)
|
|
101
|
+
return { kind: "rect", rect: parseRect(options.rect) };
|
|
128
102
|
return { kind: "all" };
|
|
129
103
|
}
|
|
104
|
+
const BOARD_EXPORT_FORMATS = ["png", "jpeg", "webp"];
|
|
105
|
+
const BOARD_VIDEO_FORMATS = ["mp4", "webm"];
|
|
106
|
+
function videoFormatFromPath(path) {
|
|
107
|
+
const lower = path.toLowerCase();
|
|
108
|
+
return lower.endsWith(".webm") ? "webm" : lower.endsWith(".mp4") ? "mp4" : null;
|
|
109
|
+
}
|
|
110
|
+
function formatFromPath(path) {
|
|
111
|
+
const lower = path.toLowerCase();
|
|
112
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
113
|
+
return "jpeg";
|
|
114
|
+
if (lower.endsWith(".webp"))
|
|
115
|
+
return "webp";
|
|
116
|
+
return "png";
|
|
117
|
+
}
|
|
130
118
|
function parseExportFormat(options, outPath) {
|
|
131
119
|
if (!options.format)
|
|
132
120
|
return formatFromPath(outPath);
|
|
133
121
|
const format = options.format.toLowerCase();
|
|
134
|
-
if (!BOARD_EXPORT_FORMATS.includes(format))
|
|
135
|
-
throw new Error(
|
|
136
|
-
}
|
|
122
|
+
if (!BOARD_EXPORT_FORMATS.includes(format))
|
|
123
|
+
throw new Error(`--format must be one of ${BOARD_EXPORT_FORMATS.join(", ")}.`);
|
|
137
124
|
return format;
|
|
138
125
|
}
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
throw new Error('--theme must be "dark" or "light"');
|
|
126
|
+
function parseChoice(value, name, choices) {
|
|
127
|
+
const chosen = (value ?? choices[0]);
|
|
128
|
+
if (!choices.includes(chosen))
|
|
129
|
+
throw new Error(`${name} must be ${choices.join(" or ")}.`);
|
|
130
|
+
return chosen;
|
|
145
131
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (value === "transparent")
|
|
150
|
-
return "transparent";
|
|
151
|
-
throw new Error('--background must be "paper" or "transparent"');
|
|
132
|
+
/** `frames/%04d.png` → `frames/0012.png`; a sequence needs a `%d` placeholder. */
|
|
133
|
+
export function framePath(pattern, index) {
|
|
134
|
+
return pattern.replace(/%(0?)(\d*)d/, (_match, zero, width) => String(index).padStart(Number(width || 0), zero ? "0" : " "));
|
|
152
135
|
}
|
|
153
136
|
function registerExportCommand(boards) {
|
|
154
137
|
withJson(boards.command("export <board>")
|
|
155
|
-
.description("Render a Board to an image
|
|
156
|
-
.requiredOption("-o, --out <file>", "Output file; extension
|
|
157
|
-
.option("--
|
|
158
|
-
.option("--
|
|
159
|
-
.option("--frame <
|
|
160
|
-
.option("--items <ids>", "Comma-separated
|
|
138
|
+
.description("Render a Board to an image, or an animation to a frame sequence")
|
|
139
|
+
.requiredOption("-o, --out <file>", "Output file; the extension picks the format. Sequences need %d, e.g. frames/%04d.png")
|
|
140
|
+
.option("--at <time>", "A moment (12.5s) or a range start:end:step (0:10s:100ms)")
|
|
141
|
+
.option("--animation <id>", "Animation the --at times refer to")
|
|
142
|
+
.option("--frame <item-id>", "Export one frame as a page")
|
|
143
|
+
.option("--items <ids>", "Comma-separated item ids")
|
|
161
144
|
.option("--rect <rect>", "World rect as x,y,width,height")
|
|
145
|
+
.option("--scale <factor>", "Output pixels per board unit", "2")
|
|
146
|
+
.option("--padding <units>", "Padding around the content in board units")
|
|
162
147
|
.option("--theme <mode>", "dark or light", "dark")
|
|
163
148
|
.option("--background <mode>", "paper or transparent", "paper")
|
|
164
|
-
.option("--format <format>", `Override format (${BOARD_EXPORT_FORMATS.join(", ")})`)
|
|
149
|
+
.option("--format <format>", `Override the format (${BOARD_EXPORT_FORMATS.join(", ")}, ${BOARD_VIDEO_FORMATS.join(", ")})`)
|
|
165
150
|
.option("--quality <q>", "JPEG/WebP quality from 0 to 1", "0.92")
|
|
166
|
-
.option("--
|
|
167
|
-
.option("--
|
|
168
|
-
.
|
|
151
|
+
.option("--fps <rate>", "Video frame rate; defaults to the --at step")
|
|
152
|
+
.option("--no-images", "Draw placeholders instead of downloading images")
|
|
153
|
+
.option("--force", "Replace existing output files"))
|
|
154
|
+
.addHelpText("after", `
|
|
155
|
+
Examples:
|
|
156
|
+
cohub boards export plan.board -o plan.png
|
|
157
|
+
cohub boards export plan.board --frame s2 -o slide-2.png
|
|
158
|
+
cohub boards export plan.board --animation lecture --at 4.5s -o moment.png
|
|
159
|
+
cohub boards export plan.board --animation lecture --at 0:6s:40ms -o frames/%04d.png`)
|
|
160
|
+
.action(async (target, options) => {
|
|
169
161
|
try {
|
|
170
162
|
const out = options.out;
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
const times = options.at ? parseBoardTimes(options.at) : undefined;
|
|
164
|
+
const pathVideoFormat = videoFormatFromPath(out);
|
|
165
|
+
const requestedFormat = options.format?.toLowerCase();
|
|
166
|
+
if (requestedFormat && ![...BOARD_EXPORT_FORMATS, ...BOARD_VIDEO_FORMATS].includes(requestedFormat))
|
|
167
|
+
throw new Error(`--format must be one of ${[...BOARD_EXPORT_FORMATS, ...BOARD_VIDEO_FORMATS].join(", ")}.`);
|
|
168
|
+
const videoFormat = requestedFormat
|
|
169
|
+
? BOARD_VIDEO_FORMATS.includes(requestedFormat) ? requestedFormat : null
|
|
170
|
+
: pathVideoFormat;
|
|
171
|
+
const sequence = (times?.length ?? 0) > 1;
|
|
172
|
+
if (sequence && !videoFormat && !/%0?\d*d/.test(out))
|
|
173
|
+
throw new Error("A range needs %d in --out, e.g. frames/%04d.png.");
|
|
174
|
+
if (videoFormat) {
|
|
175
|
+
if (!times || times.length < 2)
|
|
176
|
+
throw new Error("Video export needs an --at range.");
|
|
177
|
+
const step = times[1] - times[0];
|
|
178
|
+
const fps = options.fps ? parseNumber(options.fps, "--fps", { min: 0.01, max: 240 }) : 1000 / step;
|
|
179
|
+
if (existsSync(out) && !options.force)
|
|
180
|
+
throw new Error(`${out} already exists; use --force to replace it.`);
|
|
181
|
+
await mkdir(dirname(out), { recursive: true });
|
|
182
|
+
const { runBoardVideoExport } = await import("../board-export.js");
|
|
183
|
+
const result = await runBoardVideoExport({
|
|
184
|
+
spaceId: await resolveSpace(boards),
|
|
185
|
+
target,
|
|
186
|
+
region: parseExportRegion(options),
|
|
187
|
+
times,
|
|
188
|
+
...(options.animation ? { animation: options.animation } : {}),
|
|
189
|
+
scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
|
|
190
|
+
...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
|
|
191
|
+
colorScheme: parseChoice(options.theme, "--theme", ["dark", "light"]),
|
|
192
|
+
background: parseChoice(options.background, "--background", ["paper", "transparent"]),
|
|
193
|
+
format: videoFormat,
|
|
194
|
+
fps,
|
|
195
|
+
output: out,
|
|
196
|
+
quality: parseNumber(options.quality ?? "0.92", "--quality", { min: 0, max: 1 }),
|
|
197
|
+
withImages: options.images !== false,
|
|
198
|
+
});
|
|
199
|
+
if (jsonRequested(options))
|
|
200
|
+
return outJson({ file: out, ...result });
|
|
201
|
+
ok(`Exported ${result.width}×${result.height} ${videoFormat.toUpperCase()} to ${out}`);
|
|
202
|
+
for (const warning of result.warnings)
|
|
203
|
+
console.log(` ! ${warning}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const { runBoardExport } = await import("../board-export.js");
|
|
173
207
|
const result = await runBoardExport({
|
|
174
208
|
spaceId: await resolveSpace(boards),
|
|
175
|
-
target
|
|
209
|
+
target,
|
|
176
210
|
region: parseExportRegion(options),
|
|
177
|
-
|
|
178
|
-
...(options.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
colorScheme:
|
|
182
|
-
background:
|
|
211
|
+
...(times ? { times } : {}),
|
|
212
|
+
...(options.animation ? { animation: options.animation } : {}),
|
|
213
|
+
scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
|
|
214
|
+
...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
|
|
215
|
+
colorScheme: parseChoice(options.theme, "--theme", ["dark", "light"]),
|
|
216
|
+
background: parseChoice(options.background, "--background", ["paper", "transparent"]),
|
|
183
217
|
format: parseExportFormat(options, out),
|
|
184
|
-
quality: parseNumber(options.quality ?? "0.92", "quality", { min: 0, max: 1 }),
|
|
218
|
+
quality: parseNumber(options.quality ?? "0.92", "--quality", { min: 0, max: 1 }),
|
|
185
219
|
withImages: options.images !== false,
|
|
186
220
|
});
|
|
187
|
-
if (!result)
|
|
188
|
-
return error("Nothing to export
|
|
221
|
+
if (!result)
|
|
222
|
+
return error("Nothing to export: the region contains no items.");
|
|
223
|
+
const written = [];
|
|
224
|
+
for (const [index, frame] of result.frames.entries()) {
|
|
225
|
+
const path = sequence ? framePath(out, index) : out;
|
|
226
|
+
await mkdir(dirname(path), { recursive: true });
|
|
227
|
+
await writeBoardOutput(path, frame.bytes, Boolean(options.force));
|
|
228
|
+
written.push({ path, time: frame.time, width: frame.width, height: frame.height, bytes: frame.bytes.length });
|
|
189
229
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
scale: result.scale,
|
|
197
|
-
items: result.itemCount,
|
|
198
|
-
format: result.format,
|
|
199
|
-
bytes: result.bytes.length,
|
|
200
|
-
warnings: result.warnings,
|
|
201
|
-
});
|
|
230
|
+
if (jsonRequested(options))
|
|
231
|
+
return outJson({ files: written, warnings: result.warnings });
|
|
232
|
+
const first = result.frames[0];
|
|
233
|
+
if (first) {
|
|
234
|
+
const what = sequence ? `${written.length} frames` : (first.format ?? "png").toUpperCase();
|
|
235
|
+
ok(`Exported ${first.width}×${first.height} ${what} to ${sequence ? dirname(out) || "." : out}`);
|
|
202
236
|
}
|
|
203
|
-
ok(`Exported ${result.width}×${result.height} ${result.format.toUpperCase()} to ${out}`);
|
|
204
237
|
for (const warning of result.warnings)
|
|
205
238
|
console.log(` ! ${warning}`);
|
|
206
239
|
}
|
|
@@ -209,194 +242,208 @@ function registerExportCommand(boards) {
|
|
|
209
242
|
}
|
|
210
243
|
});
|
|
211
244
|
}
|
|
245
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
212
246
|
export function registerBoards(program) {
|
|
213
247
|
const boards = program
|
|
214
248
|
.command("boards")
|
|
215
|
-
.description("
|
|
216
|
-
.
|
|
249
|
+
.description("Read, write, animate and export Boards (by id or .board path)")
|
|
250
|
+
.addHelpText("after", `
|
|
251
|
+
A Board is one JSON document: { board, items, animations }.
|
|
252
|
+
get read it
|
|
253
|
+
apply write it with a JSON Merge Patch; null deletes
|
|
254
|
+
schema the schema of any part, and what tracks can animate
|
|
255
|
+
examples starter documents
|
|
256
|
+
Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
217
257
|
withJson(boards.command("create <path>")
|
|
218
|
-
.description("Create a Board")
|
|
258
|
+
.description("Create a Board file, optionally with an initial document")
|
|
219
259
|
.option("--title <title>", "Board title")
|
|
220
|
-
.option("
|
|
221
|
-
.option("-
|
|
260
|
+
.option("-i, --input <file>", "Initial document JSON; - for stdin")
|
|
261
|
+
.option("--mutation-id <id>", "Stable id for safe retries"))
|
|
222
262
|
.addHelpText("after", `
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
cohub boards examples create > board.json
|
|
226
|
-
cohub boards create plan.board -i board.json`))
|
|
263
|
+
cohub boards examples lesson > lesson.json
|
|
264
|
+
cohub boards create lesson.board -i lesson.json`)
|
|
227
265
|
.action(async (path, options) => {
|
|
228
266
|
try {
|
|
229
|
-
const
|
|
230
|
-
? await readBoardJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES)
|
|
231
|
-
: {};
|
|
232
|
-
if ("path" in content || "title" in content) {
|
|
233
|
-
throw new Error("create input must not contain path or title; use the command argument and --title");
|
|
234
|
-
}
|
|
267
|
+
const document = options.input ? await readBoardJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES) : undefined;
|
|
235
268
|
const input = {
|
|
236
|
-
...content,
|
|
237
269
|
path,
|
|
238
|
-
mutationId: options.mutationId ??
|
|
239
|
-
(typeof content.mutationId === "string" ? content.mutationId : randomUUID()),
|
|
270
|
+
mutationId: options.mutationId ?? randomUUID(),
|
|
240
271
|
...(options.title ? { title: options.title } : {}),
|
|
272
|
+
...(document ? { document: document } : {}),
|
|
241
273
|
};
|
|
242
274
|
const result = await createClient().space(await resolveSpace(boards)).boards.create(input);
|
|
243
275
|
if (jsonRequested(options))
|
|
244
|
-
return outJson({
|
|
245
|
-
ok(`
|
|
246
|
-
|
|
276
|
+
return outJson({ path, id: result.id, title: result.title, version: result.version });
|
|
277
|
+
ok(`Created ${path}`);
|
|
278
|
+
table([{ path, id: result.id, title: result.title, version: result.version }], [
|
|
279
|
+
{ key: "path", label: "Path" },
|
|
280
|
+
{ key: "id", label: "ID" },
|
|
281
|
+
{ key: "title", label: "Title" },
|
|
282
|
+
{ key: "version", label: "Version" },
|
|
283
|
+
]);
|
|
247
284
|
}
|
|
248
285
|
catch (cause) {
|
|
249
286
|
handleHttp(cause);
|
|
250
287
|
}
|
|
251
288
|
});
|
|
252
|
-
|
|
253
|
-
.
|
|
254
|
-
.
|
|
289
|
+
boards.command("get <board>")
|
|
290
|
+
.description("Print the Board document as JSON")
|
|
291
|
+
.option("--only <sections>", "items, animations, or both (comma-separated)")
|
|
292
|
+
.option("--items <ids>", "Only these items")
|
|
293
|
+
.option("--within <frame-id>", "A frame and everything inside it")
|
|
294
|
+
.option("--rect <rect>", "Items intersecting x,y,width,height")
|
|
295
|
+
.option("--animations <ids>", "Only these animations")
|
|
296
|
+
.option("--limit <n>", "Page size for items")
|
|
297
|
+
.option("--cursor <cursor>", "Continue from a previous page's next")
|
|
255
298
|
.addHelpText("after", `
|
|
256
|
-
|
|
257
|
-
cohub boards
|
|
258
|
-
cohub boards
|
|
259
|
-
cohub boards effects list <board> --json
|
|
260
|
-
cohub boards compositions list <board> --json
|
|
261
|
-
Apply multiple changes atomically:
|
|
262
|
-
cohub boards batch <board> --input changes.json --dry-run`))
|
|
299
|
+
cohub boards get plan.board
|
|
300
|
+
cohub boards get plan.board --within s2
|
|
301
|
+
cohub boards get plan.board --only animations`)
|
|
263
302
|
.action(async (target, options) => {
|
|
264
303
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
304
|
+
const only = list(options.only);
|
|
305
|
+
if (only?.some((section) => section !== "items" && section !== "animations"))
|
|
306
|
+
throw new Error("--only takes items, animations, or both.");
|
|
307
|
+
const input = {
|
|
308
|
+
...(only ? { only: only } : {}),
|
|
309
|
+
...(list(options.items) ? { items: list(options.items) } : {}),
|
|
310
|
+
...(options.within ? { within: options.within } : {}),
|
|
311
|
+
...(options.rect ? { rect: parseRect(options.rect) } : {}),
|
|
312
|
+
...(list(options.animations) ? { animations: list(options.animations) } : {}),
|
|
313
|
+
...(options.limit ? { limit: parseNumber(options.limit, "--limit", { min: 1, integer: true }) } : {}),
|
|
314
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
315
|
+
};
|
|
316
|
+
outJson(await (await boardClient(boards, target)).get(input));
|
|
271
317
|
}
|
|
272
318
|
catch (cause) {
|
|
273
319
|
handleHttp(cause);
|
|
274
320
|
}
|
|
275
321
|
});
|
|
276
|
-
withJson(boards.command("
|
|
277
|
-
.description("
|
|
322
|
+
withJson(boards.command("apply <board> [patch]")
|
|
323
|
+
.description("Merge a patch into the Board: fields merge, null deletes")
|
|
324
|
+
.option("-i, --input <file>", "Patch JSON; - for stdin")
|
|
325
|
+
.option("--replace", "Make the document equal to the input instead of merging")
|
|
326
|
+
.option("--cascade", "Also delete children and tracks of deleted items")
|
|
327
|
+
.option("--dry-run", "Validate without writing")
|
|
328
|
+
.option("--base-version <n>", "Fail if the Board moved past this version")
|
|
329
|
+
.option("--mutation-id <id>", "Stable id; retries never apply twice"))
|
|
278
330
|
.addHelpText("after", `
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.action(async (target, options) => {
|
|
331
|
+
cohub boards apply plan.board '{"items":{"t1":{"type":"text","props":{"text":"Hi","fontSize":48}}}}'
|
|
332
|
+
cohub boards apply plan.board '{"items":{"t1":{"position":{"x":200}}}}'
|
|
333
|
+
cohub boards apply plan.board '{"items":{"t1":null}}'
|
|
334
|
+
cohub boards preset rise --targets a,b --animation intro | cohub boards apply plan.board -i -`)
|
|
335
|
+
.action(async (target, inline, options) => {
|
|
285
336
|
try {
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
337
|
+
const patch = await readPatch(inline, options.input, BOARD_TRANSACTION_INPUT_MAX_BYTES);
|
|
338
|
+
const result = await (await boardClient(boards, target)).apply(patch, {
|
|
339
|
+
...(options.replace ? { replace: true } : {}),
|
|
340
|
+
...(options.cascade ? { cascade: true } : {}),
|
|
341
|
+
...(options.dryRun ? { dryRun: true } : {}),
|
|
342
|
+
...(options.baseVersion === undefined ? {} : { baseVersion: parseNumber(options.baseVersion, "--base-version", { min: 0, integer: true }) }),
|
|
343
|
+
...(options.mutationId ? { mutationId: options.mutationId } : {}),
|
|
344
|
+
});
|
|
289
345
|
if (jsonRequested(options))
|
|
290
346
|
return outJson(result);
|
|
291
|
-
|
|
292
|
-
...capability,
|
|
293
|
-
renderers: capability.renderers?.join(", ") ?? "",
|
|
294
|
-
coordinates: capabilityUnits(capability.schema),
|
|
295
|
-
})), [
|
|
296
|
-
{ key: "kind", label: "Kind" },
|
|
297
|
-
{ key: "id", label: "ID" },
|
|
298
|
-
{ key: "version", label: "Version" },
|
|
299
|
-
{ key: "renderers", label: "Renderers" },
|
|
300
|
-
{ key: "coordinates", label: "Coordinates / units" },
|
|
301
|
-
{ key: "digest", label: "Digest" },
|
|
302
|
-
]);
|
|
303
|
-
console.log();
|
|
304
|
-
table([{
|
|
305
|
-
types: result.items.types.join(", "),
|
|
306
|
-
colors: result.items.colors.join(", "),
|
|
307
|
-
shapes: result.items.shapes.join(", "),
|
|
308
|
-
drawPoints: result.items.coordinates.drawPoints,
|
|
309
|
-
arrowEndpoints: result.items.coordinates.arrowEndpoints,
|
|
310
|
-
}], [
|
|
311
|
-
{ key: "types", label: "Item types" },
|
|
312
|
-
{ key: "colors", label: "Colors" },
|
|
313
|
-
{ key: "shapes", label: "Shapes" },
|
|
314
|
-
{ key: "drawPoints", label: "Draw points" },
|
|
315
|
-
{ key: "arrowEndpoints", label: "Arrow endpoints" },
|
|
316
|
-
]);
|
|
347
|
+
showApplied(result);
|
|
317
348
|
}
|
|
318
349
|
catch (cause) {
|
|
319
350
|
handleHttp(cause);
|
|
320
351
|
}
|
|
321
352
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
.
|
|
328
|
-
.option("--time-scale <scale>", "Playback speed from 0 to 4")
|
|
329
|
-
.option("--seed <seed>", "Deterministic playback seed")
|
|
330
|
-
.option("--command-id <id>", "Idempotency command ID"))
|
|
331
|
-
.action(async (target, compositionId, options) => {
|
|
353
|
+
withJson(boards.command("history <board>")
|
|
354
|
+
.description("List Board versions, or restore one")
|
|
355
|
+
.option("--before <version>", "Versions older than this")
|
|
356
|
+
.option("--limit <n>", "Number of versions", "20")
|
|
357
|
+
.option("--restore <version>", "Return the Board to this version as a new write"))
|
|
358
|
+
.action(async (target, options) => {
|
|
332
359
|
try {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
...(options.
|
|
342
|
-
|
|
360
|
+
const board = await boardClient(boards, target);
|
|
361
|
+
if (options.restore !== undefined) {
|
|
362
|
+
const result = await board.restore(parseNumber(options.restore, "--restore", { min: 0, integer: true }));
|
|
363
|
+
if (jsonRequested(options))
|
|
364
|
+
return outJson(result);
|
|
365
|
+
return showApplied(result);
|
|
366
|
+
}
|
|
367
|
+
const page = await board.history({
|
|
368
|
+
...(options.before ? { before: parseNumber(options.before, "--before", { min: 1, integer: true }) } : {}),
|
|
369
|
+
limit: parseNumber(options.limit ?? "20", "--limit", { min: 1, max: 500, integer: true }),
|
|
343
370
|
});
|
|
344
371
|
if (jsonRequested(options))
|
|
345
|
-
return outJson(
|
|
346
|
-
|
|
372
|
+
return outJson(page);
|
|
373
|
+
table(page.transactions.map((transaction) => ({
|
|
374
|
+
version: transaction.version,
|
|
375
|
+
at: transaction.createdAt,
|
|
376
|
+
actor: transaction.source?.via ?? transaction.actorId,
|
|
377
|
+
items: Object.keys(transaction.after?.items ?? {}).length,
|
|
378
|
+
animations: new Set([...Object.keys(transaction.after?.animations ?? {}), ...Object.keys(transaction.after?.tracks ?? {})]).size,
|
|
379
|
+
})), [
|
|
380
|
+
{ key: "version", label: "Version" },
|
|
381
|
+
{ key: "at", label: "At" },
|
|
382
|
+
{ key: "actor", label: "By" },
|
|
383
|
+
{ key: "items", label: "Items" },
|
|
384
|
+
{ key: "animations", label: "Animations" },
|
|
385
|
+
]);
|
|
386
|
+
if (page.nextBefore)
|
|
387
|
+
console.log(` More: --before ${page.nextBefore}`);
|
|
347
388
|
}
|
|
348
389
|
catch (cause) {
|
|
349
390
|
handleHttp(cause);
|
|
350
391
|
}
|
|
351
392
|
});
|
|
352
|
-
|
|
393
|
+
boards.command("schema [target]")
|
|
394
|
+
.description("Print JSON Schema for a part of a Board")
|
|
395
|
+
.addHelpText("after", `
|
|
396
|
+
Targets: ${BOARD_SCHEMA_TARGETS.join(", ")}
|
|
397
|
+
Without a target: units, item types, colors and every animatable property.`)
|
|
398
|
+
.action((target) => {
|
|
353
399
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
: await board.stop({ commandId: id, type: "stop", playbackId });
|
|
361
|
-
if (jsonRequested(options))
|
|
362
|
-
return outJson(result);
|
|
363
|
-
showPlayback(result);
|
|
400
|
+
if (!target)
|
|
401
|
+
return outJson({ ...boardSchemaOverview(), presets: listBoardPresets() });
|
|
402
|
+
const schema = boardJsonSchema(target);
|
|
403
|
+
if (!schema)
|
|
404
|
+
throw new Error(`Unknown target ${target}; expected ${BOARD_SCHEMA_TARGETS.join(", ")}.`);
|
|
405
|
+
outJson(schema);
|
|
364
406
|
}
|
|
365
407
|
catch (cause) {
|
|
366
408
|
handleHttp(cause);
|
|
367
409
|
}
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
.description("
|
|
371
|
-
.
|
|
372
|
-
.
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
.option("--
|
|
376
|
-
.
|
|
410
|
+
});
|
|
411
|
+
boards.command("preset <name>")
|
|
412
|
+
.description("Print tracks for a preset motion as a patch for apply")
|
|
413
|
+
.requiredOption("--targets <ids>", "Comma-separated item ids")
|
|
414
|
+
.requiredOption("--animation <id>", "Animation to add the tracks to")
|
|
415
|
+
.option("--at <time>", "Start of the first target", "0")
|
|
416
|
+
.option("--stagger <time>", "Delay added per target", "0")
|
|
417
|
+
.option("--duration <time>", "Length of each target's motion")
|
|
418
|
+
.option("--ease <ease>", "CSS easing, e.g. ease-out or cubic-bezier(.2,.8,.2,1)")
|
|
419
|
+
.addHelpText("after", `
|
|
420
|
+
Presets: ${BOARD_PRESET_NAMES.join(", ")}
|
|
421
|
+
cohub boards preset rise --targets a,b,c --animation intro --stagger 120ms | cohub boards apply plan.board -i -`)
|
|
422
|
+
.action((name, options) => {
|
|
377
423
|
try {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
if (!isBoardPresetName(name))
|
|
425
|
+
throw new Error(`Unknown preset ${name}; expected ${BOARD_PRESET_NAMES.join(", ")}.`);
|
|
426
|
+
const targets = list(options.targets);
|
|
427
|
+
if (!targets)
|
|
428
|
+
throw new Error("--targets needs at least one item id.");
|
|
429
|
+
const tracks = boardPresetTracks(name, {
|
|
430
|
+
targets,
|
|
431
|
+
at: parseBoardTime(options.at, "--at"),
|
|
432
|
+
stagger: parseBoardTime(options.stagger, "--stagger"),
|
|
433
|
+
...(options.duration ? { duration: parseBoardTime(options.duration, "--duration") } : {}),
|
|
434
|
+
...(options.ease ? { ease: options.ease } : {}),
|
|
385
435
|
});
|
|
386
|
-
|
|
387
|
-
return outJson(result);
|
|
388
|
-
showPlayback(result);
|
|
436
|
+
outJson({ animations: { [options.animation]: { tracks } } });
|
|
389
437
|
}
|
|
390
438
|
catch (cause) {
|
|
391
439
|
handleHttp(cause);
|
|
392
440
|
}
|
|
393
441
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
.action(playbackAction("stop"));
|
|
442
|
+
registerBoardExampleCommands(boards);
|
|
443
|
+
registerExportCommand(boards);
|
|
444
|
+
registerPlaybackCommands(boards);
|
|
398
445
|
withJson(boards.command("watch <board>")
|
|
399
|
-
.description("Stream Board
|
|
446
|
+
.description("Stream Board changes and playback as they happen"))
|
|
400
447
|
.action(async (target, options) => {
|
|
401
448
|
try {
|
|
402
449
|
const spaceId = await resolveSpace(boards);
|
|
@@ -404,17 +451,12 @@ Use boards examples for editable starter JSON.`))
|
|
|
404
451
|
const client = createRealtimeClient();
|
|
405
452
|
const board = client.space(spaceId).board(boardId);
|
|
406
453
|
if (!jsonRequested(options))
|
|
407
|
-
process.stderr.write(`
|
|
454
|
+
process.stderr.write(`Watching Board ${boardId}…\n`);
|
|
408
455
|
const offConnection = client.onConnection((state) => {
|
|
409
|
-
if (jsonRequested(options))
|
|
456
|
+
if (jsonRequested(options))
|
|
410
457
|
process.stdout.write(`${JSON.stringify({ type: "connection", ...state })}\n`);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const detail = state.state === "reconnecting" && state.attempt
|
|
414
|
-
? ` (attempt ${state.attempt})`
|
|
415
|
-
: "";
|
|
416
|
-
process.stderr.write(`${state.state}${detail}\n`);
|
|
417
|
-
}
|
|
458
|
+
else
|
|
459
|
+
process.stderr.write(`${state.state}${state.state === "reconnecting" && state.attempt ? ` (attempt ${state.attempt})` : ""}\n`);
|
|
418
460
|
});
|
|
419
461
|
const offBoard = board.subscribe({
|
|
420
462
|
event(event) {
|
|
@@ -423,13 +465,12 @@ Use boards examples for editable starter JSON.`))
|
|
|
423
465
|
return;
|
|
424
466
|
}
|
|
425
467
|
if (event.type === "board.changed") {
|
|
426
|
-
|
|
468
|
+
const { changed } = event.payload;
|
|
469
|
+
process.stdout.write(`version ${event.payload.version} ${[changed.board ? "board" : "", ...changed.items, ...changed.animations].filter(Boolean).join(", ")}\n`);
|
|
427
470
|
}
|
|
428
471
|
else if (event.type === "board.playback.changed") {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
else if (event.type === "board.awareness.updated") {
|
|
432
|
-
process.stdout.write(`awareness ${event.payload.actorName} ${event.payload.update.type}\n`);
|
|
472
|
+
const { playback } = event.payload;
|
|
473
|
+
process.stdout.write(playback ? `${playback.status} ${playback.animationId} ${formatBoardTime(Math.round(playback.position))}\n` : "stopped\n");
|
|
433
474
|
}
|
|
434
475
|
},
|
|
435
476
|
});
|
|
@@ -445,3 +486,32 @@ Use boards examples for editable starter JSON.`))
|
|
|
445
486
|
});
|
|
446
487
|
return boards;
|
|
447
488
|
}
|
|
489
|
+
function registerPlaybackCommands(boards) {
|
|
490
|
+
const run = async (target, options, command) => {
|
|
491
|
+
try {
|
|
492
|
+
const playback = await command(await boardClient(boards, target));
|
|
493
|
+
if (jsonRequested(options))
|
|
494
|
+
return outJson({ playback });
|
|
495
|
+
showPlayback(playback);
|
|
496
|
+
}
|
|
497
|
+
catch (cause) {
|
|
498
|
+
handleHttp(cause);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
withJson(boards.command("play <board> <animation>")
|
|
502
|
+
.description("Play an animation for everyone viewing the Board")
|
|
503
|
+
.option("--at <time>", "Start position, e.g. 2.5s")
|
|
504
|
+
.option("--speed <factor>", "Playback speed, up to 4")
|
|
505
|
+
.option("--seed <seed>", "Seed for procedural effects"))
|
|
506
|
+
.action((target, animation, options) => run(target, options, (board) => board.play(animation, {
|
|
507
|
+
...(options.at ? { position: parseBoardTime(options.at, "--at") } : {}),
|
|
508
|
+
...(options.speed ? { timeScale: parseNumber(options.speed, "--speed", { min: Number.EPSILON, max: 4 }) } : {}),
|
|
509
|
+
...(options.seed ? { seed: options.seed } : {}),
|
|
510
|
+
})));
|
|
511
|
+
withJson(boards.command("pause <board>").description("Pause playback")).action((target, options) => run(target, options, (board) => board.pause()));
|
|
512
|
+
withJson(boards.command("resume <board>").description("Resume paused playback")).action((target, options) => run(target, options, (board) => board.resume()));
|
|
513
|
+
withJson(boards.command("seek <board> <time>").description("Move the playhead, e.g. 12.5s"))
|
|
514
|
+
.action((target, time, options) => run(target, options, (board) => board.seek(parseBoardTime(time))));
|
|
515
|
+
withJson(boards.command("next <board>").description("Continue past the marker a presentation holds at")).action((target, options) => run(target, options, (board) => board.next()));
|
|
516
|
+
withJson(boards.command("stop <board>").description("Stop playback")).action((target, options) => run(target, options, (board) => board.stop()));
|
|
517
|
+
}
|