@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.
@@ -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 { randomUUID } from "node:crypto";
2
- import { BoardCameraFocusParamsSchema, boardEffectDeleteOperation, boardEffectUpsertOperation, boardSequenceDeleteOperation, boardSequenceUpsertOperation, } from "@neta-art/cohub/board";
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 { finite, resolvedBoard, showUpdated, withJson, } from "./context.js";
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("upsert <board>")
37
- .description("Create or replace an effect")
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 effect = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
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 sequences = boards.command("sequences").description("Manage Board sequences");
65
- withJson(sequences.command("list <board>").alias("ls").description("List sequences"))
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: ["sequences"] });
57
+ const result = await board.inspect({ include: ["compositions"] });
70
58
  if (jsonRequested(options))
71
- return json(result.sequences);
72
- table(result.sequences, [
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(sequences.command("get <board> <sequence-id>").description("Get a sequence and its clips"))
84
- .action(async (target, sequenceId, options) => {
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: ["sequences", "clips"] });
88
- const sequence = result.sequences.find((item) => item.id === sequenceId);
89
- if (!sequence)
90
- throw new Error(`Sequence not found: ${sequenceId}`);
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(output);
97
- table([sequence], [
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(sequences.command("upsert <board>")
115
- .description("Create or replace a sequence and clips")
116
- .requiredOption("-i, --input <file>", "{ sequence, clips } JSON; use - for stdin")
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
- Minimal sequence with one clip:
119
- {"sequence":{"id":"intro","name":"Intro","duration":1200,"seed":"intro"},"clips":[{"id":"reveal-title","kind":"text.reveal","kindVersion":1,"target":{"type":"node","nodeId":"title"},"start":0,"duration":600,"seed":"reveal-title"}]}
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
- Coordinate rules:
122
- node motion x/y and path points are Board-world offsets; camera.pan x/y are screen-pixel offsets.
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 board = await resolvedBoard(boards, target);
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
- 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
- },
126
+ build: () => [boardCompositionApplyOperation(composition)],
211
127
  }), options);
212
128
  }
213
129
  catch (cause) {
214
130
  handleHttp(cause);
215
131
  }
216
132
  });
217
- withJson(sequences.command("delete <board> <sequence-id>").alias("rm").description("Delete a sequence and its clips"))
218
- .action(async (target, sequenceId, options) => {
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({ build: () => [boardSequenceDeleteOperation(sequenceId)] }), options);
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("--sequence <id>", "Sequence to play")
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.sequence))
77
- throw new Error("Use --sequence or --clear");
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
- : { sequenceId: options.sequence, delayMs, loop: Boolean(options.loop) };
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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerBoardItemCommands(boards: Command): void;
@@ -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,2 +1,3 @@
1
1
  import type { Command } from "commander";
2
+ /** Connection shortcuts; Item authoring lives in ./items.ts. */
2
3
  export declare function registerBoardNodeCommands(boards: Command): void;
@@ -1,68 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { boardNodeCreateOperation, boardNodeDeleteOperations, boardNodePatchOperation, createBoardConnection, createBoardNode, } from "@neta-art/cohub/board";
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 nodes")
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", "sequences", "clips", "playback"];
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>>;
@@ -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", "sequences", "clips", "playback"];
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
- sequences: result.sequences.length,
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: "sequences", label: "Sequences" },
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: "sequences", label: "SEQUENCES" },
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: "sequenceId", label: "Sequence" },
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 sequences for common edits.`))
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
- ok(`Board updated to version ${result.board.version}`);
185
- showBoard(result);
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 sequences.
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,sequences,clips,playback")
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> <sequence-id>")
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, sequenceId, options) => {
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
- sequenceId,
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} sequence ${event.payload.sequenceId} position ${event.payload.position}\n`);
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`);
@@ -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.12.0",
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": "5.10.0"
22
+ "@neta-art/cohub": "6.0.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"