@neta-art/cohub-cli 3.12.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ import { json } from "../../output.js";
2
+ const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
3
+ const templates = {
4
+ "create": {
5
+ items: [{
6
+ id: "title",
7
+ type: "text",
8
+ frame,
9
+ props: { text: "Launch plan", fontSize: 32 },
10
+ style: { color: "brand" },
11
+ }],
12
+ connections: [],
13
+ effects: [],
14
+ compositions: [],
15
+ },
16
+ "item:text": {
17
+ id: "title",
18
+ type: "text",
19
+ frame,
20
+ props: { text: "Launch plan", fontSize: 32 },
21
+ style: { color: "brand" },
22
+ },
23
+ "item:image": {
24
+ id: "hero",
25
+ type: "image",
26
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
27
+ source: { kind: "space-file", path: "assets/hero.webp" },
28
+ props: {},
29
+ },
30
+ "item:geo": {
31
+ id: "goal",
32
+ type: "geo",
33
+ frame: { x: 120, y: 160, width: 280, height: 140, rotation: 0 },
34
+ props: { shape: "rounded", text: "Ship" },
35
+ style: { color: "green", fillOpacity: 0.12 },
36
+ },
37
+ "item:frame": {
38
+ id: "scene",
39
+ type: "frame",
40
+ frame: { x: 80, y: 60, width: 960, height: 540, rotation: 0 },
41
+ props: { label: "Scene 1" },
42
+ style: { color: "neutral" },
43
+ },
44
+ "item:draw": {
45
+ id: "stroke",
46
+ type: "draw",
47
+ frame: { x: 100, y: 100, width: 180, height: 100, rotation: 0 },
48
+ props: { points: [{ x: 0, y: 80, p: 0.5 }, { x: 90, y: 10, p: 0.8 }, { x: 180, y: 70, p: 0.5 }] },
49
+ style: { color: "violet", strokeWidth: 4 },
50
+ },
51
+ "item:arrow": {
52
+ id: "arrow",
53
+ type: "arrow",
54
+ frame: { x: 100, y: 100, width: 260, height: 100, rotation: 0 },
55
+ props: { start: { x: 116, y: 150 }, end: { x: 344, y: 150 }, bend: 0, arrowStart: false, arrowEnd: true, label: "next" },
56
+ style: { color: "brand", strokeWidth: 2.5 },
57
+ },
58
+ "item:video": {
59
+ id: "demo-video",
60
+ type: "video",
61
+ frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
62
+ source: { kind: "space-file", path: "assets/demo.mp4" },
63
+ props: {},
64
+ },
65
+ "item:audio": {
66
+ id: "soundtrack",
67
+ type: "audio",
68
+ frame: { x: 120, y: 540, width: 480, height: 96, rotation: 0 },
69
+ source: { kind: "space-file", path: "assets/soundtrack.mp3" },
70
+ props: {},
71
+ },
72
+ "item:file": {
73
+ id: "brief",
74
+ type: "file",
75
+ frame: { x: 120, y: 160, width: 360, height: 220, rotation: 0 },
76
+ source: { kind: "space-file", path: "docs/brief.md" },
77
+ props: {},
78
+ },
79
+ "item:task": {
80
+ id: "task",
81
+ type: "task",
82
+ frame: { x: 120, y: 160, width: 420, height: 240, rotation: 0 },
83
+ props: { taskRunId: "task-run-id", snapshot: { taskType: "generation", status: "running", title: "Generate concept", artifactCount: 0, artifacts: [] } },
84
+ },
85
+ "effect:pulse": {
86
+ id: "pulse-title",
87
+ target: { type: "item", itemId: "title" },
88
+ kind: "effects.pulse",
89
+ kindVersion: 1,
90
+ enabled: true,
91
+ lifecycle: "when-visible",
92
+ timeOrigin: "visible",
93
+ layer: "front",
94
+ seed: "pulse-title",
95
+ params: { amount: 0.04, period: 1600 },
96
+ assetRefs: [],
97
+ metadata: {},
98
+ },
99
+ "effect:float": {
100
+ id: "float-hero",
101
+ target: { type: "item", itemId: "hero" },
102
+ kind: "effects.float",
103
+ kindVersion: 1,
104
+ enabled: true,
105
+ lifecycle: "when-visible",
106
+ timeOrigin: "visible",
107
+ layer: "front",
108
+ seed: "float-hero",
109
+ params: { distance: 8, period: 2200 },
110
+ assetRefs: [],
111
+ metadata: {},
112
+ },
113
+ "composition:fade": {
114
+ id: "intro",
115
+ name: "Intro",
116
+ timeline: {
117
+ duration: 800,
118
+ tracks: [{
119
+ id: "title-opacity",
120
+ target: { type: "item", itemId: "title" },
121
+ channel: "style.opacity",
122
+ channelVersion: 1,
123
+ interpolation: "linear",
124
+ fill: "both",
125
+ keyframes: [
126
+ { time: 0, value: 0 },
127
+ { time: 800, value: 1, easing: "ease-out-cubic" },
128
+ ],
129
+ metadata: {},
130
+ }],
131
+ clips: [],
132
+ markers: [],
133
+ },
134
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
135
+ metadata: {},
136
+ },
137
+ "composition:reveal": {
138
+ id: "reveal",
139
+ name: "Reveal",
140
+ timeline: {
141
+ duration: 900,
142
+ tracks: [{ id: "title-opacity", target: { type: "item", itemId: "title" }, channel: "style.opacity", channelVersion: 1, interpolation: "linear", fill: "both", keyframes: [{ time: 0, value: 0 }, { time: 600, value: 1 }], metadata: {} }],
143
+ clips: [{ id: "reveal-title", kind: "text.reveal", kindVersion: 1, target: { type: "item", itemId: "title" }, start: 0, duration: 600, layer: "content", fill: "both", easing: "ease-out-cubic", params: { mode: "words" }, assetRefs: [], seed: "reveal-title", metadata: {} }],
144
+ markers: [],
145
+ },
146
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
147
+ metadata: {},
148
+ },
149
+ "composition:motion-path": {
150
+ id: "motion",
151
+ name: "Motion path",
152
+ timeline: {
153
+ duration: 1200,
154
+ tracks: [],
155
+ clips: [{ id: "move-hero", kind: "motion.path", kindVersion: 1, target: { type: "item", itemId: "hero" }, start: 0, duration: 1200, layer: "content", fill: "both", easing: "ease-in-out-cubic", params: { points: [{ x: 0, y: 0 }, { x: 160, y: -40 }, { x: 320, y: 0 }], orient: true }, assetRefs: [], seed: "move-hero", metadata: {} }],
156
+ markers: [],
157
+ },
158
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
159
+ metadata: {},
160
+ },
161
+ "composition:particles": {
162
+ id: "celebrate",
163
+ name: "Celebrate",
164
+ timeline: {
165
+ duration: 1000,
166
+ tracks: [],
167
+ clips: [{ id: "particles", kind: "effects.particles", kindVersion: 1, target: { type: "board" }, start: 0, duration: 1000, layer: "front", fill: "none", easing: "linear", params: { count: 420, bounds: { x: 0, y: 0, width: 800, height: 600 } }, assetRefs: [], seed: "particles", metadata: {} }],
168
+ markers: [],
169
+ },
170
+ playback: { loop: false, endBehavior: "reset", reducedMotion: { mode: "base" } },
171
+ metadata: {},
172
+ },
173
+ "composition:camera-focus": {
174
+ id: "tour",
175
+ name: "Tour",
176
+ timeline: {
177
+ duration: 1000,
178
+ tracks: [],
179
+ clips: [{
180
+ id: "focus-title",
181
+ kind: "camera.focus",
182
+ kindVersion: 1,
183
+ target: { type: "camera" },
184
+ start: 0,
185
+ duration: 700,
186
+ layer: "screen",
187
+ fill: "forwards",
188
+ easing: "ease-out-cubic",
189
+ params: { focus: { type: "item", itemId: "title" }, fit: "contain", padding: 32 },
190
+ assetRefs: [],
191
+ seed: "focus-title",
192
+ metadata: {},
193
+ }],
194
+ markers: [],
195
+ },
196
+ playback: { loop: false, endBehavior: "hold", reducedMotion: { mode: "base" } },
197
+ metadata: {},
198
+ },
199
+ };
200
+ export const BOARD_EXAMPLE_KEYS = Object.keys(templates);
201
+ export function boardExample(kind, type) {
202
+ const key = type ? `${kind}:${type}` : kind;
203
+ const template = templates[key];
204
+ if (!template)
205
+ throw new Error(`Unknown Board example: ${key}`);
206
+ return template;
207
+ }
208
+ export function registerBoardExampleCommands(boards) {
209
+ boards.command("examples <kind> [type]")
210
+ .description("Print editable semantic JSON templates")
211
+ .addHelpText("after", `
212
+ Kinds:
213
+ create
214
+ item text|image|video|audio|file|task|geo|frame|draw|arrow
215
+ effect pulse|float
216
+ composition fade|reveal|motion-path|particles|camera-focus
217
+
218
+ Examples:
219
+ cohub boards examples item text > item.json
220
+ cohub boards examples composition fade > intro.json`)
221
+ .action((kind, type) => {
222
+ json(boardExample(kind, type));
223
+ });
224
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerBoardItemCommands(boards: Command): void;
@@ -0,0 +1,140 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { BoardAuthoringItemSchema, BoardItemPatchSchema, } from "@neta-art/cohub";
3
+ import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
4
+ import { handleHttp, json, jsonRequested, ok, table } from "../../output.js";
5
+ import { resolvedBoard, withJson, } from "./context.js";
6
+ const baseVersion = (value, fallback) => {
7
+ if (value === undefined)
8
+ return fallback;
9
+ const parsed = Number(value);
10
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
11
+ throw new Error("base version must be a non-negative integer");
12
+ }
13
+ return parsed;
14
+ };
15
+ async function execute(boards, target, command, options) {
16
+ const board = await resolvedBoard(boards, target);
17
+ const snapshot = await board.summary();
18
+ const mutation = {
19
+ mutationId: options.mutationId?.trim() || randomUUID(),
20
+ baseVersion: baseVersion(options.baseVersion, snapshot.board.version),
21
+ dryRun: Boolean(options.dryRun),
22
+ commands: [command],
23
+ };
24
+ // Server-side validation: schema, version, references, cascade rules — the
25
+ // same checks the real write would run, just without persisting anything.
26
+ const receipt = await board.mutateSemantic(mutation);
27
+ if (jsonRequested(options))
28
+ return json(receipt);
29
+ if (options.dryRun) {
30
+ ok(`Validated against Board version ${receipt.board.version}; no changes written`);
31
+ return;
32
+ }
33
+ ok(`${receipt.replayed ? "Replayed" : "Applied"} mutation at Board version ${receipt.board.version}`);
34
+ }
35
+ function mutationOptions(command) {
36
+ return withJson(command)
37
+ .option("--base-version <version>", "Expected Board version; defaults to latest")
38
+ .option("--mutation-id <id>", "Stable id for safe retries")
39
+ .option("--dry-run", "Validate on the server (references, version, cascade) without writing");
40
+ }
41
+ export function registerBoardItemCommands(boards) {
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"))
44
+ .action(async (target, options) => {
45
+ try {
46
+ const board = await resolvedBoard(boards, target);
47
+ const snapshot = await board.authoring();
48
+ const items = snapshot.items ?? [];
49
+ if (jsonRequested(options))
50
+ return json(items);
51
+ table(items.map((item) => ({
52
+ id: item.id,
53
+ type: item.type,
54
+ x: item.frame.x,
55
+ y: item.frame.y,
56
+ width: item.frame.width,
57
+ height: item.frame.height,
58
+ })), [
59
+ { key: "id", label: "ID" },
60
+ { key: "type", label: "TYPE" },
61
+ { key: "x", label: "X" },
62
+ { key: "y", label: "Y" },
63
+ { key: "width", label: "WIDTH" },
64
+ { key: "height", label: "HEIGHT" },
65
+ ]);
66
+ }
67
+ catch (cause) {
68
+ handleHttp(cause);
69
+ }
70
+ });
71
+ mutationOptions(items.command("create <board>")
72
+ .description("Create one item")
73
+ .requiredOption("-i, --input <file>", "Board Item JSON; use - for stdin")
74
+ .addHelpText("after", `
75
+ Minimal text item:
76
+ {"id":"title","type":"text","frame":{"x":120,"y":80,"width":320,"height":48,"rotation":0},"props":{"text":"Launch plan","fontSize":32},"style":{"color":"brand"}}`))
77
+ .action(async (target, options) => {
78
+ try {
79
+ const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
80
+ await execute(boards, target, {
81
+ type: "item.create",
82
+ item: BoardAuthoringItemSchema.parse(value),
83
+ }, options);
84
+ }
85
+ catch (cause) {
86
+ handleHttp(cause);
87
+ }
88
+ });
89
+ mutationOptions(items.command("patch <board> <item-id>")
90
+ .description("Recursively merge item fields")
91
+ .requiredOption("-i, --input <file>", "Board Item patch JSON; use - for stdin")
92
+ .addHelpText("after", `
93
+ Objects merge recursively, arrays replace, and null clears optional fields.
94
+ Move and rename a text item without replacing its size or font:
95
+ {"frame":{"x":160,"y":120},"props":{"text":"Updated"}}`))
96
+ .action(async (target, itemId, options) => {
97
+ try {
98
+ const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
99
+ await execute(boards, target, {
100
+ type: "item.patch",
101
+ itemId,
102
+ patch: BoardItemPatchSchema.parse(value),
103
+ }, options);
104
+ }
105
+ catch (cause) {
106
+ handleHttp(cause);
107
+ }
108
+ });
109
+ mutationOptions(items.command("replace <board> <item-id>")
110
+ .description("Replace one complete item")
111
+ .requiredOption("-i, --input <file>", "Complete Board Item JSON; use - for stdin"))
112
+ .action(async (target, itemId, options) => {
113
+ try {
114
+ const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
115
+ await execute(boards, target, {
116
+ type: "item.replace",
117
+ itemId,
118
+ item: BoardAuthoringItemSchema.parse(value),
119
+ }, options);
120
+ }
121
+ catch (cause) {
122
+ handleHttp(cause);
123
+ }
124
+ });
125
+ mutationOptions(items.command("delete <board> <item-id>").alias("rm")
126
+ .description("Delete one item")
127
+ .option("--cascade", "Atomically remove relations, effects, and animation references"))
128
+ .action(async (target, itemId, options) => {
129
+ try {
130
+ await execute(boards, target, {
131
+ type: "item.delete",
132
+ itemId,
133
+ cascade: Boolean(options.cascade),
134
+ }, options);
135
+ }
136
+ catch (cause) {
137
+ handleHttp(cause);
138
+ }
139
+ });
140
+ }
@@ -1,2 +1,3 @@
1
1
  import type { Command } from "commander";
