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