@neta-art/cohub-cli 6.2.2 → 6.4.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 +30 -26
- package/dist/board-export.d.ts +3 -2
- package/dist/board-export.js +3 -2
- 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 +241 -54
- package/dist/commands/boards/items.js +53 -9
- package/dist/commands/boards.d.ts +7 -0
- package/dist/commands/boards.js +20 -12
- package/dist/output.js +24 -0
- package/package.json +2 -2
- 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
|
package/dist/board-export.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Board image export for the CLI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
+
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
+
* Canvas2D. Everything platform-specific
|
|
6
7
|
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
7
8
|
* space, and locating fonts on disk.
|
|
8
9
|
*/
|
package/dist/board-export.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Board image export for the CLI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
+
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
+
* Canvas2D. Everything platform-specific
|
|
6
7
|
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
7
8
|
* space, and locating fonts on disk.
|
|
8
9
|
*/
|
|
@@ -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,11 +1,18 @@
|
|
|
1
1
|
import { json } from "../../output.js";
|
|
2
|
-
const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
|
|
3
2
|
const templates = {
|
|
3
|
+
"batch:basic": {
|
|
4
|
+
commands: [
|
|
5
|
+
{ type: "board.patch", patch: { title: "Updated title" } },
|
|
6
|
+
{ type: "item.patch", itemId: "title", patch: { props: { text: "Updated text" } } },
|
|
7
|
+
],
|
|
8
|
+
},
|
|
4
9
|
"create": {
|
|
5
10
|
items: [{
|
|
6
11
|
id: "title",
|
|
7
12
|
type: "text",
|
|
8
|
-
|
|
13
|
+
position: { x: 120, y: 80 },
|
|
14
|
+
size: { width: 320, height: 48 },
|
|
15
|
+
rotation: 0,
|
|
9
16
|
props: { text: "Launch plan", fontSize: 32 },
|
|
10
17
|
style: { color: "brand" },
|
|
11
18
|
}],
|
|
@@ -13,73 +20,233 @@ const templates = {
|
|
|
13
20
|
effects: [],
|
|
14
21
|
compositions: [],
|
|
15
22
|
},
|
|
23
|
+
"create:workflow": {
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
id: "request",
|
|
27
|
+
type: "text",
|
|
28
|
+
position: { x: 80, y: 190 },
|
|
29
|
+
size: { width: 180, height: 48 },
|
|
30
|
+
rotation: 0,
|
|
31
|
+
props: { text: "Request", fontSize: 28 },
|
|
32
|
+
style: { color: "brand" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "agent",
|
|
36
|
+
type: "geo",
|
|
37
|
+
position: { x: 360, y: 150 },
|
|
38
|
+
size: { width: 240, height: 120 },
|
|
39
|
+
rotation: 0,
|
|
40
|
+
props: { shape: "rounded", text: "Agent" },
|
|
41
|
+
style: { color: "blue", fillOpacity: 0.08 },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "result",
|
|
45
|
+
type: "geo",
|
|
46
|
+
position: { x: 700, y: 150 },
|
|
47
|
+
size: { width: 240, height: 120 },
|
|
48
|
+
rotation: 0,
|
|
49
|
+
props: { shape: "rounded", text: "Result" },
|
|
50
|
+
style: { color: "green", fillOpacity: 0.08 },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
connections: [
|
|
54
|
+
{ id: "request-agent", source: { itemId: "request" }, target: { itemId: "agent" }, relation: "triggers", label: "invoke" },
|
|
55
|
+
{ id: "agent-result", source: { itemId: "agent" }, target: { itemId: "result" }, relation: "produces", label: "output" },
|
|
56
|
+
],
|
|
57
|
+
effects: [],
|
|
58
|
+
compositions: [],
|
|
59
|
+
},
|
|
60
|
+
"create:media": {
|
|
61
|
+
items: [
|
|
62
|
+
{
|
|
63
|
+
id: "hero",
|
|
64
|
+
type: "image",
|
|
65
|
+
position: { x: 80, y: 100 },
|
|
66
|
+
size: { width: 640, height: 360 },
|
|
67
|
+
rotation: 0,
|
|
68
|
+
source: { kind: "space-file", path: "assets/hero.webp" },
|
|
69
|
+
props: {},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "demo-video",
|
|
73
|
+
type: "video",
|
|
74
|
+
position: { x: 760, y: 100 },
|
|
75
|
+
size: { width: 480, height: 270 },
|
|
76
|
+
rotation: 0,
|
|
77
|
+
source: { kind: "space-file", path: "assets/demo.mp4" },
|
|
78
|
+
props: {},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "soundtrack",
|
|
82
|
+
type: "audio",
|
|
83
|
+
position: { x: 80, y: 500 },
|
|
84
|
+
size: { width: 400, height: 96 },
|
|
85
|
+
rotation: 0,
|
|
86
|
+
source: { kind: "space-file", path: "assets/soundtrack.mp3" },
|
|
87
|
+
props: {},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "brief",
|
|
91
|
+
type: "file",
|
|
92
|
+
position: { x: 520, y: 500 },
|
|
93
|
+
size: { width: 320, height: 180 },
|
|
94
|
+
rotation: 0,
|
|
95
|
+
source: { kind: "space-file", path: "docs/brief.md" },
|
|
96
|
+
props: {},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
connections: [],
|
|
100
|
+
effects: [],
|
|
101
|
+
compositions: [],
|
|
102
|
+
},
|
|
103
|
+
"create:animation": {
|
|
104
|
+
items: [
|
|
105
|
+
{
|
|
106
|
+
id: "title",
|
|
107
|
+
type: "text",
|
|
108
|
+
position: { x: 120, y: 80 },
|
|
109
|
+
size: { width: 320, height: 48 },
|
|
110
|
+
rotation: 0,
|
|
111
|
+
props: { text: "Launch", fontSize: 32 },
|
|
112
|
+
style: { color: "brand" },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "stroke",
|
|
116
|
+
type: "draw",
|
|
117
|
+
rotation: 0,
|
|
118
|
+
props: { points: [{ x: 2, y: 98, p: 0.5 }, { x: 90, y: 2, p: 0.5 }, { x: 178, y: 98, p: 0.5 }] },
|
|
119
|
+
style: { color: "violet", strokeWidth: 4 },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
connections: [],
|
|
123
|
+
effects: [{
|
|
124
|
+
id: "pulse-title",
|
|
125
|
+
target: { type: "item", itemId: "title" },
|
|
126
|
+
kind: "effects.pulse",
|
|
127
|
+
kindVersion: 1,
|
|
128
|
+
lifecycle: "when-visible",
|
|
129
|
+
timeOrigin: "visible",
|
|
130
|
+
seed: "pulse-title",
|
|
131
|
+
params: { amount: 0.04, period: 1600 },
|
|
132
|
+
}],
|
|
133
|
+
compositions: [
|
|
134
|
+
{
|
|
135
|
+
id: "intro",
|
|
136
|
+
name: "Intro",
|
|
137
|
+
timeline: {
|
|
138
|
+
duration: 800,
|
|
139
|
+
tracks: [{
|
|
140
|
+
id: "title-opacity",
|
|
141
|
+
target: { type: "item", itemId: "title" },
|
|
142
|
+
channel: "style.opacity",
|
|
143
|
+
fill: "both",
|
|
144
|
+
keyframes: [{ time: 0, value: 0 }, { time: 800, value: 1, easing: "ease-out-cubic" }],
|
|
145
|
+
}],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "draw-stroke",
|
|
150
|
+
name: "Draw stroke",
|
|
151
|
+
timeline: {
|
|
152
|
+
duration: 900,
|
|
153
|
+
clips: [{
|
|
154
|
+
id: "reveal-stroke",
|
|
155
|
+
kind: "draw.reveal",
|
|
156
|
+
kindVersion: 1,
|
|
157
|
+
target: { type: "item", itemId: "stroke" },
|
|
158
|
+
start: 0,
|
|
159
|
+
duration: 900,
|
|
160
|
+
easing: "ease-out-cubic",
|
|
161
|
+
seed: "reveal-stroke",
|
|
162
|
+
}],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
16
167
|
"item:text": {
|
|
17
168
|
id: "title",
|
|
18
169
|
type: "text",
|
|
19
|
-
|
|
170
|
+
position: { x: 120, y: 80 },
|
|
171
|
+
size: { width: 320, height: 48 },
|
|
172
|
+
rotation: 0,
|
|
20
173
|
props: { text: "Launch plan", fontSize: 32 },
|
|
21
174
|
style: { color: "brand" },
|
|
22
175
|
},
|
|
23
176
|
"item:image": {
|
|
24
177
|
id: "hero",
|
|
25
178
|
type: "image",
|
|
26
|
-
|
|
179
|
+
position: { x: 120, y: 160 },
|
|
180
|
+
size: { width: 640, height: 360 },
|
|
181
|
+
rotation: 0,
|
|
27
182
|
source: { kind: "space-file", path: "assets/hero.webp" },
|
|
28
183
|
props: {},
|
|
29
184
|
},
|
|
30
185
|
"item:geo": {
|
|
31
186
|
id: "goal",
|
|
32
187
|
type: "geo",
|
|
33
|
-
|
|
188
|
+
position: { x: 120, y: 160 },
|
|
189
|
+
size: { width: 280, height: 140 },
|
|
190
|
+
rotation: 0,
|
|
34
191
|
props: { shape: "rounded", text: "Ship" },
|
|
35
192
|
style: { color: "green", fillOpacity: 0.12 },
|
|
36
193
|
},
|
|
37
194
|
"item:frame": {
|
|
38
195
|
id: "scene",
|
|
39
196
|
type: "frame",
|
|
40
|
-
|
|
197
|
+
position: { x: 80, y: 60 },
|
|
198
|
+
size: { width: 960, height: 540 },
|
|
199
|
+
rotation: 0,
|
|
41
200
|
props: { label: "Scene 1" },
|
|
42
201
|
style: { color: "neutral" },
|
|
43
202
|
},
|
|
44
203
|
"item:draw": {
|
|
45
204
|
id: "stroke",
|
|
46
205
|
type: "draw",
|
|
47
|
-
|
|
48
|
-
props: { points: [{ x:
|
|
206
|
+
rotation: 0,
|
|
207
|
+
props: { points: [{ x: 102, y: 198, p: 0.5 }, { x: 190, y: 102, p: 0.5 }, { x: 278, y: 198, p: 0.5 }] },
|
|
49
208
|
style: { color: "violet", strokeWidth: 4 },
|
|
50
209
|
},
|
|
51
210
|
"item:arrow": {
|
|
52
211
|
id: "arrow",
|
|
53
212
|
type: "arrow",
|
|
54
|
-
|
|
213
|
+
rotation: 0,
|
|
55
214
|
props: { start: { x: 116, y: 150 }, end: { x: 344, y: 150 }, bend: 0, arrowStart: false, arrowEnd: true, label: "next" },
|
|
56
215
|
style: { color: "brand", strokeWidth: 2.5 },
|
|
57
216
|
},
|
|
58
217
|
"item:video": {
|
|
59
218
|
id: "demo-video",
|
|
60
219
|
type: "video",
|
|
61
|
-
|
|
220
|
+
position: { x: 120, y: 160 },
|
|
221
|
+
size: { width: 640, height: 360 },
|
|
222
|
+
rotation: 0,
|
|
62
223
|
source: { kind: "space-file", path: "assets/demo.mp4" },
|
|
63
224
|
props: {},
|
|
64
225
|
},
|
|
65
226
|
"item:audio": {
|
|
66
227
|
id: "soundtrack",
|
|
67
228
|
type: "audio",
|
|
68
|
-
|
|
229
|
+
position: { x: 120, y: 540 },
|
|
230
|
+
size: { width: 480, height: 96 },
|
|
231
|
+
rotation: 0,
|
|
69
232
|
source: { kind: "space-file", path: "assets/soundtrack.mp3" },
|
|
70
233
|
props: {},
|
|
71
234
|
},
|
|
72
235
|
"item:file": {
|
|
73
236
|
id: "brief",
|
|
74
237
|
type: "file",
|
|
75
|
-
|
|
238
|
+
position: { x: 120, y: 160 },
|
|
239
|
+
size: { width: 360, height: 220 },
|
|
240
|
+
rotation: 0,
|
|
76
241
|
source: { kind: "space-file", path: "docs/brief.md" },
|
|
77
242
|
props: {},
|
|
78
243
|
},
|
|
79
244
|
"item:task": {
|
|
80
245
|
id: "task",
|
|
81
246
|
type: "task",
|
|
82
|
-
|
|
247
|
+
position: { x: 120, y: 160 },
|
|
248
|
+
size: { width: 420, height: 240 },
|
|
249
|
+
rotation: 0,
|
|
83
250
|
props: { taskRunId: "task-run-id", snapshot: { taskType: "generation", status: "running", title: "Generate concept", artifactCount: 0, artifacts: [] } },
|
|
84
251
|
},
|
|
85
252
|
"effect:pulse": {
|
|
@@ -87,28 +254,20 @@ const templates = {
|
|
|
87
254
|
target: { type: "item", itemId: "title" },
|
|
88
255
|
kind: "effects.pulse",
|
|
89
256
|
kindVersion: 1,
|
|
90
|
-
enabled: true,
|
|
91
257
|
lifecycle: "when-visible",
|
|
92
258
|
timeOrigin: "visible",
|
|
93
|
-
layer: "front",
|
|
94
259
|
seed: "pulse-title",
|
|
95
260
|
params: { amount: 0.04, period: 1600 },
|
|
96
|
-
assetRefs: [],
|
|
97
|
-
metadata: {},
|
|
98
261
|
},
|
|
99
262
|
"effect:float": {
|
|
100
263
|
id: "float-hero",
|
|
101
264
|
target: { type: "item", itemId: "hero" },
|
|
102
265
|
kind: "effects.float",
|
|
103
266
|
kindVersion: 1,
|
|
104
|
-
enabled: true,
|
|
105
267
|
lifecycle: "when-visible",
|
|
106
268
|
timeOrigin: "visible",
|
|
107
|
-
layer: "front",
|
|
108
269
|
seed: "float-hero",
|
|
109
270
|
params: { distance: 8, period: 2200 },
|
|
110
|
-
assetRefs: [],
|
|
111
|
-
metadata: {},
|
|
112
271
|
},
|
|
113
272
|
"composition:fade": {
|
|
114
273
|
id: "intro",
|
|
@@ -119,63 +278,87 @@ const templates = {
|
|
|
119
278
|
id: "title-opacity",
|
|
120
279
|
target: { type: "item", itemId: "title" },
|
|
121
280
|
channel: "style.opacity",
|
|
122
|
-
channelVersion: 1,
|
|
123
|
-
interpolation: "linear",
|
|
124
281
|
fill: "both",
|
|
125
282
|
keyframes: [
|
|
126
283
|
{ time: 0, value: 0 },
|
|
127
284
|
{ time: 800, value: 1, easing: "ease-out-cubic" },
|
|
128
285
|
],
|
|
129
|
-
metadata: {},
|
|
130
286
|
}],
|
|
131
|
-
clips: [],
|
|
132
|
-
markers: [],
|
|
133
287
|
},
|
|
134
|
-
playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
|
|
135
|
-
metadata: {},
|
|
136
288
|
},
|
|
137
289
|
"composition:reveal": {
|
|
138
290
|
id: "reveal",
|
|
139
291
|
name: "Reveal",
|
|
140
292
|
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: [],
|
|
293
|
+
duration: 600,
|
|
294
|
+
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
295
|
},
|
|
146
|
-
playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
|
|
147
|
-
metadata: {},
|
|
148
296
|
},
|
|
149
297
|
"composition:motion-path": {
|
|
150
298
|
id: "motion",
|
|
151
299
|
name: "Motion path",
|
|
152
300
|
timeline: {
|
|
153
301
|
duration: 1200,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
302
|
+
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" }],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
"composition:draw-reveal": {
|
|
306
|
+
id: "draw-stroke",
|
|
307
|
+
name: "Draw stroke",
|
|
308
|
+
timeline: {
|
|
309
|
+
duration: 900,
|
|
310
|
+
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" }],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
"composition:trail": {
|
|
314
|
+
id: "move-with-trail",
|
|
315
|
+
name: "Move with trail",
|
|
316
|
+
timeline: {
|
|
317
|
+
duration: 1200,
|
|
318
|
+
clips: [
|
|
319
|
+
{ 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" },
|
|
320
|
+
{ id: "trail-hero", kind: "effects.trail", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, params: { alpha: 0.48 }, seed: "trail-hero" },
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
"composition:impact": {
|
|
325
|
+
id: "impact",
|
|
326
|
+
name: "Impact",
|
|
327
|
+
timeline: {
|
|
328
|
+
duration: 500,
|
|
329
|
+
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" }],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"composition:flash": {
|
|
333
|
+
id: "flash",
|
|
334
|
+
name: "Flash",
|
|
335
|
+
timeline: {
|
|
336
|
+
duration: 300,
|
|
337
|
+
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
338
|
},
|
|
158
|
-
playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
|
|
159
|
-
metadata: {},
|
|
160
339
|
},
|
|
161
340
|
"composition:particles": {
|
|
162
341
|
id: "celebrate",
|
|
163
342
|
name: "Celebrate",
|
|
164
343
|
timeline: {
|
|
165
344
|
duration: 1000,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
345
|
+
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" }],
|
|
346
|
+
},
|
|
347
|
+
playback: { endBehavior: "reset" },
|
|
348
|
+
},
|
|
349
|
+
"composition:camera-shake": {
|
|
350
|
+
id: "shake",
|
|
351
|
+
name: "Camera shake",
|
|
352
|
+
timeline: {
|
|
353
|
+
duration: 500,
|
|
354
|
+
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
355
|
},
|
|
170
|
-
playback: { loop: false, endBehavior: "reset", reducedMotion: { mode: "base" } },
|
|
171
|
-
metadata: {},
|
|
172
356
|
},
|
|
173
357
|
"composition:camera-focus": {
|
|
174
358
|
id: "tour",
|
|
175
359
|
name: "Tour",
|
|
176
360
|
timeline: {
|
|
177
361
|
duration: 1000,
|
|
178
|
-
tracks: [],
|
|
179
362
|
clips: [{
|
|
180
363
|
id: "focus-title",
|
|
181
364
|
kind: "camera.focus",
|
|
@@ -187,14 +370,9 @@ const templates = {
|
|
|
187
370
|
fill: "forwards",
|
|
188
371
|
easing: "ease-out-cubic",
|
|
189
372
|
params: { focus: { type: "item", itemId: "title" }, fit: "contain", padding: 32 },
|
|
190
|
-
assetRefs: [],
|
|
191
373
|
seed: "focus-title",
|
|
192
|
-
metadata: {},
|
|
193
374
|
}],
|
|
194
|
-
markers: [],
|
|
195
375
|
},
|
|
196
|
-
playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
|
|
197
|
-
metadata: {},
|
|
198
376
|
},
|
|
199
377
|
};
|
|
200
378
|
export const BOARD_EXAMPLE_KEYS = Object.keys(templates);
|
|
@@ -206,19 +384,28 @@ export function boardExample(kind, type) {
|
|
|
206
384
|
return template;
|
|
207
385
|
}
|
|
208
386
|
export function registerBoardExampleCommands(boards) {
|
|
209
|
-
boards.command("examples
|
|
387
|
+
boards.command("examples [kind] [type]")
|
|
210
388
|
.description("Print editable semantic JSON templates")
|
|
389
|
+
.option("--list", "List available examples")
|
|
211
390
|
.addHelpText("after", `
|
|
212
391
|
Kinds:
|
|
213
|
-
create
|
|
392
|
+
create [workflow|media|animation]
|
|
393
|
+
batch basic
|
|
214
394
|
item text|image|video|audio|file|task|geo|frame|draw|arrow
|
|
215
395
|
effect pulse|float
|
|
216
|
-
composition fade|reveal|motion-path|particles|camera-focus
|
|
396
|
+
composition fade|reveal|draw-reveal|motion-path|trail|impact|flash|particles|camera-shake|camera-focus
|
|
217
397
|
|
|
218
398
|
Examples:
|
|
399
|
+
cohub boards examples --list
|
|
400
|
+
cohub boards examples create workflow > board.json
|
|
401
|
+
cohub boards examples batch basic > changes.json
|
|
219
402
|
cohub boards examples item text > item.json
|
|
220
403
|
cohub boards examples composition fade > intro.json`)
|
|
221
|
-
.action((kind, type) => {
|
|
404
|
+
.action((kind, type, options) => {
|
|
405
|
+
if (options.list)
|
|
406
|
+
return json(BOARD_EXAMPLE_KEYS);
|
|
407
|
+
if (!kind)
|
|
408
|
+
throw new Error("Example kind is required. Use --list to see available examples.");
|
|
222
409
|
json(boardExample(kind, type));
|
|
223
410
|
});
|
|
224
411
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { BoardAuthoringItemSchema, BoardItemPatchSchema, } from "@neta-art/cohub";
|
|
3
|
+
import { computeArrowFrame, computeDrawBounds } from "@neta-art/cohub/board";
|
|
3
4
|
import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
|
|
4
5
|
import { handleHttp, json, jsonRequested, ok, table } from "../../output.js";
|
|
5
|
-
import { resolvedBoard, withJson, } from "./context.js";
|
|
6
|
+
import { readInput, readOptions, resolvedBoard, withJson, } from "./context.js";
|
|
6
7
|
const baseVersion = (value, fallback) => {
|
|
7
8
|
if (value === undefined)
|
|
8
9
|
return fallback;
|
|
@@ -38,23 +39,39 @@ function mutationOptions(command) {
|
|
|
38
39
|
.option("--mutation-id <id>", "Stable id for safe retries")
|
|
39
40
|
.option("--dry-run", "Validate on the server (references, version, cascade) without writing");
|
|
40
41
|
}
|
|
42
|
+
function itemLayout(item) {
|
|
43
|
+
if ("position" in item) {
|
|
44
|
+
const size = "size" in item ? item.size : undefined;
|
|
45
|
+
return { x: item.position.x, y: item.position.y, width: size?.width ?? "", height: size?.height ?? "" };
|
|
46
|
+
}
|
|
47
|
+
if (item.type === "draw") {
|
|
48
|
+
const points = item.props.points;
|
|
49
|
+
const strokeWidth = item.style?.strokeWidth ?? 4;
|
|
50
|
+
const bounds = computeDrawBounds(points, strokeWidth);
|
|
51
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
52
|
+
}
|
|
53
|
+
const bounds = computeArrowFrame({
|
|
54
|
+
start: item.props.start,
|
|
55
|
+
end: item.props.end,
|
|
56
|
+
bend: item.props.bend,
|
|
57
|
+
size: item.style?.strokeWidth ?? 2.5,
|
|
58
|
+
});
|
|
59
|
+
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
|
60
|
+
}
|
|
41
61
|
export function registerBoardItemCommands(boards) {
|
|
42
62
|
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"))
|
|
63
|
+
readOptions(withJson(items.command("list <board>").alias("ls").description("List semantic Board items")))
|
|
44
64
|
.action(async (target, options) => {
|
|
45
65
|
try {
|
|
46
66
|
const board = await resolvedBoard(boards, target);
|
|
47
|
-
const snapshot = await board.authoring();
|
|
67
|
+
const snapshot = await board.authoring(readInput(options, "items"));
|
|
48
68
|
const items = snapshot.items ?? [];
|
|
49
69
|
if (jsonRequested(options))
|
|
50
70
|
return json(items);
|
|
51
71
|
table(items.map((item) => ({
|
|
52
72
|
id: item.id,
|
|
53
73
|
type: item.type,
|
|
54
|
-
|
|
55
|
-
y: item.frame.y,
|
|
56
|
-
width: item.frame.width,
|
|
57
|
-
height: item.frame.height,
|
|
74
|
+
...itemLayout(item),
|
|
58
75
|
})), [
|
|
59
76
|
{ key: "id", label: "ID" },
|
|
60
77
|
{ key: "type", label: "TYPE" },
|
|
@@ -68,12 +85,39 @@ export function registerBoardItemCommands(boards) {
|
|
|
68
85
|
handleHttp(cause);
|
|
69
86
|
}
|
|
70
87
|
});
|
|
88
|
+
withJson(items.command("get <board> <item-id>").description("Get one complete Board item"))
|
|
89
|
+
.action(async (target, itemId, options) => {
|
|
90
|
+
try {
|
|
91
|
+
const board = await resolvedBoard(boards, target);
|
|
92
|
+
const snapshot = await board.authoring({ include: ["items"], itemIds: [itemId] });
|
|
93
|
+
const item = snapshot.items?.[0];
|
|
94
|
+
if (!item)
|
|
95
|
+
throw new Error(`Item not found: ${itemId}`);
|
|
96
|
+
if (jsonRequested(options))
|
|
97
|
+
return json(item);
|
|
98
|
+
table([{
|
|
99
|
+
id: item.id,
|
|
100
|
+
type: item.type,
|
|
101
|
+
...itemLayout(item),
|
|
102
|
+
}], [
|
|
103
|
+
{ key: "id", label: "ID" },
|
|
104
|
+
{ key: "type", label: "TYPE" },
|
|
105
|
+
{ key: "x", label: "X" },
|
|
106
|
+
{ key: "y", label: "Y" },
|
|
107
|
+
{ key: "width", label: "WIDTH" },
|
|
108
|
+
{ key: "height", label: "HEIGHT" },
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
catch (cause) {
|
|
112
|
+
handleHttp(cause);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
71
115
|
mutationOptions(items.command("create <board>")
|
|
72
116
|
.description("Create one item")
|
|
73
117
|
.requiredOption("-i, --input <file>", "Board Item JSON; use - for stdin")
|
|
74
118
|
.addHelpText("after", `
|
|
75
119
|
Minimal text item:
|
|
76
|
-
{"id":"title","type":"text","
|
|
120
|
+
{"id":"title","type":"text","position":{"x":120,"y":80},"size":{"width":320,"height":48},"props":{"text":"Launch plan","fontSize":32},"style":{"color":"brand"}}`))
|
|
77
121
|
.action(async (target, options) => {
|
|
78
122
|
try {
|
|
79
123
|
const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
@@ -92,7 +136,7 @@ Minimal text item:
|
|
|
92
136
|
.addHelpText("after", `
|
|
93
137
|
Objects merge recursively, arrays replace, and null clears optional fields.
|
|
94
138
|
Move and rename a text item without replacing its size or font:
|
|
95
|
-
{"
|
|
139
|
+
{"position":{"x":160,"y":120},"props":{"text":"Updated"}}`))
|
|
96
140
|
.action(async (target, itemId, options) => {
|
|
97
141
|
try {
|
|
98
142
|
const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
|
|
@@ -7,4 +7,11 @@ export declare function parseViewport(value?: string): {
|
|
|
7
7
|
width: number;
|
|
8
8
|
height: number;
|
|
9
9
|
} | undefined;
|
|
10
|
+
export declare function showCreated(result: {
|
|
11
|
+
board: {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
version: number;
|
|
15
|
+
};
|
|
16
|
+
}, path: string): void;
|
|
10
17
|
export declare function registerBoards(program: Command): Command;
|
package/dist/commands/boards.js
CHANGED
|
@@ -38,8 +38,9 @@ export function parseViewport(value) {
|
|
|
38
38
|
throw new Error("viewport width and height must be greater than zero");
|
|
39
39
|
return { x, y, width, height };
|
|
40
40
|
}
|
|
41
|
-
function showCreated(result) {
|
|
42
|
-
table([result.board], [
|
|
41
|
+
export function showCreated(result, path) {
|
|
42
|
+
table([{ path, ...result.board }], [
|
|
43
|
+
{ key: "path", label: "Path" },
|
|
43
44
|
{ key: "id", label: "ID" },
|
|
44
45
|
{ key: "title", label: "Title" },
|
|
45
46
|
{ key: "version", label: "Version" },
|
|
@@ -211,7 +212,7 @@ function registerExportCommand(boards) {
|
|
|
211
212
|
export function registerBoards(program) {
|
|
212
213
|
const boards = program
|
|
213
214
|
.command("boards")
|
|
214
|
-
.description("Inspect and update Boards")
|
|
215
|
+
.description("Inspect and update Boards by ID or .board path")
|
|
215
216
|
.hook("preAction", () => { resolveSpace(boards); });
|
|
216
217
|
withJson(boards.command("create <path>")
|
|
217
218
|
.description("Create a Board")
|
|
@@ -240,9 +241,9 @@ Generate an editable seed:
|
|
|
240
241
|
};
|
|
241
242
|
const result = await createClient().space(resolveSpace(boards)).boards.create(input);
|
|
242
243
|
if (jsonRequested(options))
|
|
243
|
-
return outJson(result);
|
|
244
|
-
ok(`Board created: ${
|
|
245
|
-
showCreated(result);
|
|
244
|
+
return outJson({ ...result, path });
|
|
245
|
+
ok(`Board created: ${path}`);
|
|
246
|
+
showCreated(result, path);
|
|
246
247
|
}
|
|
247
248
|
catch (cause) {
|
|
248
249
|
handleHttp(cause);
|
|
@@ -252,10 +253,13 @@ Generate an editable seed:
|
|
|
252
253
|
.alias("get")
|
|
253
254
|
.description("Show Board metadata and semantic resource counts")
|
|
254
255
|
.addHelpText("after", `
|
|
255
|
-
Inspect
|
|
256
|
+
Inspect resources with:
|
|
256
257
|
cohub boards items list <board> --json
|
|
258
|
+
cohub boards connections list <board> --json
|
|
257
259
|
cohub boards effects list <board> --json
|
|
258
|
-
cohub boards compositions list <board> --json
|
|
260
|
+
cohub boards compositions list <board> --json
|
|
261
|
+
Apply multiple changes atomically:
|
|
262
|
+
cohub boards batch <board> --input changes.json --dry-run`))
|
|
259
263
|
.action(async (target, options) => {
|
|
260
264
|
try {
|
|
261
265
|
const spaceId = resolveSpace(boards);
|
|
@@ -273,6 +277,9 @@ Inspect content with:
|
|
|
273
277
|
.description("Show authoring schemas and runtime capabilities")
|
|
274
278
|
.addHelpText("after", `
|
|
275
279
|
Use --json for complete Item, patch, mutation, effect, Composition, and create JSON Schemas.
|
|
280
|
+
Coordinates:
|
|
281
|
+
draw points and arrow start/end points are world-space.
|
|
282
|
+
Position and size are optional; Board geometry derives the persisted frame automatically.
|
|
276
283
|
Use boards examples for editable starter JSON.`))
|
|
277
284
|
.action(async (target, options) => {
|
|
278
285
|
try {
|
|
@@ -314,7 +321,8 @@ Use boards examples for editable starter JSON.`))
|
|
|
314
321
|
});
|
|
315
322
|
registerBoardDomainCommands(boards);
|
|
316
323
|
registerExportCommand(boards);
|
|
317
|
-
|
|
324
|
+
const playback = boards.command("playback").description("Control shared Board playback");
|
|
325
|
+
withJson(playback.command("play <board> <composition-id>")
|
|
318
326
|
.description("Start shared playback")
|
|
319
327
|
.option("--position <time>", "Initial position in milliseconds")
|
|
320
328
|
.option("--time-scale <scale>", "Playback speed from 0 to 4")
|
|
@@ -358,11 +366,11 @@ Use boards examples for editable starter JSON.`))
|
|
|
358
366
|
handleHttp(cause);
|
|
359
367
|
}
|
|
360
368
|
};
|
|
361
|
-
withJson(
|
|
369
|
+
withJson(playback.command("pause <board> <playback-id>")
|
|
362
370
|
.description("Pause playback")
|
|
363
371
|
.option("--command-id <id>", "Idempotency command ID"))
|
|
364
372
|
.action(playbackAction("pause"));
|
|
365
|
-
withJson(
|
|
373
|
+
withJson(playback.command("seek <board> <playback-id> <position>")
|
|
366
374
|
.description("Seek playback")
|
|
367
375
|
.option("--command-id <id>", "Idempotency command ID"))
|
|
368
376
|
.action(async (target, playbackId, position, options) => {
|
|
@@ -383,7 +391,7 @@ Use boards examples for editable starter JSON.`))
|
|
|
383
391
|
handleHttp(cause);
|
|
384
392
|
}
|
|
385
393
|
});
|
|
386
|
-
withJson(
|
|
394
|
+
withJson(playback.command("stop <board> <playback-id>")
|
|
387
395
|
.description("Stop playback")
|
|
388
396
|
.option("--command-id <id>", "Idempotency command ID"))
|
|
389
397
|
.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.4.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"pixi.js": "^8.20.1",
|
|
21
21
|
"sharp": "^0.35.4",
|
|
22
|
-
"@neta-art/cohub": "8.
|
|
22
|
+
"@neta-art/cohub": "8.7.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
@@ -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
|
-
}
|