2
+ /** Connection shortcuts; Item authoring lives in ./items.ts. */
2
3
  export declare function registerBoardNodeCommands(boards: Command): void;
@@ -1,68 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { boardNodeCreateOperation, boardNodeDeleteOperations, boardNodePatchOperation, createBoardConnection, createBoardNode, } from "@neta-art/cohub/board";
3
- import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
2
+ import { createBoardConnection } from "@neta-art/cohub/board";
3
+ import { mutateSemantic } from "./context.js";
4
4
  import { handleHttp } from "../../output.js";
5
5
  import { resolvedBoard, showUpdated, withJson, } from "./context.js";
6
+ /** Connection shortcuts; Item authoring lives in ./items.ts. */
6
7
  export function registerBoardNodeCommands(boards) {
7
- const nodes = boards.command("nodes").description("Create and update Board nodes");
8
- withJson(nodes.command("add <board>")
9
- .description("Add a node")
10
- .requiredOption("-i, --input <file>", "BoardNodeSpec JSON; use - for stdin")
11
- .addHelpText("after", `
12
- Frame x/y/width/height use Board world units. Draw points and arrow endpoints are also world input.
13
- Minimal text node:
14
- {"id":"title","type":"text","frame":{"x":120,"y":80,"width":320,"height":48},"text":"Launch plan"}`))
15
- .action(async (target, options) => {
16
- try {
17
- const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
18
- const node = createBoardNode(input);
19
- const board = await resolvedBoard(boards, target);
20
- showUpdated(await board.mutate({ build: () => [boardNodeCreateOperation(node)] }), options);
21
- }
22
- catch (cause) {
23
- handleHttp(cause);
24
- }
25
- });
26
- withJson(nodes.command("patch <board> <node-id>")
27
- .description("Patch a node")
28
- .requiredOption("-i, --input <file>", "BoardNodeInput field patch; use - for stdin")
29
- .addHelpText("after", `
30
- x/y are absolute Board world coordinates.
31
- Move a node without changing its content:
32
- {"x":160,"y":120}
33
-
34
- Nested view, style, and data fields replace their complete stored object.`))
35
- .action(async (target, nodeId, options) => {
36
- try {
37
- const patch = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
38
- if ("nodeId" in patch)
39
- throw new Error("node patch must not contain nodeId");
40
- const board = await resolvedBoard(boards, target);
41
- showUpdated(await board.mutate({
42
- build: () => [boardNodePatchOperation(nodeId, patch)],
43
- }), options);
44
- }
45
- catch (cause) {
46
- handleHttp(cause);
47
- }
48
- });
49
- withJson(nodes.command("remove <board> <node-id>")
50
- .alias("rm")
51
- .description("Remove a node and its connections"))
52
- .action(async (target, nodeId, options) => {
53
- try {
54
- const board = await resolvedBoard(boards, target);
55
- showUpdated(await board.mutate({
56
- include: ["connections"],
57
- build: (current) => boardNodeDeleteOperations(nodeId, current.connections),
58
- }), options);
59
- }
60
- catch (cause) {
61
- handleHttp(cause);
62
- }
63
- });
64
8
  withJson(boards.command("connect <board> <source> <target>")
65
- .description("Connect two Board nodes")
9
+ .description("Connect two Board items")
66
10
  .option("--id <id>", "Connection id")
67
11
  .option("--relation <relation>", "Relation type")
68
12
  .option("--direction <direction>", "none, forward, backward, or both", "forward")
@@ -78,17 +22,15 @@ Nested view, style, and data fields replace their complete stored object.`))
78
22
  const board = await resolvedBoard(boards, target);
79
23
  const connection = createBoardConnection({
80
24
  id: options.id ?? randomUUID(),
81
- sourceNodeId: source,
82
- targetNodeId: destination,
25
+ sourceItemId: source,
26
+ targetItemId: destination,
83
27
  relation: options.relation,
84
28
  direction: direction,
85
29
  label: options.label,
86
30
  sourcePortId: options.sourcePort,
87
31
  targetPortId: options.targetPort,
88
32
  });
89
- showUpdated(await board.mutate({
90
- build: () => [{ type: "connection.create", payload: { connection } }],
91
- }), options);
33
+ showUpdated(await mutateSemantic(board, [{ type: "connection.create", connection }]), options);
92
34
  }
93
35
  catch (cause) {
94
36
  handleHttp(cause);
@@ -99,9 +41,7 @@ Nested view, style, and data fields replace their complete stored object.`))
99
41
  .action(async (target, connectionId, options) => {
100
42
  try {
101
43
  const board = await resolvedBoard(boards, target);
102
- showUpdated(await board.mutate({
103
- build: () => [{ type: "connection.delete", payload: { connectionId } }],
104
- }), options);
44
+ showUpdated(await mutateSemantic(board, [{ type: "connection.delete", connectionId }]), options);
105
45
  }
106
46
  catch (cause) {
107
47
  handleHttp(cause);
@@ -1,15 +1,10 @@
1
- import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
2
1
  import type { Command } from "commander";
3
2
  import { parseBoardJsonObject } from "../board-command-support.js";
4
- declare const INSPECT_SECTIONS: readonly ["nodes", "connections", "effects", "sequences", "clips", "playback"];
5
- type InspectSection = (typeof INSPECT_SECTIONS)[number];
6
3
  export declare const parseJsonObject: typeof parseBoardJsonObject;
7
- export declare function readJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
8
- export declare function parseInspectSections(value?: string): InspectSection[] | undefined;
9
- export declare function parseViewport(value?: string): BoardInspectInput["viewport"];
10
- export declare function createTransactionInput(input: Record<string, unknown>, options: {
11
- txId?: string;
12
- baseVersion?: string;
13
- }): BoardTransactionInput;
4
+ export declare function parseViewport(value?: string): {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ } | undefined;
14
10
  export declare function registerBoards(program: Command): Command;
15
- export {};