@neta-art/cohub-cli 4.0.0 → 6.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,18 +1,18 @@
1
- import { boardCompositionApplyOperation, boardCompositionDeleteOperation, boardEffectDeleteOperation, boardEffectUpsertOperation, } from "@neta-art/cohub/board";
2
- import { BoardEffectSchema, parseBoardCompositionInput, } from "@neta-art/cohub";
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 { resolvedBoard, showUpdated, withJson, } from "./context.js";
4
+ import { mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
6
5
  export function registerBoardAnimationCommands(boards) {
7
6
  const effects = boards.command("effects").description("Manage Board effects");
8
7
  withJson(effects.command("list <board>").alias("ls").description("List effects"))
9
8
  .action(async (target, options) => {
10
9
  try {
11
10
  const board = await resolvedBoard(boards, target);
12
- const result = await board.inspect({ include: ["effects"] });
11
+ const result = await board.authoring({ include: ["effects"] });
12
+ const effects = result.effects ?? [];
13
13
  if (jsonRequested(options))
14
- return json(result.effects);
15
- table(result.effects, [
14
+ return json(effects);
15
+ table(effects, [
16
16
  { key: "id", label: "ID" },
17
17
  { key: "kind", label: "KIND" },
18
18
  { key: "enabled", label: "ENABLED" },
@@ -25,13 +25,19 @@ export function registerBoardAnimationCommands(boards) {
25
25
  });
26
26
  withJson(effects.command("apply <board>")
27
27
  .description("Atomically create or replace an effect")
28
- .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin"))
28
+ .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
29
+ .addHelpText("after", `
30
+ Create a template:
31
+ cohub boards examples effect pulse > effect.json
32
+
33
+ Discover supported effect kinds:
34
+ cohub boards capabilities <board> --json`))
29
35
  .action(async (target, options) => {
30
36
  try {
31
37
  const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
32
- const effect = BoardEffectSchema.omit({ boardId: true, revision: true }).parse(input);
38
+ const effect = parseBoardEffectInput(input);
33
39
  const board = await resolvedBoard(boards, target);
34
- showUpdated(await board.mutate({ build: () => [boardEffectUpsertOperation(effect)] }), options);
40
+ showUpdated(await mutateSemantic(board, [{ type: "effect.apply", effect }]), options);
35
41
  }
36
42
  catch (cause) {
37
43
  handleHttp(cause);
@@ -41,7 +47,7 @@ export function registerBoardAnimationCommands(boards) {
41
47
  .action(async (target, effectId, options) => {
42
48
  try {
43
49
  const board = await resolvedBoard(boards, target);
44
- showUpdated(await board.mutate({ build: () => [boardEffectDeleteOperation(effectId)] }), options);
50
+ showUpdated(await mutateSemantic(board, [{ type: "effect.delete", effectId }]), options);
45
51
  }
46
52
  catch (cause) {
47
53
  handleHttp(cause);
@@ -54,10 +60,11 @@ export function registerBoardAnimationCommands(boards) {
54
60
  .action(async (target, options) => {
55
61
  try {
56
62
  const board = await resolvedBoard(boards, target);
57
- const result = await board.inspect({ include: ["compositions"] });
63
+ const result = await board.authoring({ include: ["compositions"] });
64
+ const compositions = result.compositions ?? [];
58
65
  if (jsonRequested(options))
59
- return json(result.compositions);
60
- table(result.compositions.map((composition) => ({
66
+ return json(compositions);
67
+ table(compositions.map((composition) => ({
61
68
  id: composition.id,
62
69
  name: composition.name,
63
70
  duration: composition.timeline.duration,
@@ -81,8 +88,8 @@ export function registerBoardAnimationCommands(boards) {
81
88
  .action(async (target, compositionId, options) => {
82
89
  try {
83
90
  const board = await resolvedBoard(boards, target);
84
- const result = await board.inspect({ include: ["compositions"] });
85
- const composition = result.compositions.find((item) => item.id === compositionId);
91
+ const result = await board.authoring({ include: ["compositions"] });
92
+ const composition = (result.compositions ?? []).find((item) => item.id === compositionId);
86
93
  if (!composition)
87
94
  throw new Error(`Composition not found: ${compositionId}`);
88
95
  if (jsonRequested(options))
@@ -114,6 +121,8 @@ export function registerBoardAnimationCommands(boards) {
114
121
  Property changes use timeline.tracks with registered channels and keyframes.
115
122
  Procedural behavior such as text reveal, particles, and camera focus uses timeline.clips.
116
123
  Run boards capabilities to discover channels and clip schemas.
124
+ Create an editable template with:
125
+ cohub boards examples composition fade > intro.json
117
126
 
118
127
  Minimal fade composition:
119
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"}}}`))
@@ -122,9 +131,7 @@ Minimal fade composition:
122
131
  const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
123
132
  const composition = parseBoardCompositionInput(input);
124
133
  const board = await resolvedBoard(boards, target);
125
- showUpdated(await board.mutate({
126
- build: () => [boardCompositionApplyOperation(composition)],
127
- }), options);
134
+ showUpdated(await mutateSemantic(board, [{ type: "composition.apply", composition }]), options);
128
135
  }
129
136
  catch (cause) {
130
137
  handleHttp(cause);
@@ -134,9 +141,7 @@ Minimal fade composition:
134
141
  .action(async (target, compositionId, options) => {
135
142
  try {
136
143
  const board = await resolvedBoard(boards, target);
137
- showUpdated(await board.mutate({
138
- build: () => [boardCompositionDeleteOperation(compositionId)],
139
- }), options);
144
+ showUpdated(await mutateSemantic(board, [{ type: "composition.delete", compositionId }]), options);
140
145
  }
141
146
  catch (cause) {
142
147
  handleHttp(cause);
@@ -1,6 +1,6 @@
1
- import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE, boardAppearanceOperation, boardPlaybackPolicyOperation, boardTitleOperation, patchBoardAppearance, } from "@neta-art/cohub/board";
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.mutate({ build: () => [boardTitleOperation(title)] }), options);
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 result = await board.mutate({
45
- build(current) {
46
- const appearance = appearanceFrom(current.board.metadata);
47
- const background = options.reset
48
- ? { kind: "solid" }
49
- : options.color
50
- ? { kind: "solid", color: options.color }
51
- : {
52
- kind: "image",
53
- imageUrl: options.image,
54
- fit: options.fit,
55
- position: options.position,
56
- opacity,
57
- color: appearance.background.color,
58
- };
59
- return [boardAppearanceOperation(patchBoardAppearance(appearance, { background }))];
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) {
@@ -81,9 +81,13 @@ Examples:
81
81
  ? null
82
82
  : { compositionId: options.composition, delayMs };
83
83
  const board = await resolvedBoard(boards, target);
84
- showUpdated(await board.mutate({
85
- build: (current) => [boardPlaybackPolicyOperation(current.board.metadata, policy)],
86
- }), options);
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);
87
91
  }
88
92
  catch (cause) {
89
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<import("@neta-art/cohub").BoardClient>;
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
- ok(`Board updated to version ${result.board.version}`);
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
  }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ export declare const BOARD_EXAMPLE_KEYS: string[];
3
+ export declare function boardExample(kind: string, type?: string): unknown;
4
+ export declare function registerBoardExampleCommands(boards: Command): void;
@@ -0,0 +1,224 @@
1
+ import { json } from "../../output.js";
2
+ const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
3
+ const templates = {
4
+ "create": {
5
+ items: [{
6
+ id: "title",
7
+ type: "text",
8
+ frame,
9
+ props: { text: "Launch plan", fontSize: 32 },
10
+ style: { color: "brand" },
11
+ }],
12
+ connections: [],
13
+ effects: [],
14
+ compositions: [],
15
+ },
16
+ "item:text": {
17
+ id: "title",
18
+ type: "text",
19
+ frame,
20
+ props: { text: "Launch plan", fontSize: 32 },
21
+ style: { color: "brand" },
22
+ },
23
+ "item:image": {
24
+ id: "hero",
25
+ type: "image",
26
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
27
+ source: { kind: "space-file", path: "assets/hero.webp" },
28
+ props: {},
29
+ },
30
+ "item:geo": {
31
+ id: "goal",
32
+ type: "geo",
33
+ frame: { x: 120, y: 160, width: 280, height: 140, rotation: 0 },
34
+ props: { shape: "rounded", text: "Ship" },
35
+ style: { color: "green", fillOpacity: 0.12 },
36
+ },
37
+ "item:frame": {
38
+ id: "scene",
39
+ type: "frame",
40
+ frame: { x: 80, y: 60, width: 960, height: 540, rotation: 0 },
41
+ props: { label: "Scene 1" },
42
+ style: { color: "neutral" },
43
+ },
44
+ "item:draw": {
45
+ id: "stroke",
46
+ type: "draw",
47
+ frame: { x: 100, y: 100, width: 180, height: 100, rotation: 0 },
48
+ props: { points: [{ x: 0, y: 80, p: 0.5 }, { x: 90, y: 10, p: 0.8 }, { x: 180, y: 70, p: 0.5 }] },
49
+ style: { color: "violet", strokeWidth: 4 },
50
+ },
51
+ "item:arrow": {
52
+ id: "arrow",
53
+ type: "arrow",
54
+ frame: { x: 100, y: 100, width: 260, height: 100, rotation: 0 },
55
+ props: { start: { x: 116, y: 150 }, end: { x: 344, y: 150 }, bend: 0, arrowStart: false, arrowEnd: true, label: "next" },
56
+ style: { color: "brand", strokeWidth: 2.5 },
57
+ },
58
+ "item:video": {
59
+ id: "demo-video",
60
+ type: "video",
61
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
62
+ source: { kind: "space-file", path: "assets/demo.mp4" },
63
+ props: {},
64
+ },
65
+ "item:audio": {
66
+ id: "soundtrack",
67
+ type: "audio",
68
+ frame: { x: 120, y: 540, width: 480, height: 96, rotation: 0 },
69
+ source: { kind: "space-file", path: "assets/soundtrack.mp3" },
70
+ props: {},
71
+ },
72
+ "item:file": {
73
+ id: "brief",
74
+ type: "file",
75
+ frame: { x: 120, y: 160, width: 360, height: 220, rotation: 0 },
76
+ source: { kind: "space-file", path: "docs/brief.md" },
77
+ props: {},
78
+ },
79
+ "item:task": {
80
+ id: "task",
81
+ type: "task",
82
+ frame: { x: 120, y: 160, width: 420, height: 240, rotation: 0 },
83
+ props: { taskRunId: "task-run-id", snapshot: { taskType: "generation", status: "running", title: "Generate concept", artifactCount: 0, artifacts: [] } },
84
+ },
85
+ "effect:pulse": {
86
+ id: "pulse-title",
87
+ target: { type: "item", itemId: "title" },
88
+ kind: "effects.pulse",
89
+ kindVersion: 1,
90
+ enabled: true,
91
+ lifecycle: "when-visible",
92
+ timeOrigin: "visible",
93
+ layer: "front",
94
+ seed: "pulse-title",
95
+ params: { amount: 0.04, period: 1600 },
96
+ assetRefs: [],
97
+ metadata: {},
98
+ },
99
+ "effect:float": {
100
+ id: "float-hero",
101
+ target: { type: "item", itemId: "hero" },
102
+ kind: "effects.float",
103
+ kindVersion: 1,
104
+ enabled: true,
105
+ lifecycle: "when-visible",
106
+ timeOrigin: "visible",
107
+ layer: "front",
108
+ seed: "float-hero",
109
+ params: { distance: 8, period: 2200 },
110
+ assetRefs: [],
111
+ metadata: {},
112
+ },
113
+ "composition:fade": {
114
+ id: "intro",
115
+ name: "Intro",
116
+ timeline: {
117
+ duration: 800,
118
+ tracks: [{
119
+ id: "title-opacity",
120
+ target: { type: "item", itemId: "title" },
121
+ channel: "style.opacity",
122
+ channelVersion: 1,
123
+ interpolation: "linear",
124
+ fill: "both",
125
+ keyframes: [
126
+ { time: 0, value: 0 },
127
+ { time: 800, value: 1, easing: "ease-out-cubic" },
128
+ ],
129
+ metadata: {},
130
+ }],
131
+ clips: [],
132
+ markers: [],
133
+ },
134
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
135
+ metadata: {},
136
+ },
137
+ "composition:reveal": {
138
+ id: "reveal",
139
+ name: "Reveal",
140
+ timeline: {
141
+ duration: 900,
142
+ tracks: [{ id: "title-opacity", target: { type: "item", itemId: "title" }, channel: "style.opacity", channelVersion: 1, interpolation: "linear", fill: "both", keyframes: [{ time: 0, value: 0 }, { time: 600, value: 1 }], metadata: {} }],
143
+ clips: [{ id: "reveal-title", kind: "text.reveal", kindVersion: 1, target: { type: "item", itemId: "title" }, start: 0, duration: 600, layer: "content", fill: "both", easing: "ease-out-cubic", params: { mode: "words" }, assetRefs: [], seed: "reveal-title", metadata: {} }],
144
+ markers: [],
145
+ },
146
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
147
+ metadata: {},
148
+ },
149
+ "composition:motion-path": {
150
+ id: "motion",
151
+ name: "Motion path",
152
+ timeline: {
153
+ duration: 1200,
154
+ tracks: [],
155
+ clips: [{ id: "move-hero", kind: "motion.path", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, layer: "content", fill: "both", easing: "ease-in-out-cubic", params: { points: [{ x: 0, y: 0 }, { x: 160, y: -40 }, { x: 320, y: 0 }], orient: true }, assetRefs: [], seed: "move-hero", metadata: {} }],
156
+ markers: [],
157
+ },
158
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
159
+ metadata: {},
160
+ },
161
+ "composition:particles": {
162
+ id: "celebrate",
163
+ name: "Celebrate",
164
+ timeline: {
165
+ duration: 1000,
166
+ tracks: [],
167
+ clips: [{ id: "particles", kind: "effects.particles", kindVersion: 1, target: { type: "board" }, start: 0, duration: 1000, layer: "front", fill: "none", easing: "linear", params: { count: 420, bounds: { x: 0, y: 0, width: 800, height: 600 } }, assetRefs: [], seed: "particles", metadata: {} }],
168
+ markers: [],
169
+ },
170
+ playback: { loop: false, endBehavior: "reset", reducedMotion: { mode: "base" } },
171
+ metadata: {},
172
+ },
173
+ "composition:camera-focus": {
174
+ id: "tour",
175
+ name: "Tour",
176
+ timeline: {
177
+ duration: 1000,
178
+ tracks: [],
179
+ clips: [{
180
+ id: "focus-title",
181
+ kind: "camera.focus",
182
+ kindVersion: 1,
183
+ target: { type: "camera" },
184
+ start: 0,
185
+ duration: 700,
186
+ layer: "screen",
187
+ fill: "forwards",
188
+ easing: "ease-out-cubic",
189
+ params: { focus: { type: "item", itemId: "title" }, fit: "contain", padding: 32 },
190
+ assetRefs: [],
191
+ seed: "focus-title",
192
+ metadata: {},
193
+ }],
194
+ markers: [],
195
+ },
196
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
197
+ metadata: {},
198
+ },
199
+ };
200
+ export const BOARD_EXAMPLE_KEYS = Object.keys(templates);
201
+ export function boardExample(kind, type) {
202
+ const key = type ? `${kind}:${type}` : kind;
203
+ const template = templates[key];
204
+ if (!template)
205
+ throw new Error(`Unknown Board example: ${key}`);
206
+ return template;
207
+ }
208
+ export function registerBoardExampleCommands(boards) {
209
+ boards.command("examples <kind> [type]")
210
+ .description("Print editable semantic JSON templates")
211
+ .addHelpText("after", `
212
+ Kinds:
213
+ create
214
+ item text|image|video|audio|file|task|geo|frame|draw|arrow
215
+ effect pulse|float
216
+ composition fade|reveal|motion-path|particles|camera-focus
217
+
218
+ Examples:
219
+ cohub boards examples item text > item.json
220
+ cohub boards examples composition fade > intro.json`)
221
+ .action((kind, type) => {
222
+ json(boardExample(kind, type));
223
+ });
224
+ }
@@ -14,34 +14,29 @@ const baseVersion = (value, fallback) => {
14
14
  };
15
15
  async function execute(boards, target, command, options) {
16
16
  const board = await resolvedBoard(boards, target);
17
- const snapshot = await board.authoring();
17
+ const snapshot = await board.summary();
18
18
  const mutation = {
19
19
  mutationId: options.mutationId?.trim() || randomUUID(),
20
20
  baseVersion: baseVersion(options.baseVersion, snapshot.board.version),
21
+ dryRun: Boolean(options.dryRun),
21
22
  commands: [command],
22
23
  };
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
- }
24
+ // Server-side validation: schema, version, references, cascade rules — the
25
+ // same checks the real write would run, just without persisting anything.
35
26
  const receipt = await board.mutateSemantic(mutation);
36
27
  if (jsonRequested(options))
37
28
  return json(receipt);
29
+ if (options.dryRun) {
30
+ ok(`Validated against Board version ${receipt.board.version}; no changes written`);
31
+ return;
32
+ }
38
33
  ok(`${receipt.replayed ? "Replayed" : "Applied"} mutation at Board version ${receipt.board.version}`);
39
34
  }
40
35
  function mutationOptions(command) {
41
36
  return withJson(command)
42
37
  .option("--base-version <version>", "Expected Board version; defaults to latest")
43
38
  .option("--mutation-id <id>", "Stable id for safe retries")
44
- .option("--dry-run", "Prepare locally without server validation or writing");
39
+ .option("--dry-run", "Validate on the server (references, version, cascade) without writing");
45
40
  }
46
41
  export function registerBoardItemCommands(boards) {
47
42
  const items = boards.command("items").description("Author Board items with semantic JSON");
@@ -50,9 +45,10 @@ export function registerBoardItemCommands(boards) {
50
45
  try {
51
46
  const board = await resolvedBoard(boards, target);
52
47
  const snapshot = await board.authoring();
48
+ const items = snapshot.items ?? [];
53
49
  if (jsonRequested(options))
54
- return json(snapshot.items);
55
- table(snapshot.items.map((item) => ({
50
+ return json(items);
51
+ table(items.map((item) => ({
56
52
  id: item.id,
57
53
  type: item.type,
58
54
  x: item.frame.x,
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createBoardConnection } from "@neta-art/cohub/board";
3
+ import { mutateSemantic } from "./context.js";
3
4
  import { handleHttp } from "../../output.js";
4
5
  import { resolvedBoard, showUpdated, withJson, } from "./context.js";
5
6
  /** Connection shortcuts; Item authoring lives in ./items.ts. */
@@ -21,17 +22,15 @@ export function registerBoardNodeCommands(boards) {
21
22
  const board = await resolvedBoard(boards, target);
22
23
  const connection = createBoardConnection({
23
24
  id: options.id ?? randomUUID(),
24
- sourceNodeId: source,
25
- targetNodeId: destination,
25
+ sourceItemId: source,
26
+ targetItemId: destination,
26
27
  relation: options.relation,
27
28
  direction: direction,
28
29
  label: options.label,
29
30
  sourcePortId: options.sourcePort,
30
31
  targetPortId: options.targetPort,
31
32
  });
32
- showUpdated(await board.mutate({
33
- build: () => [{ type: "connection.create", payload: { connection } }],
34
- }), options);
33
+ showUpdated(await mutateSemantic(board, [{ type: "connection.create", connection }]), options);
35
34
  }
36
35
  catch (cause) {
37
36
  handleHttp(cause);
@@ -42,9 +41,7 @@ export function registerBoardNodeCommands(boards) {
42
41
  .action(async (target, connectionId, options) => {
43
42
  try {
44
43
  const board = await resolvedBoard(boards, target);
45
- showUpdated(await board.mutate({
46
- build: () => [{ type: "connection.delete", payload: { connectionId } }],
47
- }), options);
44
+ showUpdated(await mutateSemantic(board, [{ type: "connection.delete", connectionId }]), options);
48
45
  }
49
46
  catch (cause) {
50
47
  handleHttp(cause);
@@ -1,15 +1,10 @@
1
- import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
2
1
  import type { Command } from "commander";
3
2
  import { parseBoardJsonObject } from "../board-command-support.js";
4
- declare const INSPECT_SECTIONS: readonly ["nodes", "connections", "effects", "compositions", "playback"];
5
- type InspectSection = (typeof INSPECT_SECTIONS)[number];
6
3
  export declare const parseJsonObject: typeof parseBoardJsonObject;
7
- export declare function readJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
8
- export declare function parseInspectSections(value?: string): InspectSection[] | undefined;
9
- export declare function parseViewport(value?: string): BoardInspectInput["viewport"];
10
- export declare function createTransactionInput(input: Record<string, unknown>, options: {
11
- txId?: string;
12
- baseVersion?: string;
13
- }): BoardTransactionInput;
4
+ export declare function parseViewport(value?: string): {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ } | undefined;
14
10
  export declare function registerBoards(program: Command): Command;
15
- export {};