@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.
- package/README.md +25 -2
- 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 +346 -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 +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/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/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
148
|
.option("--background <mode>", "paper or transparent", "paper")
|
|
173
|
-
.option("--format <format>", `Override format (${BOARD_EXPORT_FORMATS.join(", ")})`)
|
|
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.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
|
+
}
|
|
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.background, "--background", ["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,208 @@ 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
|
+
examples starter documents
|
|
256
|
+
Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
227
257
|
withJson(boards.command("create <path>")
|
|
228
|
-
.description("Create a Board")
|
|
258
|
+
.description("Create a Board file, optionally with an initial document")
|
|
229
259
|
.option("--title <title>", "Board title")
|
|
230
|
-
.option("
|
|
231
|
-
.option("-
|
|
260
|
+
.option("-i, --input <file>", "Initial document JSON; - for stdin")
|
|
261
|
+
.option("--mutation-id <id>", "Stable id for safe retries"))
|
|
232
262
|
.addHelpText("after", `
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
cohub boards examples create > board.json
|
|
236
|
-
cohub boards create plan.board -i board.json`))
|
|
263
|
+
cohub boards examples lesson > lesson.json
|
|
264
|
+
cohub boards create lesson.board -i lesson.json`)
|
|
237
265
|
.action(async (path, options) => {
|
|
238
266
|
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
|
-
}
|
|
267
|
+
const document = options.input ? await readBoardJsonObject(options.input, BOARD_CREATE_INPUT_MAX_BYTES) : undefined;
|
|
245
268
|
const input = {
|
|
246
|
-
...content,
|
|
247
269
|
path,
|
|
248
|
-
mutationId: options.mutationId ??
|
|
249
|
-
(typeof content.mutationId === "string" ? content.mutationId : randomUUID()),
|
|
270
|
+
mutationId: options.mutationId ?? randomUUID(),
|
|
250
271
|
...(options.title ? { title: options.title } : {}),
|
|
272
|
+
...(document ? { document: document } : {}),
|
|
251
273
|
};
|
|
252
274
|
const result = await createClient().space(await resolveSpace(boards)).boards.create(input);
|
|
253
275
|
if (jsonRequested(options))
|
|
254
|
-
return outJson({
|
|
255
|
-
ok(`
|
|
256
|
-
|
|
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
|
+
]);
|
|
257
284
|
}
|
|
258
285
|
catch (cause) {
|
|
259
286
|
handleHttp(cause);
|
|
260
287
|
}
|
|
261
288
|
});
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
.
|
|
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")
|
|
265
298
|
.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`))
|
|
299
|
+
cohub boards get plan.board
|
|
300
|
+
cohub boards get plan.board --within s2
|
|
301
|
+
cohub boards get plan.board --only animations`)
|
|
273
302
|
.action(async (target, options) => {
|
|
274
303
|
try {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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));
|
|
281
317
|
}
|
|
282
318
|
catch (cause) {
|
|
283
319
|
handleHttp(cause);
|
|
284
320
|
}
|
|
285
321
|
});
|
|
286
|
-
withJson(boards.command("
|
|
287
|
-
.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"))
|
|
288
330
|
.addHelpText("after", `
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.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) => {
|
|
295
336
|
try {
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
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
|
+
});
|
|
299
345
|
if (jsonRequested(options))
|
|
300
346
|
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
|
-
]);
|
|
347
|
+
showApplied(result);
|
|
327
348
|
}
|
|
328
349
|
catch (cause) {
|
|
329
350
|
handleHttp(cause);
|
|
330
351
|
}
|
|
331
352
|
});
|
|
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) => {
|
|
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) => {
|
|
342
359
|
try {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
...(options.
|
|
352
|
-
|
|
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 }),
|
|
353
370
|
});
|
|
354
371
|
if (jsonRequested(options))
|
|
355
|
-
return outJson(
|
|
356
|
-
|
|
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}`);
|
|
357
388
|
}
|
|
358
389
|
catch (cause) {
|
|
359
390
|
handleHttp(cause);
|
|
360
391
|
}
|
|
361
392
|
});
|
|
362
|
-
|
|
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) => {
|
|
363
399
|
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);
|
|
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);
|
|
374
406
|
}
|
|
375
407
|
catch (cause) {
|
|
376
408
|
handleHttp(cause);
|
|
377
409
|
}
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
.description("
|
|
381
|
-
.
|
|
382
|
-
.
|
|
383
|
-
|
|
384
|
-
.
|
|
385
|
-
.option("--
|
|
386
|
-
.
|
|
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) => {
|
|
387
423
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 } : {}),
|
|
395
435
|
});
|
|
396
|
-
|
|
397
|
-
return outJson(result);
|
|
398
|
-
showPlayback(result);
|
|
436
|
+
outJson({ animations: { [options.animation]: { tracks } } });
|
|
399
437
|
}
|
|
400
438
|
catch (cause) {
|
|
401
439
|
handleHttp(cause);
|
|
402
440
|
}
|
|
403
441
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.action(playbackAction("stop"));
|
|
442
|
+
registerBoardExampleCommands(boards);
|
|
443
|
+
registerExportCommand(boards);
|
|
444
|
+
registerPlaybackCommands(boards);
|
|
408
445
|
withJson(boards.command("watch <board>")
|
|
409
|
-
.description("Stream Board
|
|
446
|
+
.description("Stream Board changes and playback as they happen"))
|
|
410
447
|
.action(async (target, options) => {
|
|
411
448
|
try {
|
|
412
449
|
const spaceId = await resolveSpace(boards);
|
|
@@ -414,17 +451,12 @@ Use boards examples for editable starter JSON.`))
|
|
|
414
451
|
const client = createRealtimeClient();
|
|
415
452
|
const board = client.space(spaceId).board(boardId);
|
|
416
453
|
if (!jsonRequested(options))
|
|
417
|
-
process.stderr.write(`
|
|
454
|
+
process.stderr.write(`Watching Board ${boardId}…\n`);
|
|
418
455
|
const offConnection = client.onConnection((state) => {
|
|
419
|
-
if (jsonRequested(options))
|
|
456
|
+
if (jsonRequested(options))
|
|
420
457
|
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
|
-
}
|
|
458
|
+
else
|
|
459
|
+
process.stderr.write(`${state.state}${state.state === "reconnecting" && state.attempt ? ` (attempt ${state.attempt})` : ""}\n`);
|
|
428
460
|
});
|
|
429
461
|
const offBoard = board.subscribe({
|
|
430
462
|
event(event) {
|
|
@@ -433,13 +465,12 @@ Use boards examples for editable starter JSON.`))
|
|
|
433
465
|
return;
|
|
434
466
|
}
|
|
435
467
|
if (event.type === "board.changed") {
|
|
436
|
-
|
|
468
|
+
const { changed } = event.payload;
|
|
469
|
+
process.stdout.write(`version ${event.payload.version} ${[changed.board ? "board" : "", ...changed.items, ...changed.animations].filter(Boolean).join(", ")}\n`);
|
|
437
470
|
}
|
|
438
471
|
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`);
|
|
472
|
+
const { playback } = event.payload;
|
|
473
|
+
process.stdout.write(playback ? `${playback.status} ${playback.animationId} ${formatBoardTime(Math.round(playback.position))}\n` : "stopped\n");
|
|
443
474
|
}
|
|
444
475
|
},
|
|
445
476
|
});
|
|
@@ -455,3 +486,32 @@ Use boards examples for editable starter JSON.`))
|
|
|
455
486
|
});
|
|
456
487
|
return boards;
|
|
457
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
|
+
}
|