@neta-art/cohub-cli 6.2.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,53 +157,57 @@ cohub -s <spaceId> spaces activity 365 --json
157
157
 
158
158
  ## Boards
159
159
 
160
- Board commands use the selected Space and support `-h` at every level:
160
+ Board targets accept a Board ID or a `.board` path. Every command supports `-h` and `--json`:
161
161
 
162
162
  ```bash
163
163
  cohub boards -h
164
- cohub boards inspect -h
165
- cohub -s <spaceId> boards create boards/plan.board --title "Plan"
166
- cohub -s <spaceId> boards inspect <boardId> --json
164
+ cohub -s <spaceId> boards inspect boards/plan.board --json
165
+ cohub -s <spaceId> boards items list <boardId>
166
+ cohub -s <spaceId> boards connections list <boardId>
167
167
  cohub -s <spaceId> boards capabilities <boardId>
168
168
  cohub -s <spaceId> boards watch <boardId> --json
169
169
  ```
170
170
 
171
- Pass semantic items, effects, and compositions as JSON when creating a Board. The path and title stay explicit in the command. Generate editable templates instead of guessing fields:
171
+ Use targeted reads for large Boards:
172
172
 
173
173
  ```bash
174
- cohub boards examples create > board-content.json
175
- cohub boards examples item geo > item.json
176
- cohub boards examples effect pulse > effect.json
177
- cohub boards examples composition fade > intro.json
178
- ```
179
-
180
- Inspect `boards capabilities --json` for supported Item types, animation channels, effect kinds, and coordinate spaces.
181
-
182
- ```bash
183
- cohub -s <spaceId> boards create boards/plan.board \
184
- --title "Plan" \
185
- --input board-content.json
174
+ cohub -s <spaceId> boards items get <boardId> <itemId> --json
175
+ cohub -s <spaceId> boards connections get <boardId> <connectionId> --json
176
+ cohub -s <spaceId> boards effects get <boardId> <effectId> --json
177
+ cohub -s <spaceId> boards compositions get <boardId> <compositionId> --json
186
178
  ```
187
179
 
188
- Generate editable semantic JSON templates instead of authoring storage transactions:
180
+ Create semantic JSON from an example, then apply it to one resource:
189
181
 
190
182
  ```bash
191
183
  cohub boards examples item text > item.json
192
- cohub boards items create <boardId> --input item.json
184
+ cohub -s <spaceId> boards items create <boardId> --input item.json
193
185
 
194
186
  cohub boards examples composition fade > intro.json
195
- cohub boards compositions apply <boardId> --input intro.json
187
+ cohub -s <spaceId> boards compositions apply <boardId> --input intro.json
188
+ ```
189
+
190
+ Apply related changes atomically with one request. The batch contains semantic commands, not a full Board snapshot:
196
191
 
197
- cohub boards examples effect pulse > effect.json
198
- cohub boards effects apply <boardId> --input effect.json
192
+ ```json
193
+ {"commands":[
194
+ {"type":"item.patch","itemId":"title","patch":{"props":{"text":"Updated"}}},
195
+ {"type":"connection.create","connection":{"id":"title-agent","source":{"itemId":"title"},"target":{"itemId":"agent"}}}
196
+ ]}
197
+ ```
198
+
199
+ ```bash
200
+ cohub boards examples batch basic > changes.json
201
+ cohub -s <spaceId> boards batch <boardId> --input changes.json --dry-run
202
+ cohub -s <spaceId> boards batch <boardId> --input changes.json
199
203
  ```
200
204
 
201
- Use `--mutation-id` or `--command-id` when a script needs a stable idempotency key across retries.
205
+ Use `--base-version` and `--mutation-id` for controlled, retry-safe scripts. Use `--dry-run` to validate without writing. Playback is grouped under `playback`:
202
206
 
203
207
  ```bash
204
- cohub -s <spaceId> boards play <boardId> <compositionId>
205
- cohub -s <spaceId> boards seek <boardId> <playbackId> 400
206
- cohub -s <spaceId> boards stop <boardId> <playbackId>
208
+ cohub -s <spaceId> boards playback play <boardId> <compositionId>
209
+ cohub -s <spaceId> boards playback seek <boardId> <playbackId> 400
210
+ cohub -s <spaceId> boards playback stop <boardId> <playbackId>
207
211
  ```
208
212
 
209
213
  ## Search
@@ -285,9 +289,9 @@ overwrites files; it never deletes existing ones.
285
289
 
286
290
  Confirm before deleting files or directories.
287
291
 
288
- ## Works
292
+ ## Apps
289
293
 
290
- Publish and manage Work entries from a Space workspace. Public Work URLs require a username and a Space slug.
294
+ Publish and manage Apps from a Space workspace. Public App URLs require a username and a Space slug.
291
295
 
292
296
  `--file` and `--dir` take paths relative to the Space workspace — the same paths
293
297
  `spaces files ls` shows, not your local filesystem. To publish local build
@@ -297,51 +301,51 @@ Space-side path.
297
301
  ```bash
298
302
  cohub profile update --username <username>
299
303
  cohub spaces update <spaceId> --slug <space-slug>
300
- cohub -s <spaceId> works ls --json
301
- cohub works get <workId|url|username/space/work> --json
302
- cohub works stats <workId|url|username/space/work>
303
- cohub works download <workId|url|username/space/work> --output <path>
304
- cohub -s <spaceId> works publish demo --file dist/index.html
305
- cohub -s <spaceId> works publish site --dir dist
306
- cohub -s <spaceId> works publish app --port 3000
307
- cohub works publish-version <workId>
308
- cohub works versions <workId> --json
309
- cohub works rm <workId> --yes
304
+ cohub -s <spaceId> apps ls --json
305
+ cohub apps get <appId|url|username/space/app> --json
306
+ cohub apps stats <appId|url|username/space/app>
307
+ cohub apps download <appId|url|username/space/app> --output <path>
308
+ cohub -s <spaceId> apps publish demo --file dist/index.html
309
+ cohub -s <spaceId> apps publish site --dir dist
310
+ cohub -s <spaceId> apps publish app --port 3000
311
+ cohub apps publish-version <appId>
312
+ cohub apps versions <appId> --json
313
+ cohub apps rm <appId> --yes
310
314
  ```
311
315
 
312
- Resolve a published Work by public identity:
316
+ Resolve a published App by public identity:
313
317
 
314
318
  ```bash
315
- cohub works resolve <workSlug> --owner <username> --space-slug <spaceSlug>
319
+ cohub apps resolve <appSlug> --owner <username> --space-slug <spaceSlug>
316
320
  ```
