@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 +61 -57
- package/dist/commands/board-domain.js +4 -2
- package/dist/commands/boards/animation.js +28 -7
- package/dist/commands/boards/batch.d.ts +4 -0
- package/dist/commands/boards/batch.js +62 -0
- package/dist/commands/boards/connections.d.ts +2 -0
- package/dist/commands/boards/connections.js +95 -0
- package/dist/commands/boards/context.d.ts +9 -6
- package/dist/commands/boards/context.js +16 -7
- package/dist/commands/boards/examples.js +196 -42
- package/dist/commands/boards/items.js +33 -3
- package/dist/commands/boards.js +11 -7
- package/dist/output.js +24 -0
- package/package.json +7 -7
- package/dist/commands/boards/nodes.d.ts +0 -3
- package/dist/commands/boards/nodes.js +0 -50
package/README.md
CHANGED
|
@@ -157,53 +157,57 @@ cohub -s <spaceId> spaces activity 365 --json
|
|
|
157
157
|
|
|
158
158
|
## Boards
|
|
159
159
|
|
|
160
|
-
Board
|
|
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
|
|
165
|
-
cohub -s <spaceId> boards
|
|
166
|
-
cohub -s <spaceId> boards
|
|
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
|
-
|
|
171
|
+
Use targeted reads for large Boards:
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
|
-
cohub boards
|
|
175
|
-
cohub boards
|
|
176
|
-
cohub boards
|
|
177
|
-
cohub boards
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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 `--
|
|
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
|
-
##
|
|
292
|
+
## Apps
|
|
289
293
|
|
|
290
|
-
Publish and manage
|
|
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>
|
|
301
|
-
cohub
|
|
302
|
-
cohub
|
|
303
|
-
cohub
|
|
304
|
-
cohub -s <spaceId>
|
|
305
|
-
cohub -s <spaceId>
|
|
306
|
-
cohub -s <spaceId>
|
|
307
|
-
cohub
|
|
308
|
-
cohub
|
|
309
|
-
cohub
|
|
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
|
|
316
|
+
Resolve a published App by public identity:
|
|
313
317
|
|
|
314
318
|
```bash
|
|
315
|
-
cohub
|
|
319
|
+
cohub apps resolve <appSlug> --owner <username> --space-slug <spaceSlug>
|
|
316
320
|
```
|
|
317
321
|
|
|
318
|
-
Use `--json` for machine-readable output. `
|
|
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
|
|
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
|
|
326
|
-
methods the
|
|
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 <
|
|
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
|
|
332
|
-
cohub desktop open <
|
|
333
|
-
cohub desktop open <
|
|
334
|
-
cohub desktop open <
|
|
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, `
|
|
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
|
|
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
|
|
342
|
-
and waits for the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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,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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
142
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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")
|
package/dist/commands/boards.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
18
|
+
"@neta-art/generation": "^0.1.23",
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
|
-
"pixi.js": "^8.
|
|
21
|
-
"sharp": "^0.35.
|
|
22
|
-
"@neta-art/cohub": "8.
|
|
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.
|
|
28
|
+
"@types/node": "^26.4.0",
|
|
29
29
|
"typescript": "^7.0.2"
|
|
30
30
|
},
|
|
31
31
|
"optionalDependencies": {
|
|
32
|
-
"@napi-rs/canvas": "^1.0.
|
|
32
|
+
"@napi-rs/canvas": "^1.0.8"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -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
|
-
}
|