@neta-art/cohub-cli 3.12.0 → 4.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/dist/commands/board-domain.js +2 -0
- package/dist/commands/boards/animation.js +56 -138
- package/dist/commands/boards/appearance.js +4 -5
- package/dist/commands/boards/items.d.ts +2 -0
- package/dist/commands/boards/items.js +144 -0
- package/dist/commands/boards/nodes.d.ts +1 -0
- package/dist/commands/boards/nodes.js +3 -60
- package/dist/commands/boards.d.ts +1 -1
- package/dist/commands/boards.js +14 -16
- package/dist/commands/works.js +2 -2
- package/package.json +2 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { registerBoardAnimationCommands } from "./boards/animation.js";
|
|
2
2
|
import { registerBoardAppearanceCommands } from "./boards/appearance.js";
|
|
3
|
+
import { registerBoardItemCommands } from "./boards/items.js";
|
|
3
4
|
import { registerBoardNodeCommands } from "./boards/nodes.js";
|
|
4
5
|
export function registerBoardDomainCommands(boards) {
|
|
5
6
|
registerBoardAppearanceCommands(boards);
|
|
6
7
|
registerBoardNodeCommands(boards);
|
|
8
|
+
registerBoardItemCommands(boards);
|
|
7
9
|
registerBoardAnimationCommands(boards);
|
|
8
10
|
}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { boardCompositionApplyOperation, boardCompositionDeleteOperation, boardEffectDeleteOperation, boardEffectUpsertOperation, } from "@neta-art/cohub/board";
|
|
2
|
+
import { BoardEffectSchema, parseBoardCompositionInput, } from "@neta-art/cohub";
|
|
3
3
|
import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
|
|
4
4
|
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
|
-
}
|
|
5
|
+
import { resolvedBoard, showUpdated, withJson, } from "./context.js";
|
|
16
6
|
export function registerBoardAnimationCommands(boards) {
|
|
17
7
|
const effects = boards.command("effects").description("Manage Board effects");
|
|
18
8
|
withJson(effects.command("list <board>").alias("ls").description("List effects"))
|
|
@@ -33,17 +23,13 @@ export function registerBoardAnimationCommands(boards) {
|
|
|
33
23
|
handleHttp(cause);
|
|
34
24
|
}
|
|
35
25
|
});
|
|
36
|
-
withJson(effects.command("
|
|
37
|
-
.description("
|
|
38
|
-
.requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
|
|
39
|
-
.addHelpText("after", `
|
|
40
|
-
Minimal pulse effect:
|
|
41
|
-
{"id":"pulse-title","target":{"type":"node","nodeId":"title"},"kind":"effects.pulse","kindVersion":1,"lifecycle":"when-visible","timeOrigin":"visible","seed":"pulse-title"}
|
|
42
|
-
|
|
43
|
-
Run boards capabilities <board> to discover supported effect kinds.`))
|
|
26
|
+
withJson(effects.command("apply <board>")
|
|
27
|
+
.description("Atomically create or replace an effect")
|
|
28
|
+
.requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin"))
|
|
44
29
|
.action(async (target, options) => {
|
|
45
30
|
try {
|
|
46
|
-
const
|
|
31
|
+
const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
32
|
+
const effect = BoardEffectSchema.omit({ boardId: true, revision: true }).parse(input);
|
|
47
33
|
const board = await resolvedBoard(boards, target);
|
|
48
34
|
showUpdated(await board.mutate({ build: () => [boardEffectUpsertOperation(effect)] }), options);
|
|
49
35
|
}
|
|
@@ -61,18 +47,29 @@ Run boards capabilities <board> to discover supported effect kinds.`))
|
|
|
61
47
|
handleHttp(cause);
|
|
62
48
|
}
|
|
63
49
|
});
|
|
64
|
-
const
|
|
65
|
-
|
|
50
|
+
const compositions = boards
|
|
51
|
+
.command("compositions")
|
|
52
|
+
.description("Manage atomic Board animation compositions");
|
|
53
|
+
withJson(compositions.command("list <board>").alias("ls").description("List compositions"))
|
|
66
54
|
.action(async (target, options) => {
|
|
67
55
|
try {
|
|
68
56
|
const board = await resolvedBoard(boards, target);
|
|
69
|
-
const result = await board.inspect({ include: ["
|
|
57
|
+
const result = await board.inspect({ include: ["compositions"] });
|
|
70
58
|
if (jsonRequested(options))
|
|
71
|
-
return json(result.
|
|
72
|
-
table(result.
|
|
59
|
+
return json(result.compositions);
|
|
60
|
+
table(result.compositions.map((composition) => ({
|
|
61
|
+
id: composition.id,
|
|
62
|
+
name: composition.name,
|
|
63
|
+
duration: composition.timeline.duration,
|
|
64
|
+
tracks: composition.timeline.tracks.length,
|
|
65
|
+
clips: composition.timeline.clips.length,
|
|
66
|
+
revision: composition.revision,
|
|
67
|
+
})), [
|
|
73
68
|
{ key: "id", label: "ID" },
|
|
74
69
|
{ key: "name", label: "NAME" },
|
|
75
70
|
{ key: "duration", label: "DURATION" },
|
|
71
|
+
{ key: "tracks", label: "TRACKS" },
|
|
72
|
+
{ key: "clips", label: "CLIPS" },
|
|
76
73
|
{ key: "revision", label: "REVISION" },
|
|
77
74
|
]);
|
|
78
75
|
}
|
|
@@ -80,145 +77,66 @@ Run boards capabilities <board> to discover supported effect kinds.`))
|
|
|
80
77
|
handleHttp(cause);
|
|
81
78
|
}
|
|
82
79
|
});
|
|
83
|
-
withJson(
|
|
84
|
-
.action(async (target,
|
|
80
|
+
withJson(compositions.command("get <board> <composition-id>").description("Get one complete composition"))
|
|
81
|
+
.action(async (target, compositionId, options) => {
|
|
85
82
|
try {
|
|
86
83
|
const board = await resolvedBoard(boards, target);
|
|
87
|
-
const result = await board.inspect({ include: ["
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
throw new Error(`
|
|
91
|
-
const output = {
|
|
92
|
-
sequence,
|
|
93
|
-
clips: result.clips.filter((clip) => clip.sequenceId === sequenceId),
|
|
94
|
-
};
|
|
84
|
+
const result = await board.inspect({ include: ["compositions"] });
|
|
85
|
+
const composition = result.compositions.find((item) => item.id === compositionId);
|
|
86
|
+
if (!composition)
|
|
87
|
+
throw new Error(`Composition not found: ${compositionId}`);
|
|
95
88
|
if (jsonRequested(options))
|
|
96
|
-
return json(
|
|
97
|
-
table([
|
|
89
|
+
return json(composition);
|
|
90
|
+
table([{
|
|
91
|
+
id: composition.id,
|
|
92
|
+
name: composition.name,
|
|
93
|
+
duration: composition.timeline.duration,
|
|
94
|
+
tracks: composition.timeline.tracks.length,
|
|
95
|
+
clips: composition.timeline.clips.length,
|
|
96
|
+
revision: composition.revision,
|
|
97
|
+
}], [
|
|
98
98
|
{ key: "id", label: "ID" },
|
|
99
99
|
{ key: "name", label: "NAME" },
|
|
100
100
|
{ key: "duration", label: "DURATION" },
|
|
101
|
+
{ key: "tracks", label: "TRACKS" },
|
|
102
|
+
{ key: "clips", label: "CLIPS" },
|
|
101
103
|
{ key: "revision", label: "REVISION" },
|
|
102
104
|
]);
|
|
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
105
|
}
|
|
110
106
|
catch (cause) {
|
|
111
107
|
handleHttp(cause);
|
|
112
108
|
}
|
|
113
109
|
});
|
|
114
|
-
withJson(
|
|
115
|
-
.description("
|
|
116
|
-
.requiredOption("-i, --input <file>", "
|
|
110
|
+
withJson(compositions.command("apply <board>")
|
|
111
|
+
.description("Atomically create or replace a composition")
|
|
112
|
+
.requiredOption("-i, --input <file>", "BoardComposition JSON; use - for stdin")
|
|
117
113
|
.addHelpText("after", `
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
Property changes use timeline.tracks with registered channels and keyframes.
|
|
115
|
+
Procedural behavior such as text reveal, particles, and camera focus uses timeline.clips.
|
|
116
|
+
Run boards capabilities to discover channels and clip schemas.
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Edit an existing sequence:
|
|
125
|
-
cohub boards sequences get <board> intro --json > intro.json
|
|
126
|
-
cohub boards sequences upsert <board> -i intro.json`))
|
|
118
|
+
Minimal fade composition:
|
|
119
|
+
{"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
120
|
.action(async (target, options) => {
|
|
128
121
|
try {
|
|
129
122
|
const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
130
|
-
const
|
|
131
|
-
showUpdated(await board.mutate({ build: () => [boardSequenceUpsertOperation(input)] }), options);
|
|
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();
|
|
123
|
+
const composition = parseBoardCompositionInput(input);
|
|
179
124
|
const board = await resolvedBoard(boards, target);
|
|
180
125
|
showUpdated(await board.mutate({
|
|
181
|
-
|
|
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
|
-
},
|
|
126
|
+
build: () => [boardCompositionApplyOperation(composition)],
|
|
211
127
|
}), options);
|
|
212
128
|
}
|
|
213
129
|
catch (cause) {
|
|
214
130
|
handleHttp(cause);
|
|
215
131
|
}
|
|
216
132
|
});
|
|
217
|
-
withJson(
|
|
218
|
-
.action(async (target,
|
|
133
|
+
withJson(compositions.command("delete <board> <composition-id>").alias("rm").description("Delete a composition"))
|
|
134
|
+
.action(async (target, compositionId, options) => {
|
|
219
135
|
try {
|
|
220
136
|
const board = await resolvedBoard(boards, target);
|
|
221
|
-
showUpdated(await board.mutate({
|
|
137
|
+
showUpdated(await board.mutate({
|
|
138
|
+
build: () => [boardCompositionDeleteOperation(compositionId)],
|
|
139
|
+
}), options);
|
|
222
140
|
}
|
|
223
141
|
catch (cause) {
|
|
224
142
|
handleHttp(cause);
|
|
@@ -67,20 +67,19 @@ 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
84
|
showUpdated(await board.mutate({
|
|
86
85
|
build: (current) => [boardPlaybackPolicyOperation(current.board.metadata, policy)],
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { BoardAuthoringItemSchema, BoardItemPatchSchema, } from "@neta-art/cohub";
|
|
3
|
+
import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
|
|
4
|
+
import { handleHttp, json, jsonRequested, ok, table } from "../../output.js";
|
|
5
|
+
import { resolvedBoard, withJson, } from "./context.js";
|
|
6
|
+
const baseVersion = (value, fallback) => {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return fallback;
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
11
|
+
throw new Error("base version must be a non-negative integer");
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
};
|
|
15
|
+
async function execute(boards, target, command, options) {
|
|
16
|
+
const board = await resolvedBoard(boards, target);
|
|
17
|
+
const snapshot = await board.authoring();
|
|
18
|
+
const mutation = {
|
|
19
|
+
mutationId: options.mutationId?.trim() || randomUUID(),
|
|
20
|
+
baseVersion: baseVersion(options.baseVersion, snapshot.board.version),
|
|
21
|
+
commands: [command],
|
|
22
|
+
};
|
|
23
|
+
if (options.dryRun) {
|
|
24
|
+
const result = {
|
|
25
|
+
mutationId: mutation.mutationId,
|
|
26
|
+
status: "prepared",
|
|
27
|
+
board: { id: snapshot.board.id, version: snapshot.board.version },
|
|
28
|
+
commands: mutation.commands,
|
|
29
|
+
};
|
|
30
|
+
if (jsonRequested(options))
|
|
31
|
+
return json(result);
|
|
32
|
+
ok(`Prepared against Board version ${snapshot.board.version}; no server validation or write performed`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const receipt = await board.mutateSemantic(mutation);
|
|
36
|
+
if (jsonRequested(options))
|
|
37
|
+
return json(receipt);
|
|
38
|
+
ok(`${receipt.replayed ? "Replayed" : "Applied"} mutation at Board version ${receipt.board.version}`);
|
|
39
|
+
}
|
|
40
|
+
function mutationOptions(command) {
|
|
41
|
+
return withJson(command)
|
|
42
|
+
.option("--base-version <version>", "Expected Board version; defaults to latest")
|
|
43
|
+
.option("--mutation-id <id>", "Stable id for safe retries")
|
|
44
|
+
.option("--dry-run", "Prepare locally without server validation or writing");
|
|
45
|
+
}
|
|
46
|
+
export function registerBoardItemCommands(boards) {
|
|
47
|
+
const items = boards.command("items").description("Author Board items with semantic JSON");
|
|
48
|
+
withJson(items.command("list <board>").alias("ls").description("List semantic Board items"))
|
|
49
|
+
.action(async (target, options) => {
|
|
50
|
+
try {
|
|
51
|
+
const board = await resolvedBoard(boards, target);
|
|
52
|
+
const snapshot = await board.authoring();
|
|
53
|
+
if (jsonRequested(options))
|
|
54
|
+
return json(snapshot.items);
|
|
55
|
+
table(snapshot.items.map((item) => ({
|
|
56
|
+
id: item.id,
|
|
57
|
+
type: item.type,
|
|
58
|
+
x: item.frame.x,
|
|
59
|
+
y: item.frame.y,
|
|
60
|
+
width: item.frame.width,
|
|
61
|
+
height: item.frame.height,
|
|
62
|
+
})), [
|
|
63
|
+
{ key: "id", label: "ID" },
|
|
64
|
+
{ key: "type", label: "TYPE" },
|
|
65
|
+
{ key: "x", label: "X" },
|
|
66
|
+
{ key: "y", label: "Y" },
|
|
67
|
+
{ key: "width", label: "WIDTH" },
|
|
68
|
+
{ key: "height", label: "HEIGHT" },
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
handleHttp(cause);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
mutationOptions(items.command("create <board>")
|
|
76
|
+
.description("Create one item")
|
|
77
|
+
.requiredOption("-i, --input <file>", "Board Item JSON; use - for stdin")
|
|
78
|
+
.addHelpText("after", `
|
|
79
|
+
Minimal text item:
|
|
80
|
+
{"id":"title","type":"text","frame":{"x":120,"y":80,"width":320,"height":48,"rotation":0},"props":{"text":"Launch plan","fontSize":32},"style":{"color":"brand"}}`))
|
|
81
|
+
.action(async (target, options) => {
|
|
82
|
+
try {
|
|
83
|
+
const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
84
|
+
await execute(boards, target, {
|
|
85
|
+
type: "item.create",
|
|
86
|
+
item: BoardAuthoringItemSchema.parse(value),
|
|
87
|
+
}, options);
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
handleHttp(cause);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
mutationOptions(items.command("patch <board> <item-id>")
|
|
94
|
+
.description("Recursively merge item fields")
|
|
95
|
+
.requiredOption("-i, --input <file>", "Board Item patch JSON; use - for stdin")
|
|
96
|
+
.addHelpText("after", `
|
|
97
|
+
Objects merge recursively, arrays replace, and null clears optional fields.
|
|
98
|
+
Move and rename a text item without replacing its size or font:
|
|
99
|
+
{"frame":{"x":160,"y":120},"props":{"text":"Updated"}}`))
|
|
100
|
+
.action(async (target, itemId, options) => {
|
|
101
|
+
try {
|
|
102
|
+
const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
103
|
+
await execute(boards, target, {
|
|
104
|
+
type: "item.patch",
|
|
105
|
+
itemId,
|
|
106
|
+
patch: BoardItemPatchSchema.parse(value),
|
|
107
|
+
}, options);
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
handleHttp(cause);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
mutationOptions(items.command("replace <board> <item-id>")
|
|
114
|
+
.description("Replace one complete item")
|
|
115
|
+
.requiredOption("-i, --input <file>", "Complete Board Item JSON; use - for stdin"))
|
|
116
|
+
.action(async (target, itemId, options) => {
|
|
117
|
+
try {
|
|
118
|
+
const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
119
|
+
await execute(boards, target, {
|
|
120
|
+
type: "item.replace",
|
|
121
|
+
itemId,
|
|
122
|
+
item: BoardAuthoringItemSchema.parse(value),
|
|
123
|
+
}, options);
|
|
124
|
+
}
|
|
125
|
+
catch (cause) {
|
|
126
|
+
handleHttp(cause);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
mutationOptions(items.command("delete <board> <item-id>").alias("rm")
|
|
130
|
+
.description("Delete one item")
|
|
131
|
+
.option("--cascade", "Atomically remove relations, effects, and animation references"))
|
|
132
|
+
.action(async (target, itemId, options) => {
|
|
133
|
+
try {
|
|
134
|
+
await execute(boards, target, {
|
|
135
|
+
type: "item.delete",
|
|
136
|
+
itemId,
|
|
137
|
+
cascade: Boolean(options.cascade),
|
|
138
|
+
}, options);
|
|
139
|
+
}
|
|
140
|
+
catch (cause) {
|
|
141
|
+
handleHttp(cause);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -1,68 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
|
|
2
|
+
import { createBoardConnection } from "@neta-art/cohub/board";
|
|
4
3
|
import { handleHttp } from "../../output.js";
|
|
5
4
|
import { resolvedBoard, showUpdated, withJson, } from "./context.js";
|
|
5
|
+
/** Connection shortcuts; Item authoring lives in ./items.ts. */
|
|
6
6
|
export function registerBoardNodeCommands(boards) {
|
|
7
|
-
const nodes = boards.command("nodes").description("Create and update Board nodes");
|
|
8
|
-
withJson(nodes.command("add <board>")
|
|
9
|
-
.description("Add a node")
|
|
10
|
-
.requiredOption("-i, --input <file>", "BoardNodeSpec JSON; use - for stdin")
|
|
11
|
-
.addHelpText("after", `
|
|
12
|
-
Frame x/y/width/height use Board world units. Draw points and arrow endpoints are also world input.
|
|
13
|
-
Minimal text node:
|
|
14
|
-
{"id":"title","type":"text","frame":{"x":120,"y":80,"width":320,"height":48},"text":"Launch plan"}`))
|
|
15
|
-
.action(async (target, options) => {
|
|
16
|
-
try {
|
|
17
|
-
const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
18
|
-
const node = createBoardNode(input);
|
|
19
|
-
const board = await resolvedBoard(boards, target);
|
|
20
|
-
showUpdated(await board.mutate({ build: () => [boardNodeCreateOperation(node)] }), options);
|
|
21
|
-
}
|
|
22
|
-
catch (cause) {
|
|
23
|
-
handleHttp(cause);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
withJson(nodes.command("patch <board> <node-id>")
|
|
27
|
-
.description("Patch a node")
|
|
28
|
-
.requiredOption("-i, --input <file>", "BoardNodeInput field patch; use - for stdin")
|
|
29
|
-
.addHelpText("after", `
|
|
30
|
-
x/y are absolute Board world coordinates.
|
|
31
|
-
Move a node without changing its content:
|
|
32
|
-
{"x":160,"y":120}
|
|
33
|
-
|
|
34
|
-
Nested view, style, and data fields replace their complete stored object.`))
|
|
35
|
-
.action(async (target, nodeId, options) => {
|
|
36
|
-
try {
|
|
37
|
-
const patch = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
38
|
-
if ("nodeId" in patch)
|
|
39
|
-
throw new Error("node patch must not contain nodeId");
|
|
40
|
-
const board = await resolvedBoard(boards, target);
|
|
41
|
-
showUpdated(await board.mutate({
|
|
42
|
-
build: () => [boardNodePatchOperation(nodeId, patch)],
|
|
43
|
-
}), options);
|
|
44
|
-
}
|
|
45
|
-
catch (cause) {
|
|
46
|
-
handleHttp(cause);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
withJson(nodes.command("remove <board> <node-id>")
|
|
50
|
-
.alias("rm")
|
|
51
|
-
.description("Remove a node and its connections"))
|
|
52
|
-
.action(async (target, nodeId, options) => {
|
|
53
|
-
try {
|
|
54
|
-
const board = await resolvedBoard(boards, target);
|
|
55
|
-
showUpdated(await board.mutate({
|
|
56
|
-
include: ["connections"],
|
|
57
|
-
build: (current) => boardNodeDeleteOperations(nodeId, current.connections),
|
|
58
|
-
}), options);
|
|
59
|
-
}
|
|
60
|
-
catch (cause) {
|
|
61
|
-
handleHttp(cause);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
7
|
withJson(boards.command("connect <board> <source> <target>")
|
|
65
|
-
.description("Connect two Board
|
|
8
|
+
.description("Connect two Board items")
|
|
66
9
|
.option("--id <id>", "Connection id")
|
|
67
10
|
.option("--relation <relation>", "Relation type")
|
|
68
11
|
.option("--direction <direction>", "none, forward, backward, or both", "forward")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import { parseBoardJsonObject } from "../board-command-support.js";
|
|
4
|
-
declare const INSPECT_SECTIONS: readonly ["nodes", "connections", "effects", "
|
|
4
|
+
declare const INSPECT_SECTIONS: readonly ["nodes", "connections", "effects", "compositions", "playback"];
|
|
5
5
|
type InspectSection = (typeof INSPECT_SECTIONS)[number];
|
|
6
6
|
export declare const parseJsonObject: typeof parseBoardJsonObject;
|
|
7
7
|
export declare function readJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
|
package/dist/commands/boards.js
CHANGED
|
@@ -5,7 +5,7 @@ import { registerBoardDomainCommands } from "./board-domain.js";
|
|
|
5
5
|
import { createClient, createRealtimeClient } from "../client.js";
|
|
6
6
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
7
7
|
import { resolveSpace } from "../space.js";
|
|
8
|
-
const INSPECT_SECTIONS = ["nodes", "connections", "effects", "
|
|
8
|
+
const INSPECT_SECTIONS = ["nodes", "connections", "effects", "compositions", "playback"];
|
|
9
9
|
export const parseJsonObject = parseBoardJsonObject;
|
|
10
10
|
export async function readJsonObject(source, maxBytes = BOARD_TRANSACTION_INPUT_MAX_BYTES) {
|
|
11
11
|
return readBoardJsonObject(source, maxBytes);
|
|
@@ -80,8 +80,7 @@ function showBoard(result) {
|
|
|
80
80
|
nodes: result.nodes.length,
|
|
81
81
|
connections: result.connections.length,
|
|
82
82
|
effects: result.effects.length,
|
|
83
|
-
|
|
84
|
-
clips: result.clips.length,
|
|
83
|
+
compositions: result.compositions.length,
|
|
85
84
|
},
|
|
86
85
|
], [
|
|
87
86
|
{ key: "id", label: "ID" },
|
|
@@ -90,8 +89,7 @@ function showBoard(result) {
|
|
|
90
89
|
{ key: "nodes", label: "Nodes" },
|
|
91
90
|
{ key: "connections", label: "Connections" },
|
|
92
91
|
{ key: "effects", label: "Effects" },
|
|
93
|
-
{ key: "
|
|
94
|
-
{ key: "clips", label: "Clips" },
|
|
92
|
+
{ key: "compositions", label: "Compositions" },
|
|
95
93
|
]);
|
|
96
94
|
}
|
|
97
95
|
function showSummary(result) {
|
|
@@ -110,7 +108,7 @@ function showSummary(result) {
|
|
|
110
108
|
{ key: "nodes", label: "NODES" },
|
|
111
109
|
{ key: "connections", label: "CONNECTIONS" },
|
|
112
110
|
{ key: "effects", label: "EFFECTS" },
|
|
113
|
-
{ key: "
|
|
111
|
+
{ key: "compositions", label: "COMPOSITIONS" },
|
|
114
112
|
{ key: "background", label: "BACKGROUND" },
|
|
115
113
|
{ key: "updatedAt", label: "UPDATED" },
|
|
116
114
|
]);
|
|
@@ -135,7 +133,7 @@ function showValidation(result) {
|
|
|
135
133
|
function showPlayback(result) {
|
|
136
134
|
table([result], [
|
|
137
135
|
{ key: "playbackId", label: "Playback ID" },
|
|
138
|
-
{ key: "
|
|
136
|
+
{ key: "compositionId", label: "Composition" },
|
|
139
137
|
{ key: "status", label: "Status" },
|
|
140
138
|
{ key: "position", label: "Position" },
|
|
141
139
|
{ key: "timeScale", label: "Time Scale" },
|
|
@@ -168,7 +166,7 @@ function registerTransactionCommand(boards, name) {
|
|
|
168
166
|
Input example:
|
|
169
167
|
{"baseVersion":12,"operations":[{"type":"board.patch","payload":{"patch":{"title":"Launch plan"}}}]}
|
|
170
168
|
|
|
171
|
-
Prefer semantic commands such as boards background, nodes, effects, or
|
|
169
|
+
Prefer semantic commands such as boards background, nodes, effects, or compositions for common edits.`))
|
|
172
170
|
.action(async (target, options) => {
|
|
173
171
|
try {
|
|
174
172
|
const transaction = createTransactionInput(await readJsonObject(options.input, BOARD_TRANSACTION_INPUT_MAX_BYTES), options);
|
|
@@ -181,8 +179,8 @@ Prefer semantic commands such as boards background, nodes, effects, or sequences
|
|
|
181
179
|
if (name === "validate")
|
|
182
180
|
showValidation(result);
|
|
183
181
|
else {
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
const receipt = result;
|
|
183
|
+
ok(`Board updated to version ${receipt.board.version}`);
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
186
|
catch (cause) {
|
|
@@ -317,7 +315,7 @@ export function registerBoards(program) {
|
|
|
317
315
|
.option("--mutation-id <id>", "Stable id for safe retries")
|
|
318
316
|
.option("-i, --input <file>", "BoardCreateInput fields; use - for stdin")
|
|
319
317
|
.addHelpText("after", `
|
|
320
|
-
For normal use, create an empty Board and add content with boards nodes, effects, and
|
|
318
|
+
For normal use, create an empty Board and add content with boards nodes, effects, and compositions.
|
|
321
319
|
--input is intended for bulk creation and accepts BoardCreateInput fields except path and title.`))
|
|
322
320
|
.action(async (path, options) => {
|
|
323
321
|
try {
|
|
@@ -347,7 +345,7 @@ For normal use, create an empty Board and add content with boards nodes, effects
|
|
|
347
345
|
withJson(boards.command("inspect <board>")
|
|
348
346
|
.alias("get")
|
|
349
347
|
.description("Inspect a Board")
|
|
350
|
-
.option("--include <sections>", "Comma-separated nodes,connections,effects,
|
|
348
|
+
.option("--include <sections>", "Comma-separated nodes,connections,effects,compositions,playback")
|
|
351
349
|
.option("--viewport <rect>", "Viewport as x,y,width,height"))
|
|
352
350
|
.action(async (target, options) => {
|
|
353
351
|
try {
|
|
@@ -416,20 +414,20 @@ For normal use, create an empty Board and add content with boards nodes, effects
|
|
|
416
414
|
registerTransactionCommand(boards, "apply");
|
|
417
415
|
registerBoardDomainCommands(boards);
|
|
418
416
|
registerExportCommand(boards);
|
|
419
|
-
withJson(boards.command("play <board> <
|
|
417
|
+
withJson(boards.command("play <board> <composition-id>")
|
|
420
418
|
.description("Start shared playback")
|
|
421
419
|
.option("--position <time>", "Initial position in milliseconds")
|
|
422
420
|
.option("--time-scale <scale>", "Playback speed from 0 to 4")
|
|
423
421
|
.option("--seed <seed>", "Deterministic playback seed")
|
|
424
422
|
.option("--command-id <id>", "Idempotency command ID"))
|
|
425
|
-
.action(async (target,
|
|
423
|
+
.action(async (target, compositionId, options) => {
|
|
426
424
|
try {
|
|
427
425
|
const spaceId = resolveSpace(boards);
|
|
428
426
|
const boardId = await resolveBoardId(spaceId, target);
|
|
429
427
|
const result = await createClient().space(spaceId).board(boardId).play({
|
|
430
428
|
commandId: commandId(options),
|
|
431
429
|
type: "play",
|
|
432
|
-
|
|
430
|
+
compositionId,
|
|
433
431
|
shared: true,
|
|
434
432
|
...(options.position === undefined ? {} : { position: parseNumber(options.position, "position", { min: 0 }) }),
|
|
435
433
|
...(options.timeScale === undefined ? {} : { timeScale: parseNumber(options.timeScale, "timeScale", { min: Number.EPSILON, max: 4 }) }),
|
|
@@ -520,7 +518,7 @@ For normal use, create an empty Board and add content with boards nodes, effects
|
|
|
520
518
|
process.stdout.write(`version ${event.payload.version} transaction ${event.payload.txId} operations ${event.payload.operations.length}\n`);
|
|
521
519
|
}
|
|
522
520
|
else if (event.type === "board.playback.changed") {
|
|
523
|
-
process.stdout.write(`${event.payload.status}
|
|
521
|
+
process.stdout.write(`${event.payload.status} composition ${event.payload.compositionId} position ${event.payload.position}\n`);
|
|
524
522
|
}
|
|
525
523
|
else {
|
|
526
524
|
process.stdout.write(`awareness ${event.payload.actorName} ${event.payload.update.type}\n`);
|
package/dist/commands/works.js
CHANGED
|
@@ -259,7 +259,7 @@ export function registerWorks(program) {
|
|
|
259
259
|
.option("--status <status>", "Work status: published, disabled")
|
|
260
260
|
.option("--visibility <visibility>", "Work visibility: public, space")
|
|
261
261
|
.option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
|
|
262
|
-
.option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
262
|
+
.option("--viewer-scope <scope>", "Scope viewers may request (taskrun.view, session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
263
263
|
.option("--meta <json>", "Work metadata as a JSON object")
|
|
264
264
|
.option("--hide-cohub-bar", "Hide the Cohub footer bar on the public work page")
|
|
265
265
|
.option("--show-cohub-bar", "Show the Cohub footer bar on the public work page")
|
|
@@ -338,7 +338,7 @@ export function registerWorks(program) {
|
|
|
338
338
|
.option("--status <status>", "Work status: published, disabled")
|
|
339
339
|
.option("--visibility <visibility>", "Work visibility: public, space")
|
|
340
340
|
.option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
|
|
341
|
-
.option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
341
|
+
.option("--viewer-scope <scope>", "Scope viewers may request (taskrun.view, session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
|
|
342
342
|
.option("--clear-work-scopes", "Clear work runtime scopes")
|
|
343
343
|
.option("--clear-viewer-scopes", "Clear viewer-requestable scopes")
|
|
344
344
|
.option("--meta <json>", "Work metadata as a JSON object")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"pixi.js": "^8.19.0",
|
|
21
21
|
"sharp": "^0.35.3",
|
|
22
|
-
"@neta-art/cohub": "
|
|
22
|
+
"@neta-art/cohub": "6.0.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|