317
321
 
318
- Use `--json` for machine-readable output. `works get`, `works stats`, and `works download` also accept `cohub://works/<username>/<space>/<work>` mention URIs. `works stats` reports total, 24-hour, 7-day, and 30-day views with a source breakdown. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port Works are not downloadable. The resolve command remains available for explicit slug-based lookup.
322
+ Use `--json` for machine-readable output. `apps get`, `apps stats`, and `apps download` also accept `app://<username>/<space>/<app>` mention URIs. `apps stats` reports total, 24-hour, 7-day, and 30-day views with a source breakdown. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port apps are not downloadable. The resolve command remains available for explicit slug-based lookup.
319
323
 
320
- Realtime rooms use a published Work's runtime identity, so they are available
324
+ Realtime rooms use a published App's runtime identity, so they are available
321
325
  through `client.app.realtime` in the SDK rather than as CLI commands.
322
326
 
323
327
  ## Drive the Cohub UI
324
328
 
325
- Show a file or Work preview in the Cohub tab that started the current work, and call
326
- methods the Work exposes.
329
+ Show a file or App preview in the Cohub tab that started the current session, and call
330
+ methods the App exposes.
327
331
 
328
332
  ```bash
329
- cohub desktop open <workId|url|cohub://works/...|username/space/work|file://path>
333
+ cohub desktop open <appId|url|app://...|username/space/app|file://path>
330
334
  cohub desktop open file://src/main.ts
331
- cohub desktop open work://alice/studio/launch
332
- cohub desktop open <work-or-file> --call selection.get
333
- cohub desktop open <work> --call board.focus --data '{"nodeId":"n1"}'
334
- cohub desktop open <work> --call report.build --input payload.json --json
335
+ cohub desktop open app://alice/studio/launch
336
+ cohub desktop open <app-or-file> --call selection.get
337
+ cohub desktop open <app> --call board.focus --data '{"nodeId":"n1"}'
338
+ cohub desktop open <app> --call report.build --input payload.json --json
335
339
  ```
336
340
 
337
- `ui preview` accepts `file://` Space-relative paths, `work://` Work references, and
341
+ `ui preview` accepts `file://` Space-relative paths, `app://` App references, and
338
342
  legacy bare targets. A bare target checks the current Space for a file first, then
339
- falls back to the same Work references as `works get`. Showing a preview is
343
+ falls back to the same App references as `apps get`. Showing a preview is
340
344
  idempotent: repeating it re-activates the same tab and refreshes any launch state
341
- carried by the reference. With `--call`, the command waits for the Work to announce readiness, invokes the method,
342
- and waits for the Work to complete the same UI command with `client.ui.reportResult()`.
345
+ carried by the reference. With `--call`, the command waits for the App to announce readiness, invokes the method,
346
+ and waits for the App to complete the same UI command with `client.ui.reportResult()`.
343
347
 
344
- Work authors decide what is callable by registering handlers inside the Work:
348
+ App authors decide what is callable by registering handlers inside the App:
345
349
 
346
350
  ```ts
347
351
  client.app.surface.handle("image.open", async (input, { commandId }) => {
@@ -349,7 +353,7 @@ client.app.surface.handle("image.open", async (input, { commandId }) => {
349
353
  });
350
354
  ```
351
355
 
352
- A Work answers only a Cohub app origin, so a third-party site that embeds it
356
+ An App answers only a Cohub app origin, so a third-party site that embeds it
353
357
  cannot invoke these methods.
354
358
 
355
359
  Retrying with the same `--command-id` re-delivers the command rather than
@@ -361,7 +365,7 @@ Delivery is at-least-once. Deduplication lives in the receiving tab's memory, so
361
365
  retry that spans a page reload can run a `--call` method a second time. Prefer
362
366
  methods that are safe to repeat.
363
367
 
364
- Commands reach only the frontend instance that originated the current work,
368
+ Commands reach only the frontend instance that originated the current session,
365
369
  resolved from request provenance (`X-Cohub-Source-Client`, propagated into the
366
370
  Sandbox as `COHUB_SOURCE_CLIENT_ID`). They cannot target another user's browser,
367
371
  and offer no DOM access or script evaluation. Pass `--client` to address a
@@ -414,7 +418,7 @@ Confirm before enabling, disabling, or deleting recurring scheduled prompts.
414
418
 
415
419
  Confirm before:
416
420
 
417
- - deleting files, directories, or Works
421
+ - deleting files, directories, or Apps
418
422
  - creating scheduled or recurring prompts with side effects
419
423
  - enabling, disabling, or deleting recurring scheduled prompts
420
424
  - changing access policies, member roles, or membership
@@ -1,12 +1,14 @@
1
1
  import { registerBoardAnimationCommands } from "./boards/animation.js";
2
2
  import { registerBoardAppearanceCommands } from "./boards/appearance.js";
3
3
  import { registerBoardExampleCommands } from "./boards/examples.js";
4
+ import { registerBoardBatchCommand } from "./boards/batch.js";
5
+ import { registerBoardConnectionCommands } from "./boards/connections.js";
4
6
  import { registerBoardItemCommands } from "./boards/items.js";
5
- import { registerBoardNodeCommands } from "./boards/nodes.js";
6
7
  export function registerBoardDomainCommands(boards) {
7
8
  registerBoardExampleCommands(boards);
8
9
  registerBoardAppearanceCommands(boards);
9
- registerBoardNodeCommands(boards);
10
+ registerBoardBatchCommand(boards);
11
+ registerBoardConnectionCommands(boards);
10
12
  registerBoardItemCommands(boards);
11
13
  registerBoardAnimationCommands(boards);
12
14
  }
@@ -1,14 +1,14 @@
1
1
  import { parseBoardEffectInput, parseBoardCompositionInput, } from "@neta-art/cohub";
2
2
  import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
3
3
  import { handleHttp, json, jsonRequested, table } from "../../output.js";
