@neta-art/cohub-cli 3.12.0 → 5.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 +23 -51
- package/dist/board-export.js +5 -5
- package/dist/commands/board-domain.js +4 -0
- package/dist/commands/boards/animation.js +65 -142
- package/dist/commands/boards/appearance.js +32 -29
- package/dist/commands/boards/context.d.ts +17 -1
- package/dist/commands/boards/context.js +38 -1
- package/dist/commands/boards/examples.d.ts +4 -0
- package/dist/commands/boards/examples.js +224 -0
- package/dist/commands/boards/items.d.ts +2 -0
- package/dist/commands/boards/items.js +140 -0
- package/dist/commands/boards/nodes.d.ts +1 -0
- package/dist/commands/boards/nodes.js +8 -68
- package/dist/commands/boards.d.ts +6 -11
- package/dist/commands/boards.js +46 -148
- package/dist/commands/cron-jobs.js +1 -1
- package/dist/commands/tasks.js +1 -1
- package/dist/commands/works.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -157,72 +157,44 @@ cohub -s <spaceId> boards capabilities <boardId>
|
|
|
157
157
|
cohub -s <spaceId> boards watch <boardId> --json
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
Pass
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
"nodeId": "goal",
|
|
169
|
-
"type": "geo",
|
|
170
|
-
"parentId": null,
|
|
171
|
-
"orderKey": null,
|
|
172
|
-
"x": 80,
|
|
173
|
-
"y": 80,
|
|
174
|
-
"width": 240,
|
|
175
|
-
"height": 120,
|
|
176
|
-
"rotation": 0,
|
|
177
|
-
"refKind": null,
|
|
178
|
-
"refPath": null,
|
|
179
|
-
"refUrl": null,
|
|
180
|
-
"view": {},
|
|
181
|
-
"style": {},
|
|
182
|
-
"data": {
|
|
183
|
-
"geo": "rectangle",
|
|
184
|
-
"text": "Ship",
|
|
185
|
-
"color": "green",
|
|
186
|
-
"fillOpacity": 0.12
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
]
|
|
190
|
-
}
|
|
160
|
+
Pass semantic items, effects, and compositions as JSON when creating a Board. The path and title stay explicit in the command. Generate editable templates instead of guessing fields:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cohub boards examples create > board-content.json
|
|
164
|
+
cohub boards examples item geo > item.json
|
|
165
|
+
cohub boards examples effect pulse > effect.json
|
|
166
|
+
cohub boards examples composition fade > intro.json
|
|
191
167
|
```
|
|
192
168
|
|
|
169
|
+
Inspect `boards capabilities --json` for supported Item types, animation channels, effect kinds, and coordinate spaces.
|
|
170
|
+
|
|
193
171
|
```bash
|
|
194
172
|
cohub -s <spaceId> boards create boards/plan.board \
|
|
195
173
|
--title "Plan" \
|
|
196
174
|
--input board-content.json
|
|
197
175
|
```
|
|
198
176
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
}
|
|
177
|
+
Generate editable semantic JSON templates instead of authoring storage transactions:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cohub boards examples item text > item.json
|
|
181
|
+
cohub boards items create <boardId> --input item.json
|
|
182
|
+
|
|
183
|
+
cohub boards examples composition fade > intro.json
|
|
184
|
+
cohub boards compositions apply <boardId> --input intro.json
|
|
185
|
+
|
|
186
|
+
cohub boards examples effect pulse > effect.json
|
|
187
|
+
cohub boards effects apply <boardId> --input effect.json
|
|
213
188
|
```
|
|
214
189
|
|
|
190
|
+
Use `--mutation-id` or `--command-id` when a script needs a stable idempotency key across retries.
|
|
191
|
+
|
|
215
192
|
```bash
|
|
216
|
-
cohub -s <spaceId> boards
|
|
217
|
-
cat transaction.json | cohub -s <spaceId> boards apply <boardId> --input - --json
|
|
218
|
-
cohub -s <spaceId> boards play <boardId> <sequenceId>
|
|
193
|
+
cohub -s <spaceId> boards play <boardId> <compositionId>
|
|
219
194
|
cohub -s <spaceId> boards seek <boardId> <playbackId> 400
|
|
220
195
|
cohub -s <spaceId> boards stop <boardId> <playbackId>
|
|
221
196
|
```
|
|
222
197
|
|
|
223
|
-
Pass `--tx-id` or `--command-id` when a script needs a stable idempotency key
|
|
224
|
-
across retries.
|
|
225
|
-
|
|
226
198
|
## Search
|
|
227
199
|
|
|
228
200
|
Search Spaces, Chats, and prior turns:
|
package/dist/board-export.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { boardAuthoringSnapshotToDocument, boardImageKeySource, imageAssetKey, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
13
13
|
import { createBoardHeadlessRenderer, exportBoardImageBytes, } from "@neta-art/cohub/board/headless";
|
|
14
14
|
import { resolveBoardId } from "./board-command-support.js";
|
|
15
15
|
import { createClient } from "./client.js";
|
|
@@ -64,11 +64,11 @@ export function resolveBundledFonts() {
|
|
|
64
64
|
export async function loadBoardDocument(spaceId, target) {
|
|
65
65
|
const client = createClient();
|
|
66
66
|
const boardId = await resolveBoardId(spaceId, target);
|
|
67
|
-
const
|
|
67
|
+
const snapshot = await client.space(spaceId).board(boardId).authoring({ include: ["items", "connections"] });
|
|
68
68
|
return {
|
|
69
|
-
document:
|
|
70
|
-
boardId:
|
|
71
|
-
title:
|
|
69
|
+
document: boardAuthoringSnapshotToDocument(snapshot),
|
|
70
|
+
boardId: snapshot.board.id,
|
|
71
|
+
title: snapshot.board.title ?? null,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { registerBoardAnimationCommands } from "./boards/animation.js";
|
|
2
2
|
import { registerBoardAppearanceCommands } from "./boards/appearance.js";
|
|
3
|
+
import { registerBoardExampleCommands } from "./boards/examples.js";
|
|
4
|
+
import { registerBoardItemCommands } from "./boards/items.js";
|
|
3
5
|
import { registerBoardNodeCommands } from "./boards/nodes.js";
|
|
4
6
|
export function registerBoardDomainCommands(boards) {
|
|
7
|
+
registerBoardExampleCommands(boards);
|
|
5
8
|
registerBoardAppearanceCommands(boards);
|
|
6
9
|
registerBoardNodeCommands(boards);
|
|
10
|
+
registerBoardItemCommands(boards);
|
|
7
11
|
registerBoardAnimationCommands(boards);
|
|
8
12
|
}
|
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BoardCameraFocusParamsSchema, boardEffectDeleteOperation, boardEffectUpsertOperation, boardSequenceDeleteOperation, boardSequenceUpsertOperation, } from "@neta-art/cohub/board";
|
|
1
|
+
import { parseBoardEffectInput, parseBoardCompositionInput, } from "@neta-art/cohub";
|
|
3
2
|
import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
|
|
4
3
|
import { handleHttp, json, jsonRequested, table } from "../../output.js";
|
|
5
|
-
import {
|
|
6
|
-
function rect(value) {
|
|
7
|
-
const parts = value.split(",").map(Number);
|
|
8
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
9
|
-
throw new Error("--rect must be x,y,width,height");
|
|
10
|
-
}
|
|
11
|
-
const [x, y, width, height] = parts;
|
|
12
|
-
if (width <= 0 || height <= 0)
|
|
13
|
-
throw new Error("--rect width and height must be positive");
|
|
14
|
-
return { x, y, width, height };
|
|
15
|
-
}
|
|
4
|
+
import { mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
|
|
16
5
|
export function registerBoardAnimationCommands(boards) {
|
|
17
6
|
const effects = boards.command("effects").description("Manage Board effects");
|
|
18
7
|
withJson(effects.command("list <board>").alias("ls").description("List effects"))
|
|
19
8
|
.action(async (target, options) => {
|
|
20
9
|
try {
|
|
21
10
|
const board = await resolvedBoard(boards, target);
|
|
22
|
-
const result = await board.
|
|
11
|
+
const result = await board.authoring({ include: ["effects"] });
|
|
12
|
+
const effects = result.effects ?? [];
|
|
23
13
|
if (jsonRequested(options))
|
|
24
|
-
return json(
|
|
25
|
-
table(
|
|
14
|
+
return json(effects);
|
|
15
|
+
table(effects, [
|
|
26
16
|
{ key: "id", label: "ID" },
|
|
27
17
|
{ key: "kind", label: "KIND" },
|
|
28
18
|
{ key: "enabled", label: "ENABLED" },
|
|
@@ -33,19 +23,21 @@ export function registerBoardAnimationCommands(boards) {
|
|
|
33
23
|
handleHttp(cause);
|
|
34
24
|
}
|
|
35
25
|
});
|
|
36
|
-
withJson(effects.command("
|
|
37
|
-
.description("
|
|
26
|
+
withJson(effects.command("apply <board>")
|
|
27
|
+
.description("Atomically create or replace an effect")
|
|
38
28
|
.requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
|
|
39
29
|
.addHelpText("after", `
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
Create a template:
|
|
31
|
+
cohub boards examples effect pulse > effect.json
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
Discover supported effect kinds:
|
|
34
|
+
cohub boards capabilities <board> --json`))
|
|
44
35
|
.action(async (target, options) => {
|
|
45
36
|
try {
|
|
46
|
-
const
|
|
37
|
+
const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
38
|
+
const effect = parseBoardEffectInput(input);
|
|
47
39
|
const board = await resolvedBoard(boards, target);
|
|
48
|
-
showUpdated(await board
|
|
40
|
+
showUpdated(await mutateSemantic(board, [{ type: "effect.apply", effect }]), options);
|
|
49
41
|
}
|
|
50
42
|
catch (cause) {
|
|
51
43
|
handleHttp(cause);
|
|
@@ -55,24 +47,36 @@ Run boards capabilities <board> to discover supported effect kinds.`))
|
|
|
55
47
|
.action(async (target, effectId, options) => {
|
|
56
48
|
try {
|
|
57
49
|
const board = await resolvedBoard(boards, target);
|
|
58
|
-
showUpdated(await board
|
|
50
|
+
showUpdated(await mutateSemantic(board, [{ type: "effect.delete", effectId }]), options);
|
|
59
51
|
}
|
|
60
52
|
catch (cause) {
|
|
61
53
|
handleHttp(cause);
|
|
62
54
|
}
|
|
63
55
|
});
|
|
64
|
-
const
|
|
65
|
-
|
|
56
|
+
const compositions = boards
|
|
57
|
+
.command("compositions")
|
|
58
|
+
.description("Manage atomic Board animation compositions");
|
|
59
|
+
withJson(compositions.command("list <board>").alias("ls").description("List compositions"))
|
|
66
60
|
.action(async (target, options) => {
|
|
67
61
|
try {
|
|
68
62
|
const board = await resolvedBoard(boards, target);
|
|
69
|
-
const result = await board.
|
|
63
|
+
const result = await board.authoring({ include: ["compositions"] });
|
|
64
|
+
const compositions = result.compositions ?? [];
|
|
70
65
|
if (jsonRequested(options))
|
|
71
|
-
return json(
|
|
72
|
-
table(
|
|
66
|
+
return json(compositions);
|
|
67
|
+
table(compositions.map((composition) => ({
|
|
68
|
+
id: composition.id,
|
|
69
|
+
name: composition.name,
|
|
70
|
+
duration: composition.timeline.duration,
|
|
71
|
+
tracks: composition.timeline.tracks.length,
|
|
72
|
+
clips: composition.timeline.clips.length,
|
|
73
|
+
revision: composition.revision,
|
|
74
|
+
})), [
|
|
73
75
|
{ key: "id", label: "ID" },
|
|
74
76
|
{ key: "name", label: "NAME" },
|
|
75
77
|
{ key: "duration", label: "DURATION" },
|
|
78
|
+
{ key: "tracks", label: "TRACKS" },
|
|
79
|
+
{ key: "clips", label: "CLIPS" },
|
|
76
80
|
{ key: "revision", label: "REVISION" },
|
|
77
81
|
]);
|
|
78
82
|
}
|
|
@@ -80,145 +84,64 @@ Run boards capabilities <board> to discover supported effect kinds.`))
|
|
|
80
84
|
handleHttp(cause);
|
|
81
85
|
}
|
|
82
86
|
});
|
|
83
|
-
withJson(
|
|
84
|
-
.action(async (target,
|
|
87
|
+
withJson(compositions.command("get <board> <composition-id>").description("Get one complete composition"))
|
|
88
|
+
.action(async (target, compositionId, options) => {
|
|
85
89
|
try {
|
|
86
90
|
const board = await resolvedBoard(boards, target);
|
|
87
|
-
const result = await board.
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
throw new Error(`
|
|
91
|
-
const output = {
|
|
92
|
-
sequence,
|
|
93
|
-
clips: result.clips.filter((clip) => clip.sequenceId === sequenceId),
|
|
94
|
-
};
|
|
91
|
+
const result = await board.authoring({ include: ["compositions"] });
|
|
92
|
+
const composition = (result.compositions ?? []).find((item) => item.id === compositionId);
|
|
93
|
+
if (!composition)
|
|
94
|
+
throw new Error(`Composition not found: ${compositionId}`);
|
|
95
95
|
if (jsonRequested(options))
|
|
96
|
-
return json(
|
|
97
|
-
table([
|
|
96
|
+
return json(composition);
|
|
97
|
+
table([{
|
|
98
|
+
id: composition.id,
|
|
99
|
+
name: composition.name,
|
|
100
|
+
duration: composition.timeline.duration,
|
|
101
|
+
tracks: composition.timeline.tracks.length,
|
|
102
|
+
clips: composition.timeline.clips.length,
|
|
103
|
+
revision: composition.revision,
|
|
104
|
+
}], [
|
|
98
105
|
{ key: "id", label: "ID" },
|
|
99
106
|
{ key: "name", label: "NAME" },
|
|
100
107
|
{ key: "duration", label: "DURATION" },
|
|
108
|
+
{ key: "tracks", label: "TRACKS" },
|
|
109
|
+
{ key: "clips", label: "CLIPS" },
|
|
101
110
|
{ key: "revision", label: "REVISION" },
|
|
102
111
|
]);
|
|
103
|
-
table(output.clips, [
|
|
104
|
-
{ key: "id", label: "CLIP" },
|
|
105
|
-
{ key: "kind", label: "KIND" },
|
|
106
|
-
{ key: "start", label: "START" },
|
|
107
|
-
{ key: "duration", label: "DURATION" },
|
|
108
|
-
]);
|
|
109
112
|
}
|
|
110
113
|
catch (cause) {
|
|
111
114
|
handleHttp(cause);
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
|
-
withJson(
|
|
115
|
-
.description("
|
|
116
|
-
.requiredOption("-i, --input <file>", "
|
|
117
|
+
withJson(compositions.command("apply <board>")
|
|
118
|
+
.description("Atomically create or replace a composition")
|
|
119
|
+
.requiredOption("-i, --input <file>", "BoardComposition JSON; use - for stdin")
|
|
117
120
|
.addHelpText("after", `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
Property changes use timeline.tracks with registered channels and keyframes.
|
|
122
|
+
Procedural behavior such as text reveal, particles, and camera focus uses timeline.clips.
|
|
123
|
+
Run boards capabilities to discover channels and clip schemas.
|
|
124
|
+
Create an editable template with:
|
|
125
|
+
cohub boards examples composition fade > intro.json
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
cohub boards sequences upsert <board> -i intro.json`))
|
|
127
|
+
Minimal fade composition:
|
|
128
|
+
{"id":"intro","name":"Intro","timeline":{"duration":800,"tracks":[{"id":"title-opacity","target":{"type":"item","itemId":"title"},"channel":"style.opacity","fill":"both","keyframes":[{"time":0,"value":0},{"time":800,"value":1,"easing":"ease-out-cubic"}]}],"clips":[],"markers":[]},"playback":{"loop":false,"endBehavior":"hold","reducedMotion":{"mode":"base"}}}`))
|
|
127
129
|
.action(async (target, options) => {
|
|
128
130
|
try {
|
|
129
131
|
const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
132
|
+
const composition = parseBoardCompositionInput(input);
|
|
130
133
|
const board = await resolvedBoard(boards, target);
|
|
131
|
-
showUpdated(await board
|
|
132
|
-
}
|
|
133
|
-
catch (cause) {
|
|
134
|
-
handleHttp(cause);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
withJson(sequences.command("camera-focus <board> <sequence-id>")
|
|
138
|
-
.description("Add or replace a semantic camera focus clip")
|
|
139
|
-
.option("--id <id>", "Stable clip id")
|
|
140
|
-
.option("--node <id>", "Focus one node")
|
|
141
|
-
.option("--nodes <ids>", "Focus comma-separated nodes")
|
|
142
|
-
.option("--frame <id>", "Focus a frame")
|
|
143
|
-
.option("--rect <rect>", "Board world rect as x,y,width,height")
|
|
144
|
-
.requiredOption("--at <ms>", "Clip start time")
|
|
145
|
-
.option("--duration <ms>", "Transition duration", "700")
|
|
146
|
-
.option("--padding <px>", "Screen padding in CSS pixels", "32")
|
|
147
|
-
.option("--fit <mode>", "contain or cover", "contain")
|
|
148
|
-
.option("--min-zoom <zoom>", "Minimum zoom multiplier")
|
|
149
|
-
.option("--max-zoom <zoom>", "Maximum zoom multiplier")
|
|
150
|
-
.option("--easing <name>", "Easing", "ease-out-cubic")
|
|
151
|
-
.addHelpText("after", `
|
|
152
|
-
Examples:
|
|
153
|
-
cohub boards sequences camera-focus plan.board intro --node hero --at 1200
|
|
154
|
-
cohub boards sequences camera-focus plan.board intro --rect 120,80,640,360 --at 2000 --duration 800`))
|
|
155
|
-
.action(async (target, sequenceId, options) => {
|
|
156
|
-
try {
|
|
157
|
-
const selected = [options.node, options.nodes, options.frame, options.rect].filter(Boolean);
|
|
158
|
-
if (selected.length !== 1)
|
|
159
|
-
throw new Error("Choose one of --node, --nodes, --frame, or --rect");
|
|
160
|
-
const focus = options.node
|
|
161
|
-
? { type: "node", nodeId: options.node }
|
|
162
|
-
: options.nodes
|
|
163
|
-
? { type: "nodes", nodeIds: options.nodes.split(",").map((id) => id.trim()).filter(Boolean) }
|
|
164
|
-
: options.frame
|
|
165
|
-
? { type: "frame", frameId: options.frame }
|
|
166
|
-
: { type: "rect", rect: rect(options.rect) };
|
|
167
|
-
const params = BoardCameraFocusParamsSchema.parse({
|
|
168
|
-
focus,
|
|
169
|
-
fit: options.fit,
|
|
170
|
-
padding: finite(options.padding, "padding"),
|
|
171
|
-
...(options.minZoom === undefined ? {} : { minZoom: finite(options.minZoom, "min zoom") }),
|
|
172
|
-
...(options.maxZoom === undefined ? {} : { maxZoom: finite(options.maxZoom, "max zoom") }),
|
|
173
|
-
});
|
|
174
|
-
const start = finite(options.at, "start");
|
|
175
|
-
const duration = finite(options.duration, "duration");
|
|
176
|
-
if (start < 0 || duration <= 0)
|
|
177
|
-
throw new Error("start must be non-negative and duration must be positive");
|
|
178
|
-
const clipId = options.id ?? randomUUID();
|
|
179
|
-
const board = await resolvedBoard(boards, target);
|
|
180
|
-
showUpdated(await board.mutate({
|
|
181
|
-
include: ["sequences", "clips"],
|
|
182
|
-
build(current) {
|
|
183
|
-
const currentSequence = current.sequences.find((sequence) => sequence.id === sequenceId);
|
|
184
|
-
if (!currentSequence)
|
|
185
|
-
throw new Error(`Sequence not found: ${sequenceId}`);
|
|
186
|
-
const { boardId: _boardId, revision: _revision, ...sequence } = currentSequence;
|
|
187
|
-
const clips = current.clips
|
|
188
|
-
.filter((clip) => clip.sequenceId === sequenceId && clip.id !== clipId)
|
|
189
|
-
.map(({ sequenceId: _sequenceId, ...clip }) => clip);
|
|
190
|
-
clips.push({
|
|
191
|
-
id: clipId,
|
|
192
|
-
kind: "camera.focus",
|
|
193
|
-
kindVersion: 1,
|
|
194
|
-
target: { type: "camera" },
|
|
195
|
-
start,
|
|
196
|
-
duration,
|
|
197
|
-
layer: "screen",
|
|
198
|
-
fill: "forwards",
|
|
199
|
-
easing: options.easing,
|
|
200
|
-
params,
|
|
201
|
-
keyframes: [],
|
|
202
|
-
assetRefs: [],
|
|
203
|
-
seed: clipId,
|
|
204
|
-
metadata: {},
|
|
205
|
-
});
|
|
206
|
-
return [boardSequenceUpsertOperation({
|
|
207
|
-
sequence: { ...sequence, duration: Math.max(sequence.duration, start + duration) },
|
|
208
|
-
clips,
|
|
209
|
-
})];
|
|
210
|
-
},
|
|
211
|
-
}), options);
|
|
134
|
+
showUpdated(await mutateSemantic(board, [{ type: "composition.apply", composition }]), options);
|
|
212
135
|
}
|
|
213
136
|
catch (cause) {
|
|
214
137
|
handleHttp(cause);
|
|
215
138
|
}
|
|
216
139
|
});
|
|
217
|
-
withJson(
|
|
218
|
-
.action(async (target,
|
|
140
|
+
withJson(compositions.command("delete <board> <composition-id>").alias("rm").description("Delete a composition"))
|
|
141
|
+
.action(async (target, compositionId, options) => {
|
|
219
142
|
try {
|
|
220
143
|
const board = await resolvedBoard(boards, target);
|
|
221
|
-
showUpdated(await board
|
|
144
|
+
showUpdated(await mutateSemantic(board, [{ type: "composition.delete", compositionId }]), options);
|
|
222
145
|
}
|
|
223
146
|
catch (cause) {
|
|
224
147
|
handleHttp(cause);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE,
|
|
1
|
+
import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE, patchBoardAppearance, } from "@neta-art/cohub/board";
|
|
2
2
|
import { handleHttp } from "../../output.js";
|
|
3
|
-
import { finite, resolvedBoard, showUpdated, withJson, } from "./context.js";
|
|
3
|
+
import { finite, mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
|
|
4
4
|
function appearanceFrom(metadata) {
|
|
5
5
|
const parsed = BoardAppearanceSchema.safeParse(metadata.appearance);
|
|
6
6
|
return parsed.success ? parsed.data : DEFAULT_BOARD_APPEARANCE;
|
|
@@ -10,7 +10,7 @@ export function registerBoardAppearanceCommands(boards) {
|
|
|
10
10
|
.action(async (target, title, options) => {
|
|
11
11
|
try {
|
|
12
12
|
const board = await resolvedBoard(boards, target);
|
|
13
|
-
showUpdated(await board
|
|
13
|
+
showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch: { title } }]), options);
|
|
14
14
|
}
|
|
15
15
|
catch (cause) {
|
|
16
16
|
handleHttp(cause);
|
|
@@ -41,24 +41,24 @@ Examples:
|
|
|
41
41
|
if (opacity < 0 || opacity > 1)
|
|
42
42
|
throw new Error("opacity must be between 0 and 1");
|
|
43
43
|
const board = await resolvedBoard(boards, target);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
const current = await board.summary();
|
|
45
|
+
const appearance = appearanceFrom(current.board.metadata);
|
|
46
|
+
const background = options.reset
|
|
47
|
+
? { kind: "solid" }
|
|
48
|
+
: options.color
|
|
49
|
+
? { kind: "solid", color: options.color }
|
|
50
|
+
: {
|
|
51
|
+
kind: "image",
|
|
52
|
+
imageUrl: options.image,
|
|
53
|
+
fit: options.fit,
|
|
54
|
+
position: options.position,
|
|
55
|
+
opacity,
|
|
56
|
+
color: appearance.background.color,
|
|
57
|
+
};
|
|
58
|
+
const result = await mutateSemantic(board, [{
|
|
59
|
+
type: "board.patch",
|
|
60
|
+
patch: { metadataPatch: { appearance: patchBoardAppearance(appearance, { background }) } },
|
|
61
|
+
}], { baseVersion: current.board.version });
|
|
62
62
|
showUpdated(result, options);
|
|
63
63
|
}
|
|
64
64
|
catch (cause) {
|
|
@@ -67,24 +67,27 @@ Examples:
|
|
|
67
67
|
});
|
|
68
68
|
withJson(boards.command("playback-policy <board>")
|
|
69
69
|
.description("Configure automatic Board playback")
|
|
70
|
-
.option("--
|
|
70
|
+
.option("--composition <id>", "Composition to play")
|
|
71
71
|
.option("--delay <ms>", "Delay before playback", "0")
|
|
72
|
-
.option("--loop", "Loop the sequence")
|
|
73
72
|
.option("--clear", "Remove the playback policy"))
|
|
74
73
|
.action(async (target, options) => {
|
|
75
74
|
try {
|
|
76
|
-
if (options.clear === Boolean(options.
|
|
77
|
-
throw new Error("Use --
|
|
75
|
+
if (options.clear === Boolean(options.composition))
|
|
76
|
+
throw new Error("Use --composition or --clear");
|
|
78
77
|
const delayMs = finite(options.delay, "delay");
|
|
79
78
|
if (delayMs < 0)
|
|
80
79
|
throw new Error("delay must be non-negative");
|
|
81
80
|
const policy = options.clear
|
|
82
81
|
? null
|
|
83
|
-
: {
|
|
82
|
+
: { compositionId: options.composition, delayMs };
|
|
84
83
|
const board = await resolvedBoard(boards, target);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const current = await board.summary();
|
|
85
|
+
const patch = policy
|
|
86
|
+
? { metadataPatch: { playback: policy } }
|
|
87
|
+
: { metadata: { ...current.board.metadata, playback: undefined } };
|
|
88
|
+
if (!policy)
|
|
89
|
+
delete patch.metadata.playback;
|
|
90
|
+
showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch }], { baseVersion: current.board.version }), options);
|
|
88
91
|
}
|
|
89
92
|
catch (cause) {
|
|
90
93
|
handleHttp(cause);
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import type { BoardClient, BoardSemanticCommand } from "@neta-art/cohub";
|
|
1
2
|
import type { Command } from "commander";
|
|
2
3
|
export type JsonOptions = {
|
|
3
4
|
json?: boolean;
|
|
4
5
|
};
|
|
5
6
|
export declare function withJson(command: Command): Command;
|
|
6
7
|
export declare function finite(value: string | undefined, name: string, fallback?: number): number;
|
|
7
|
-
export declare function resolvedBoard(boards: Command, target: string): Promise<
|
|
8
|
+
export declare function resolvedBoard(boards: Command, target: string): Promise<BoardClient>;
|
|
9
|
+
/**
|
|
10
|
+
* Send semantic commands with one automatic version-conflict retry: a racing
|
|
11
|
+
* edit between our version read and the submit is transparently rebased (same
|
|
12
|
+
* commands, fresh baseVersion, same id — a retry that lands is idempotent).
|
|
13
|
+
* `options.baseVersion` lets an agent chain mutations without re-reading the
|
|
14
|
+
* board between writes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mutateSemantic(board: BoardClient, commands: BoardSemanticCommand[], options?: {
|
|
17
|
+
baseVersion?: number;
|
|
18
|
+
mutationId?: string;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
}): Promise<import("@neta-art/cohub").BoardMutationReceipt>;
|
|
8
21
|
export declare function showUpdated(result: {
|
|
9
22
|
board: {
|
|
10
23
|
version: number;
|
|
11
24
|
};
|
|
25
|
+
status?: string;
|
|
26
|
+
outcome?: string;
|
|
27
|
+
replayed?: boolean;
|
|
12
28
|
}, options: JsonOptions): void;
|
|
@@ -18,8 +18,45 @@ export async function resolvedBoard(boards, target) {
|
|
|
18
18
|
const boardId = await resolveBoardId(spaceId, target);
|
|
19
19
|
return createClient().space(spaceId).board(boardId);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Send semantic commands with one automatic version-conflict retry: a racing
|
|
23
|
+
* edit between our version read and the submit is transparently rebased (same
|
|
24
|
+
* commands, fresh baseVersion, same id — a retry that lands is idempotent).
|
|
25
|
+
* `options.baseVersion` lets an agent chain mutations without re-reading the
|
|
26
|
+
* board between writes.
|
|
27
|
+
*/
|
|
28
|
+
export async function mutateSemantic(board, commands, options = {}) {
|
|
29
|
+
const baseVersion = options.baseVersion ?? (await board.summary()).board.version;
|
|
30
|
+
const send = (version) => board.mutateSemantic({
|
|
31
|
+
mutationId: options.mutationId,
|
|
32
|
+
baseVersion: version,
|
|
33
|
+
dryRun: options.dryRun ?? false,
|
|
34
|
+
commands,
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
return await send(baseVersion);
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (options.dryRun || !isVersionConflict(cause))
|
|
41
|
+
throw cause;
|
|
42
|
+
const retry = (await board.summary()).board.version;
|
|
43
|
+
return send(retry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isVersionConflict(error) {
|
|
47
|
+
return Boolean(error && typeof error === "object" &&
|
|
48
|
+
error.code === "VERSION_CONFLICT");
|
|
49
|
+
}
|
|
21
50
|
export function showUpdated(result, options) {
|
|
22
51
|
if (jsonRequested(options))
|
|
23
52
|
return json(result);
|
|
24
|
-
|
|
53
|
+
if (result.outcome === "dry-run") {
|
|
54
|
+
ok(`Validated against Board version ${result.board.version}; no changes written`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (result.outcome === "noop" || result.status === "validated") {
|
|
58
|
+
ok(`Board already matches the requested state at version ${result.board.version}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
ok(`Board updated to version ${result.board.version}${result.replayed ? " (replayed)" : ""}`);
|
|
25
62
|
}
|