4
- import { mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
4
+ import { mutateSemantic, readInput, readOptions, resolvedBoard, showUpdated, withJson, } from "./context.js";
5
5
  export function registerBoardAnimationCommands(boards) {
6
6
  const effects = boards.command("effects").description("Manage Board effects");
7
- withJson(effects.command("list <board>").alias("ls").description("List effects"))
7
+ readOptions(withJson(effects.command("list <board>").alias("ls").description("List effects")))
8
8
  .action(async (target, options) => {
9
9
  try {
10
10
  const board = await resolvedBoard(boards, target);
11
- const result = await board.authoring({ include: ["effects"] });
11
+ const result = await board.authoring(readInput(options, "effects"));
12
12
  const effects = result.effects ?? [];
13
13
  if (jsonRequested(options))
14
14
  return json(effects);
@@ -23,6 +23,27 @@ export function registerBoardAnimationCommands(boards) {
23
23
  handleHttp(cause);
24
24
  }
25
25
  });
26
+ withJson(effects.command("get <board> <effect-id>").description("Get one complete Board effect"))
27
+ .action(async (target, effectId, options) => {
28
+ try {
29
+ const board = await resolvedBoard(boards, target);
30
+ const result = await board.authoring({ include: ["effects"], effectIds: [effectId] });
31
+ const effect = result.effects?.[0];
32
+ if (!effect)
33
+ throw new Error(`Effect not found: ${effectId}`);
34
+ if (jsonRequested(options))
35
+ return json(effect);
36
+ table([effect], [
37
+ { key: "id", label: "ID" },
38
+ { key: "kind", label: "KIND" },
39
+ { key: "enabled", label: "ENABLED" },
40
+ { key: "layer", label: "LAYER" },
41
+ ]);
42
+ }
43
+ catch (cause) {
44
+ handleHttp(cause);
45
+ }
46
+ });
26
47
  withJson(effects.command("apply <board>")
27
48
  .description("Atomically create or replace an effect")
28
49
  .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
@@ -56,11 +77,11 @@ Discover supported effect kinds:
56
77
  const compositions = boards
57
78
  .command("compositions")
58
79
  .description("Manage atomic Board animation compositions");
59
- withJson(compositions.command("list <board>").alias("ls").description("List compositions"))
80
+ readOptions(withJson(compositions.command("list <board>").alias("ls").description("List compositions")))
60
81
  .action(async (target, options) => {
61
82
  try {
62
83
  const board = await resolvedBoard(boards, target);
63
- const result = await board.authoring({ include: ["compositions"] });
84
+ const result = await board.authoring(readInput(options, "compositions"));
64
85
  const compositions = result.compositions ?? [];
65
86
  if (jsonRequested(options))
66
87
  return json(compositions);
@@ -88,8 +109,8 @@ Discover supported effect kinds:
88
109
  .action(async (target, compositionId, options) => {
89
110
  try {
90
111
  const board = await resolvedBoard(boards, target);
91
- const result = await board.authoring({ include: ["compositions"] });
92
- const composition = (result.compositions ?? []).find((item) => item.id === compositionId);
112
+ const result = await board.authoring({ include: ["compositions"], compositionIds: [compositionId] });
113
+ const composition = result.compositions?.[0];
93
114
  if (!composition)
94
115
  throw new Error(`Composition not found: ${compositionId}`);
95
116
  if (jsonRequested(options))
@@ -0,0 +1,4 @@
1
+ import { type BoardSemanticCommand } from "@neta-art/cohub";
2
+ import type { Command } from "commander";
3
+ export declare function parseBatchCommands(input: Record<string, unknown>): BoardSemanticCommand[];
4
+ export declare function registerBoardBatchCommand(boards: Command): void;
@@ -0,0 +1,62 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { BoardSemanticCommandSchema } from "@neta-art/cohub";
3
+ import { BOARD_TRANSACTION_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
4
+ import { handleHttp, json, jsonRequested, ok } from "../../output.js";
5
+ import { mutateSemantic, resolvedBoard, withJson, } from "./context.js";
6
+ export function parseBatchCommands(input) {
7
+ if (!Array.isArray(input.commands) || input.commands.length === 0) {
8
+ throw new Error("batch input must contain a non-empty commands array");
9
+ }
10
+ return input.commands.map((command, index) => {
11
+ const parsed = BoardSemanticCommandSchema.safeParse(command);
12
+ if (!parsed.success) {
13
+ const issue = parsed.error.issues[0];
14
+ const path = issue?.path.length ? `.${issue.path.join(".")}` : "";
15
+ throw new Error(`commands[${index}]${path}: ${issue?.message ?? "invalid command"}`);
16
+ }
17
+ return parsed.data;
18
+ });
19
+ }
20
+ export function registerBoardBatchCommand(boards) {
21
+ withJson(boards.command("batch <board>")
22
+ .description("Apply an atomic batch of semantic Board changes")
23
+ .requiredOption("-i, --input <file>", "Batch JSON with a commands array; use - for stdin")
24
+ .option("--base-version <version>", "Expected Board version; defaults to latest")
25
+ .option("--mutation-id <id>", "Stable id for safe retries")
26
+ .option("--dry-run", "Validate the whole batch without writing")
27
+ .addHelpText("after", `
28
+ Input format:
29
+ {"commands":[{"type":"item.patch","itemId":"title","patch":{"props":{"text":"Updated"}}}]}
30
+
31
+ Commands are applied atomically. See boards capabilities for supported fields.`))
32
+ .action(async (target, options) => {
33
+ try {
34
+ const input = await readBoardJsonObject(options.input, BOARD_TRANSACTION_INPUT_MAX_BYTES);
35
+ const commands = parseBatchCommands(input);
36
+ const parsedVersion = options.baseVersion === undefined ? undefined : Number(options.baseVersion);
37
+ if (parsedVersion !== undefined && (!Number.isSafeInteger(parsedVersion) || parsedVersion < 0)) {
38
+ throw new Error("base version must be a non-negative integer");
39
+ }
40
+ const board = await resolvedBoard(boards, target);
41
+ const result = await mutateSemantic(board, commands, {
42
+ ...(parsedVersion === undefined ? {} : { baseVersion: parsedVersion }),
43
+ mutationId: options.mutationId?.trim() || randomUUID(),
44
+ dryRun: Boolean(options.dryRun),
45
+ });
46
+ if (jsonRequested(options))
47
+ return json(result);
48
+ if (result.outcome === "dry-run") {
49
+ ok(`Validated ${commands.length} changes; no changes written`);
50
+ }
51
+ else if (result.outcome === "noop" || result.status === "validated") {
52
+ ok(`Board already matches the requested ${commands.length} changes`);
53
+ }
54
+ else {
55
+ ok(`${result.replayed ? "Replayed" : "Applied"} ${commands.length} changes at Board version ${result.board.version}`);
56
+ }
57
+ }
58
+ catch (cause) {
59
+ handleHttp(cause);
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerBoardConnectionCommands(boards: Command): void;
@@ -0,0 +1,95 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createBoardConnection } from "@neta-art/cohub/board";
3
+ import { handleHttp, json, jsonRequested, table } from "../../output.js";
4
+ import { mutateSemantic, readInput, readOptions, resolvedBoard, showUpdated, withJson, } from "./context.js";
5
+ const columns = [
6
+ { key: "id", label: "ID" },
7
+ { key: "source", label: "SOURCE" },
8
+ { key: "target", label: "TARGET" },
9
+ { key: "relation", label: "RELATION" },
10
+ { key: "direction", label: "DIRECTION" },
11
+ { key: "label", label: "LABEL" },
12
+ ];
13
+ function rows(connections) {
14
+ return connections.map((connection) => ({
15
+ id: connection.id,
16
+ source: connection.source.itemId,
17
+ target: connection.target.itemId,
18
+ relation: connection.relation ?? "",
19
+ direction: connection.direction ?? "",
20
+ label: connection.label ?? "",
21
+ }));
22
+ }
23
+ export function registerBoardConnectionCommands(boards) {
24
+ const connections = boards.command("connections").description("Manage Board connections");
25
+ readOptions(withJson(connections.command("list <board>").alias("ls").description("List Board connections")))
26
+ .action(async (target, options) => {
27
+ try {
28
+ const board = await resolvedBoard(boards, target);
29
+ const result = await board.authoring(readInput(options, "connections"));
30
+ const value = result.connections ?? [];
31
+ if (jsonRequested(options))
32
+ return json(value);
33
+ table(rows(value), columns);
34
+ }
35
+ catch (cause) {
36
+ handleHttp(cause);
37
+ }
38
+ });
39
+ withJson(connections.command("get <board> <connection-id>").description("Get one complete Board connection"))
40
+ .action(async (target, connectionId, options) => {
41
+ try {
42
+ const board = await resolvedBoard(boards, target);
43
+ const result = await board.authoring({ include: ["connections"], connectionIds: [connectionId] });
44
+ const connection = result.connections?.[0];
45
+ if (!connection)
46
+ throw new Error(`Connection not found: ${connectionId}`);
47
+ if (jsonRequested(options))
48
+ return json(connection);
49
+ table(rows([connection]), columns);
50
+ }
51
+ catch (cause) {
52
+ handleHttp(cause);
53
+ }
54
+ });
55
+ withJson(connections.command("create <board> <source> <target>").description("Create a Board connection")
56
+ .option("--id <id>", "Connection id")
57
+ .option("--relation <relation>", "Relation type")
58
+ .option("--direction <direction>", "none, forward, backward, or both", "forward")
59
+ .option("--label <label>", "Connection label")
60
+ .option("--source-port <id>", "Source port id")
61
+ .option("--target-port <id>", "Target port id"))
62
+ .action(async (target, source, destination, options) => {
63
+ try {
64
+ const direction = options.direction ?? "forward";
65
+ if (!["none", "forward", "backward", "both"].includes(direction)) {
66
+ throw new Error("--direction must be none, forward, backward, or both");
67
+ }
68
+ const board = await resolvedBoard(boards, target);
69
+ const connection = createBoardConnection({
70
+ id: options.id ?? randomUUID(),
71
+ sourceItemId: source,
72
+ targetItemId: destination,
73
+ relation: options.relation,
74
+ direction: direction,
75
+ label: options.label,
76
+ sourcePortId: options.sourcePort,
77
+ targetPortId: options.targetPort,
78
+ });
79
+ showUpdated(await mutateSemantic(board, [{ type: "connection.create", connection }]), options);
80
+ }
81
+ catch (cause) {
82
+ handleHttp(cause);
83
+ }
84
+ });
85
+ withJson(connections.command("delete <board> <connection-id>").alias("rm").description("Delete a Board connection"))
86
+ .action(async (target, connectionId, options) => {
87
+ try {
88
+ const board = await resolvedBoard(boards, target);
89
+ showUpdated(await mutateSemantic(board, [{ type: "connection.delete", connectionId }]), options);
90
+ }
91
+ catch (cause) {
92
+ handleHttp(cause);
93
+ }
94
+ });
95
+ }
@@ -1,17 +1,20 @@
1
- import type { BoardClient, BoardSemanticCommand } from "@neta-art/cohub";
1
+ import type { BoardAuthoringReadInput, BoardClient, BoardSemanticCommand } from "@neta-art/cohub";
2
2
  import type { Command } from "commander";
3
3
  export type JsonOptions = {
4
4
  json?: boolean;
5
5
  };
6
+ export type ReadOptions = JsonOptions & {
7
+ ids?: string;
8
+ };
6
9
  export declare function withJson(command: Command): Command;
7
10
  export declare function finite(value: string | undefined, name: string, fallback?: number): number;
11
+ export declare function readInput(options: ReadOptions, section: NonNullable<BoardAuthoringReadInput["include"]>[number]): BoardAuthoringReadInput;
12
+ export declare function readOptions(command: Command, label?: string): Command;
8
13
  export declare function resolvedBoard(boards: Command, target: string): Promise<BoardClient>;
9
14
  /**
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
+ * Send semantic commands with one automatic version-conflict retry when the
16
+ * caller did not pin a base version. An explicit `baseVersion` is a strict
17
+ * optimistic-concurrency check and must fail instead of being rebased.
15
18
  */
16
19
  export declare function mutateSemantic(board: BoardClient, commands: BoardSemanticCommand[], options?: {
17
20
  baseVersion?: number;
@@ -13,20 +13,29 @@ export function finite(value, name, fallback) {
13
13
  throw new Error(`${name} must be a finite number`);
14
14
  return result;
15
15
  }
16
+ export function readInput(options, section) {
17
+ const ids = options.ids?.split(",").map((id) => id.trim()).filter(Boolean);
18
+ return {
19
+ include: [section],
20
+ ...(ids?.length ? { [`${section.slice(0, -1)}Ids`]: ids } : {}),
21
+ };
22
+ }
23
+ export function readOptions(command, label = "Comma-separated resource IDs") {
24
+ return command.option("--ids <ids>", label);
25
+ }
16
26
  export async function resolvedBoard(boards, target) {
17
27
  const spaceId = resolveSpace(boards);
18
28
  const boardId = await resolveBoardId(spaceId, target);
19
29
  return createClient().space(spaceId).board(boardId);
20
30
  }
21
31
  /**
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.
32
+ * Send semantic commands with one automatic version-conflict retry when the
33
+ * caller did not pin a base version. An explicit `baseVersion` is a strict
34
+ * optimistic-concurrency check and must fail instead of being rebased.
27
35
  */
28
36
  export async function mutateSemantic(board, commands, options = {}) {
29
- const baseVersion = options.baseVersion ?? (await board.summary()).board.version;
37
+ const pinnedVersion = options.baseVersion;
38
+ const baseVersion = pinnedVersion ?? (await board.summary()).board.version;
30
39
  const send = (version) => board.mutateSemantic({
31
40
  mutationId: options.mutationId,
32
41
  baseVersion: version,
@@ -37,7 +46,7 @@ export async function mutateSemantic(board, commands, options = {}) {
37
46
  return await send(baseVersion);
38
47
  }
39
48
  catch (cause) {
40
- if (options.dryRun || !isVersionConflict(cause))
49
+ if (options.dryRun || pinnedVersion !== undefined || !isVersionConflict(cause))
41
50
  throw cause;
42
51
  const retry = (await board.summary()).board.version;
43
52
  return send(retry);
@@ -1,6 +1,12 @@
1
1
  import { json } from "../../output.js";
2
2
  const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
3
3
  const templates = {
4
+ "batch:basic": {
5
+ commands: [
6
+ { type: "board.patch", patch: { title: "Updated title" } },
7
+ { type: "item.patch", itemId: "title", patch: { props: { text: "Updated text" } } },
8
+ ],
9
+ },
4
10
  "create": {
5
11
  items: [{
6
12
  id: "title",
@@ -13,6 +19,134 @@ const templates = {
13
19
  effects: [],
14
20
  compositions: [],
15
21
  },
22
+ "create:workflow": {
23
+ items: [
24
+ {
25
+ id: "request",
26
+ type: "text",
27
+ frame: { x: 80, y: 190, width: 180, height: 48, rotation: 0 },
28
+ props: { text: "Request", fontSize: 28 },
29
+ style: { color: "brand" },
30
+ },
31
+ {
32
+ id: "agent",
33
+ type: "geo",
34
+ frame: { x: 360, y: 150, width: 240, height: 120, rotation: 0 },
35
+ props: { shape: "rounded", text: "Agent" },
36
+ style: { color: "blue", fillOpacity: 0.08 },
37
+ },
38
+ {
39
+ id: "result",
40
+ type: "geo",
41
+ frame: { x: 700, y: 150, width: 240, height: 120, rotation: 0 },
42
+ props: { shape: "rounded", text: "Result" },
43
+ style: { color: "green", fillOpacity: 0.08 },
44
+ },
45
+ ],
46
+ connections: [
47
+ { id: "request-agent", source: { itemId: "request" }, target: { itemId: "agent" }, relation: "triggers", label: "invoke" },
48
+ { id: "agent-result", source: { itemId: "agent" }, target: { itemId: "result" }, relation: "produces", label: "output" },
49
+ ],
50
+ effects: [],
51
+ compositions: [],
52
+ },
53
+ "create:media": {
54
+ items: [
55
+ {
56
+ id: "hero",
57
+ type: "image",
58
+ frame: { x: 80, y: 100, width: 640, height: 360, rotation: 0 },
59
+ source: { kind: "space-file", path: "assets/hero.webp" },
60
+ props: {},
61
+ },
62
+ {
63
+ id: "demo-video",
64
+ type: "video",
65
+ frame: { x: 760, y: 100, width: 480, height: 270, rotation: 0 },
66
+ source: { kind: "space-file", path: "assets/demo.mp4" },
67
+ props: {},
68
+ },
69
+ {
70
+ id: "soundtrack",
71
+ type: "audio",
72
+ frame: { x: 80, y: 500, width: 400, height: 96, rotation: 0 },
73
+ source: { kind: "space-file", path: "assets/soundtrack.mp3" },
74
+ props: {},
75
+ },
76
+ {
77
+ id: "brief",
78
+ type: "file",
79
+ frame: { x: 520, y: 500, width: 320, height: 180, rotation: 0 },
80
+ source: { kind: "space-file", path: "docs/brief.md" },
81
+ props: {},
82
+ },
83
+ ],
84
+ connections: [],
85
+ effects: [],
86
+ compositions: [],
87
+ },
88
+ "create:animation": {
89
+ items: [
90
+ {
91
+ id: "title",
92
+ type: "text",
93
+ frame: { x: 120, y: 80, width: 320, height: 48, rotation: 0 },
94
+ props: { text: "Launch", fontSize: 32 },
95
+ style: { color: "brand" },
96
+ },
97
+ {
98
+ id: "stroke",
99
+ type: "draw",
100
+ frame: { x: 120, y: 180, width: 180, height: 100, rotation: 0 },
101
+ props: { points: [{ x: 2, y: 98, p: 0.5 }, { x: 90, y: 2, p: 0.5 }, { x: 178, y: 98, p: 0.5 }] },
102
+ style: { color: "violet", strokeWidth: 4 },
103
+ },
104
+ ],
105
+ connections: [],
106
+ effects: [{
107
+ id: "pulse-title",
108
+ target: { type: "item", itemId: "title" },
109
+ kind: "effects.pulse",
110
+ kindVersion: 1,
111
+ lifecycle: "when-visible",
112
+ timeOrigin: "visible",
113
+ seed: "pulse-title",
114
+ params: { amount: 0.04, period: 1600 },
115
+ }],
116
+ compositions: [
117
+ {
118
+ id: "intro",
119
+ name: "Intro",
120
+ timeline: {
121
+ duration: 800,
122
+ tracks: [{
123
+ id: "title-opacity",
124
+ target: { type: "item", itemId: "title" },
125
+ channel: "style.opacity",
126
+ fill: "both",
127
+ keyframes: [{ time: 0, value: 0 }, { time: 800, value: 1, easing: "ease-out-cubic" }],
128
+ }],
129
+ },
130
+ },
131
+ {
132
+ id: "draw-stroke",
133
+ name: "Draw stroke",
134
+ timeline: {
135
+ duration: 900,
136
+ clips: [{
137
+ id: "reveal-stroke",
138
+ kind: "draw.reveal",
139
+ kindVersion: 1,
140
+ target: { type: "item", itemId: "stroke" },
141
+ start: 0,
142
+ duration: 900,
143
+ easing: "ease-out-cubic",
144
+ seed: "reveal-stroke",
145
+ }],
146
+ },
147
+ },
148
+ ],
149
+ },
16
150
  "item:text": {
17
151
  id: "title",
18
152
  type: "text",
@@ -45,7 +179,7 @@ const templates = {
45
179
  id: "stroke",
46
180
  type: "draw",
47
181
  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 }] },
182
+ props: { points: [{ x: 2, y: 98, p: 0.5 }, { x: 90, y: 2, p: 0.5 }, { x: 178, y: 98, p: 0.5 }] },
49
183
  style: { color: "violet", strokeWidth: 4 },
50
184
  },
51
185
  "item:arrow": {
@@ -87,28 +221,20 @@ const templates = {
87
221
  target: { type: "item", itemId: "title" },
88
222
  kind: "effects.pulse",
89
223
  kindVersion: 1,
90
- enabled: true,
91
224
  lifecycle: "when-visible",
92
225
  timeOrigin: "visible",
93
- layer: "front",
94
226
  seed: "pulse-title",
95
227
  params: { amount: 0.04, period: 1600 },
96
- assetRefs: [],
97
- metadata: {},
98
228
  },
99
229
  "effect:float": {
100
230
  id: "float-hero",
101
231
  target: { type: "item", itemId: "hero" },
102
232
  kind: "effects.float",
103
233
  kindVersion: 1,
104
- enabled: true,
105
234
  lifecycle: "when-visible",
106
235
  timeOrigin: "visible",
107
- layer: "front",
108
236
  seed: "float-hero",
109
237
  params: { distance: 8, period: 2200 },
110
- assetRefs: [],
111
- metadata: {},
112
238
  },
113
239
  "composition:fade": {
114
240
  id: "intro",
@@ -119,63 +245,87 @@ const templates = {
119
245
  id: "title-opacity",
120
246
  target: { type: "item", itemId: "title" },
121
247
  channel: "style.opacity",
122
- channelVersion: 1,
123
- interpolation: "linear",
124
248
  fill: "both",
125
249
  keyframes: [
126
250
  { time: 0, value: 0 },
127
251
  { time: 800, value: 1, easing: "ease-out-cubic" },
128
252
  ],
129
- metadata: {},
130
253
  }],
131
- clips: [],
132
- markers: [],
133
254
  },
134
- playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
135
- metadata: {},
136
255
  },
137
256
  "composition:reveal": {
138
257
  id: "reveal",
139
258
  name: "Reveal",
140
259
  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: [],
260
+ duration: 600,
261
+ clips: [{ id: "reveal-title", kind: "text.reveal", kindVersion: 1, target: { type: "item", itemId: "title" }, start: 0, duration: 600, fill: "both", easing: "ease-out-cubic", params: { mode: "words" }, seed: "reveal-title" }],
145
262
  },
146
- playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
147
- metadata: {},
148
263
  },
149
264
  "composition:motion-path": {
150
265
  id: "motion",
151
266
  name: "Motion path",
152
267
  timeline: {
153
268
  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: [],
269
+ clips: [{ id: "move-hero", kind: "motion.path", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, fill: "both", easing: "ease-in-out-cubic", params: { points: [{ x: 0, y: 0 }, { x: 160, y: -40 }, { x: 320, y: 0 }], orient: true }, seed: "move-hero" }],
270
+ },
271
+ },
272
+ "composition:draw-reveal": {
273
+ id: "draw-stroke",
274
+ name: "Draw stroke",
275
+ timeline: {
276
+ duration: 900,
277
+ clips: [{ id: "reveal-stroke", kind: "draw.reveal", kindVersion: 1, target: { type: "item", itemId: "stroke" }, start: 0, duration: 900, easing: "ease-out-cubic", seed: "reveal-stroke" }],
278
+ },
279
+ },
280
+ "composition:trail": {
281
+ id: "move-with-trail",
282
+ name: "Move with trail",
283
+ timeline: {
284
+ duration: 1200,
285
+ clips: [
286
+ { id: "move-hero", kind: "motion.path", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, easing: "ease-in-out-cubic", fill: "both", params: { points: [{ x: 0, y: 0 }, { x: 320, y: 0 }] }, seed: "move-hero" },
287
+ { id: "trail-hero", kind: "effects.trail", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, params: { alpha: 0.48 }, seed: "trail-hero" },
288
+ ],
289
+ },
290
+ },
291
+ "composition:impact": {
292
+ id: "impact",
293
+ name: "Impact",
294
+ timeline: {
295
+ duration: 500,
296
+ clips: [{ id: "impact-ring", kind: "effects.impact", kindVersion: 1, target: { type: "board" }, start: 0, duration: 500, layer: "front", params: { center: { x: 400, y: 300 }, radius: 120 }, seed: "impact-ring" }],
297
+ },
298
+ },
299
+ "composition:flash": {
300
+ id: "flash",
301
+ name: "Flash",
302
+ timeline: {
303
+ duration: 300,
304
+ clips: [{ id: "screen-flash", kind: "effects.flash", kindVersion: 1, target: { type: "board" }, start: 0, duration: 300, layer: "screen", params: { alpha: 0.3 }, seed: "screen-flash" }],
157
305
  },
158
- playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
159
- metadata: {},
160
306
  },
161
307
  "composition:particles": {
162
308
  id: "celebrate",
163
309
  name: "Celebrate",
164
310
  timeline: {
165
311
  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: [],
312
+ clips: [{ id: "particles", kind: "effects.particles", kindVersion: 1, target: { type: "board" }, start: 0, duration: 1000, layer: "front", params: { count: 420, bounds: { x: 0, y: 0, width: 800, height: 600 } }, seed: "particles" }],
313
+ },
314
+ playback: { endBehavior: "reset" },
315
+ },
316
+ "composition:camera-shake": {
317
+ id: "shake",
318
+ name: "Camera shake",
319
+ timeline: {
320
+ duration: 500,
321
+ clips: [{ id: "shake-camera", kind: "camera.shake", kindVersion: 1, target: { type: "camera" }, start: 0, duration: 500, layer: "screen", params: { amount: 8, frequency: 28 }, seed: "shake-camera" }],
169
322
  },
170
- playback: { loop: false, endBehavior: "reset", reducedMotion: { mode: "base" } },
171
- metadata: {},
172
323
  },
173
324
  "composition:camera-focus": {
174
325
  id: "tour",
175
326
  name: "Tour",
176
327
  timeline: {
177
328
  duration: 1000,
178
- tracks: [],
179
329
  clips: [{
180
330
  id: "focus-title",
181
331
  kind: "camera.focus",
@@ -187,14 +337,9 @@ const templates = {
187
337
  fill: "forwards",
188
338
  easing: "ease-out-cubic",
189
339
  params: { focus: { type: "item", itemId: "title" }, fit: "contain", padding: 32 },
190
- assetRefs: [],
191
340
  seed: "focus-title",
192
- metadata: {},
193
341
  }],
194
- markers: [],
195
342
  },
196
- playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
197
- metadata: {},
198
343
  },
199
344
  };
200
345
  export const BOARD_EXAMPLE_KEYS = Object.keys(templates);
@@ -206,19 +351,28 @@ export function boardExample(kind, type) {
206
351
  return template;
207
352
  }
208
353
  export function registerBoardExampleCommands(boards) {
209
- boards.command("examples <kind> [type]")
354
+ boards.command("examples [kind] [type]")
210
355
  .description("Print editable semantic JSON templates")
356
+ .option("--list", "List available examples")
211
357
  .addHelpText("after", `
212
358
  Kinds:
213
- create
359
+ create [workflow|media|animation]
360
+ batch basic
214
361
  item text|image|video|audio|file|task|geo|frame|draw|arrow
215
362
  effect pulse|float
216
- composition fade|reveal|motion-path|particles|camera-focus
363
+ composition fade|reveal|draw-reveal|motion-path|trail|impact|flash|particles|camera-shake|camera-focus
217
364
 
218
365
  Examples:
366
+ cohub boards examples --list
367
+ cohub boards examples create workflow > board.json
368
+ cohub boards examples batch basic > changes.json
219
369
  cohub boards examples item text > item.json
220
370
  cohub boards examples composition fade > intro.json`)
221
- .action((kind, type) => {
371
+ .action((kind, type, options) => {
372
+ if (options.list)
373
+ return json(BOARD_EXAMPLE_KEYS);
374
+ if (!kind)
375
+ throw new Error("Example kind is required. Use --list to see available examples.");
222
376
  json(boardExample(kind, type));
223
377
  });
224
378
  }
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { BoardAuthoringItemSchema, BoardItemPatchSchema, } from "@neta-art/cohub";
3
3
  import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
4
4
  import { handleHttp, json, jsonRequested, ok, table } from "../../output.js";
5
- import { resolvedBoard, withJson, } from "./context.js";
5
+ import { readInput, readOptions, resolvedBoard, withJson, } from "./context.js";
6
6
  const baseVersion = (value, fallback) => {
7
7
  if (value === undefined)
8
8
  return fallback;
@@ -40,11 +40,11 @@ function mutationOptions(command) {
40
40
  }
41
41
  export function registerBoardItemCommands(boards) {
42
42
  const items = boards.command("items").description("Author Board items with semantic JSON");
43
- withJson(items.command("list <board>").alias("ls").description("List semantic Board items"))
43
+ readOptions(withJson(items.command("list <board>").alias("ls").description("List semantic Board items")))
44
44
  .action(async (target, options) => {
45
45
  try {
46
46
  const board = await resolvedBoard(boards, target);
47
- const snapshot = await board.authoring();
47
+ const snapshot = await board.authoring(readInput(options, "items"));
48
48
  const items = snapshot.items ?? [];
49
49
  if (jsonRequested(options))
50
50
  return json(items);
@@ -68,6 +68,36 @@ export function registerBoardItemCommands(boards) {
68
68
  handleHttp(cause);
69
69
  }
70
70
  });
71
+ withJson(items.command("get <board> <item-id>").description("Get one complete Board item"))
72
+ .action(async (target, itemId, options) => {
73
+ try {
74
+ const board = await resolvedBoard(boards, target);
75
+ const snapshot = await board.authoring({ include: ["items"], itemIds: [itemId] });
76
+ const item = snapshot.items?.[0];
77
+ if (!item)
78
+ throw new Error(`Item not found: ${itemId}`);
79
+ if (jsonRequested(options))
80
+ return json(item);
81
+ table([{
82
+ id: item.id,
83
+ type: item.type,
84
+ x: item.frame.x,
85
+ y: item.frame.y,
86
+ width: item.frame.width,
87
+ height: item.frame.height,
88
+ }], [
89
+ { key: "id", label: "ID" },
90
+ { key: "type", label: "TYPE" },
91
+ { key: "x", label: "X" },
92
+ { key: "y", label: "Y" },
93
+ { key: "width", label: "WIDTH" },
94
+ { key: "height", label: "HEIGHT" },
95
+ ]);
96
+ }
97
+ catch (cause) {
98
+ handleHttp(cause);
99
+ }
100
+ });
71
101
  mutationOptions(items.command("create <board>")
72
102
  .description("Create one item")
73
103
  .requiredOption("-i, --input <file>", "Board Item JSON; use - for stdin")
@@ -211,7 +211,7 @@ function registerExportCommand(boards) {
211
211
  export function registerBoards(program) {
212
212
  const boards = program
213
213
  .command("boards")
214
- .description("Inspect and update Boards")
214
+ .description("Inspect and update Boards by ID or .board path")
215
215
  .hook("preAction", () => { resolveSpace(boards); });
216
216
  withJson(boards.command("create <path>")
217
217
  .description("Create a Board")
@@ -252,10 +252,13 @@ Generate an editable seed:
252
252
  .alias("get")
253
253
  .description("Show Board metadata and semantic resource counts")
254
254
  .addHelpText("after", `
255
- Inspect content with:
255
+ Inspect resources with:
256
256
  cohub boards items list <board> --json
257
+ cohub boards connections list <board> --json
257
258
  cohub boards effects list <board> --json
258
- cohub boards compositions list <board> --json`))
259
+ cohub boards compositions list <board> --json
260
+ Apply multiple changes atomically:
261
+ cohub boards batch <board> --input changes.json --dry-run`))
259
262
  .action(async (target, options) => {
260
263
  try {
261
264
  const spaceId = resolveSpace(boards);
@@ -314,7 +317,8 @@ Use boards examples for editable starter JSON.`))
314
317
  });
315
318
  registerBoardDomainCommands(boards);
316
319
  registerExportCommand(boards);
317
- withJson(boards.command("play <board> <composition-id>")
320
+ const playback = boards.command("playback").description("Control shared Board playback");
321
+ withJson(playback.command("play <board> <composition-id>")
318
322
  .description("Start shared playback")
319
323
  .option("--position <time>", "Initial position in milliseconds")
320
324
  .option("--time-scale <scale>", "Playback speed from 0 to 4")
@@ -358,11 +362,11 @@ Use boards examples for editable starter JSON.`))
358
362
  handleHttp(cause);
359
363
  }
360
364
  };
361
- withJson(boards.command("pause <board> <playback-id>")
365
+ withJson(playback.command("pause <board> <playback-id>")
362
366
  .description("Pause playback")
363
367
  .option("--command-id <id>", "Idempotency command ID"))
364
368
  .action(playbackAction("pause"));
365
- withJson(boards.command("seek <board> <playback-id> <position>")
369
+ withJson(playback.command("seek <board> <playback-id> <position>")
366
370
  .description("Seek playback")
367
371
  .option("--command-id <id>", "Idempotency command ID"))
368
372
  .action(async (target, playbackId, position, options) => {
@@ -383,7 +387,7 @@ Use boards examples for editable starter JSON.`))
383
387
  handleHttp(cause);
384
388
  }
385
389
  });
386
- withJson(boards.command("stop <board> <playback-id>")
390
+ withJson(playback.command("stop <board> <playback-id>")
387
391
  .description("Stop playback")
388
392
  .option("--command-id <id>", "Idempotency command ID"))
389
393
  .action(playbackAction("stop"));
package/dist/output.js CHANGED
@@ -75,6 +75,27 @@ function errorMessageFromBody(body) {
75
75
  return errorBody.message;
76
76
  return null;
77
77
  }
78
+ function diagnosticDetailFromBody(body) {
79
+ if (!body || typeof body !== "object")
80
+ return null;
81
+ const diagnostics = body.diagnostics;
82
+ if (!Array.isArray(diagnostics) || diagnostics.length === 0)
83
+ return null;
84
+ const lines = diagnostics.slice(0, 8).flatMap((item) => {
85
+ if (!item || typeof item !== "object")
86
+ return [];
87
+ const diagnostic = item;
88
+ if (typeof diagnostic.message !== "string" || !diagnostic.message.trim())
89
+ return [];
90
+ const path = typeof diagnostic.path === "string" ? diagnostic.path : "";
91
+ const message = diagnostic.message.replace(path ? `${path}: ` : "", "");
92
+ return [`${path ? `${path}: ` : ""}${message}`];
93
+ });
94
+ if (lines.length === 0)
95
+ return null;
96
+ const suffix = diagnostics.length > lines.length ? ` (+${diagnostics.length - lines.length} more)` : "";
97
+ return `${lines.join("; ")}${suffix}`;
98
+ }
78
99
  function debugErrorMetaFromBody(body) {
79
100
  if (!body || typeof body !== "object")
80
101
  return [];
@@ -139,7 +160,10 @@ export function handleHttp(e) {
139
160
  const presentation = errorPresentationFromHttpError(e);
140
161
  const message = presentation?.message ?? errorMessageFromBody(body) ?? (e instanceof Error ? e.message : String(e));
141
162
  const fetchDetail = fetchFailureDetail(e);
163
+ const diagnosticDetail = diagnosticDetailFromBody(body);
142
164
  const detailParts = [];
165
+ if (diagnosticDetail)
166
+ detailParts.push(diagnosticDetail);
143
167
  if (presentation?.detail)
144
168
  detailParts.push(presentation.detail);
145
169
  if (process.env.COHUB_DEBUG_ERRORS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.2.1",
3
+ "version": "6.3.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -15,21 +15,21 @@
15
15
  "NOTICE"
16
16
  ],
17
17
  "dependencies": {
18
- "@neta-art/generation": "^0.1.22",
18
+ "@neta-art/generation": "^0.1.23",
19
19
  "commander": "^15.0.0",
20
- "pixi.js": "^8.19.0",
21
- "sharp": "^0.35.3",
22
- "@neta-art/cohub": "8.5.0"
20
+ "pixi.js": "^8.20.1",
21
+ "sharp": "^0.35.4",
22
+ "@neta-art/cohub": "8.6.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^26.1.1",
28
+ "@types/node": "^26.4.0",
29
29
  "typescript": "^7.0.2"
30
30
  },
31
31
  "optionalDependencies": {
32
- "@napi-rs/canvas": "^1.0.2"
32
+ "@napi-rs/canvas": "^1.0.8"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsc -p tsconfig.build.json",
@@ -1,3 +0,0 @@
1
- import type { Command } from "commander";
2
- /** Connection shortcuts; Item authoring lives in ./items.ts. */
3
- export declare function registerBoardNodeCommands(boards: Command): void;
@@ -1,50 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { createBoardConnection } from "@neta-art/cohub/board";
3
- import { mutateSemantic } from "./context.js";
4
- import { handleHttp } from "../../output.js";
5
- import { resolvedBoard, showUpdated, withJson, } from "./context.js";
6
- /** Connection shortcuts; Item authoring lives in ./items.ts. */
7
- export function registerBoardNodeCommands(boards) {
8
- withJson(boards.command("connect <board> <source> <target>")
9
- .description("Connect two Board items")
10
- .option("--id <id>", "Connection id")
11
- .option("--relation <relation>", "Relation type")
12
- .option("--direction <direction>", "none, forward, backward, or both", "forward")
13
- .option("--label <label>", "Connection label")
14
- .option("--source-port <id>", "Source port id")
15
- .option("--target-port <id>", "Target port id"))
16
- .action(async (target, source, destination, options) => {
17
- try {
18
- const direction = options.direction ?? "forward";
19
- if (!["none", "forward", "backward", "both"].includes(direction)) {
20
- throw new Error("--direction must be none, forward, backward, or both");
21
- }
22
- const board = await resolvedBoard(boards, target);
23
- const connection = createBoardConnection({
24
- id: options.id ?? randomUUID(),
25
- sourceItemId: source,
26
- targetItemId: destination,
27
- relation: options.relation,
28
- direction: direction,
29
- label: options.label,
30
- sourcePortId: options.sourcePort,
31
- targetPortId: options.targetPort,
32
- });
33
- showUpdated(await mutateSemantic(board, [{ type: "connection.create", connection }]), options);
34
- }
35
- catch (cause) {
36
- handleHttp(cause);
37
- }
38
- });
39
- withJson(boards.command("disconnect <board> <connection-id>")
40
- .description("Remove a Board connection"))
41
- .action(async (target, connectionId, options) => {
42
- try {
43
- const board = await resolvedBoard(boards, target);
44
- showUpdated(await mutateSemantic(board, [{ type: "connection.delete", connectionId }]), options);
45
- }
46
- catch (cause) {
47
- handleHttp(cause);
48
- }
49
- });
50
- }