@neta-art/cohub-cli 6.3.0 → 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.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Board image export for the CLI.
3
3
  *
4
- * Rendering runs through the same PixiJS card renderers the web editor uses (see
5
- * `@neta-art/cohub/board`), on a Canvas2D backend. Everything platform-specific
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,8 +1,9 @@
1
1
  /**
2
2
  * Board image export for the CLI.
3
3
  *
4
- * Rendering runs through the same PixiJS card renderers the web editor uses (see
5
- * `@neta-art/cohub/board`), on a Canvas2D backend. Everything platform-specific
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,5 +1,4 @@
1
1
  import { json } from "../../output.js";
2
- const frame = { x: 120, y: 80, width: 320, height: 48, rotation: 0 };
3
2
  const templates = {
4
3
  "batch:basic": {
5
4
  commands: [
@@ -11,7 +10,9 @@ const templates = {
11
10
  items: [{
12
11
  id: "title",
13
12
  type: "text",
14
- frame,
13
+ position: { x: 120, y: 80 },
14
+ size: { width: 320, height: 48 },
15
+ rotation: 0,
15
16
  props: { text: "Launch plan", fontSize: 32 },
16
17
  style: { color: "brand" },
17
18
  }],
@@ -24,21 +25,27 @@ const templates = {
24
25
  {
25
26
  id: "request",
26
27
  type: "text",
27
- frame: { x: 80, y: 190, width: 180, height: 48, rotation: 0 },
28
+ position: { x: 80, y: 190 },
29
+ size: { width: 180, height: 48 },
30
+ rotation: 0,
28
31
  props: { text: "Request", fontSize: 28 },
29
32
  style: { color: "brand" },
30
33
  },
31
34
  {
32
35
  id: "agent",
33
36
  type: "geo",
34
- frame: { x: 360, y: 150, width: 240, height: 120, rotation: 0 },
37
+ position: { x: 360, y: 150 },
38
+ size: { width: 240, height: 120 },
39
+ rotation: 0,
35
40
  props: { shape: "rounded", text: "Agent" },
36
41
  style: { color: "blue", fillOpacity: 0.08 },
37
42
  },
38
43
  {
39
44
  id: "result",
40
45
  type: "geo",
41
- frame: { x: 700, y: 150, width: 240, height: 120, rotation: 0 },
46
+ position: { x: 700, y: 150 },
47
+ size: { width: 240, height: 120 },
48
+ rotation: 0,
42
49
  props: { shape: "rounded", text: "Result" },
43
50
  style: { color: "green", fillOpacity: 0.08 },
44
51
  },
@@ -55,28 +62,36 @@ const templates = {
55
62
  {
56
63
  id: "hero",
57
64
  type: "image",
58
- frame: { x: 80, y: 100, width: 640, height: 360, rotation: 0 },
65
+ position: { x: 80, y: 100 },
66
+ size: { width: 640, height: 360 },
67
+ rotation: 0,
59
68
  source: { kind: "space-file", path: "assets/hero.webp" },
60
69
  props: {},
61
70
  },
62
71
  {
63
72
  id: "demo-video",
64
73
  type: "video",
65
- frame: { x: 760, y: 100, width: 480, height: 270, rotation: 0 },
74
+ position: { x: 760, y: 100 },
75
+ size: { width: 480, height: 270 },
76
+ rotation: 0,
66
77
  source: { kind: "space-file", path: "assets/demo.mp4" },
67
78
  props: {},
68
79
  },
69
80
  {
70
81
  id: "soundtrack",
71
82
  type: "audio",
72
- frame: { x: 80, y: 500, width: 400, height: 96, rotation: 0 },
83
+ position: { x: 80, y: 500 },
84
+ size: { width: 400, height: 96 },
85
+ rotation: 0,
73
86
  source: { kind: "space-file", path: "assets/soundtrack.mp3" },
74
87
  props: {},
75
88
  },
76
89
  {
77
90
  id: "brief",
78
91
  type: "file",
79
- frame: { x: 520, y: 500, width: 320, height: 180, rotation: 0 },
92
+ position: { x: 520, y: 500 },
93
+ size: { width: 320, height: 180 },
94
+ rotation: 0,
80
95
  source: { kind: "space-file", path: "docs/brief.md" },
81
96
  props: {},
82
97
  },
@@ -90,14 +105,16 @@ const templates = {
90
105
  {
91
106
  id: "title",
92
107
  type: "text",
93
- frame: { x: 120, y: 80, width: 320, height: 48, rotation: 0 },
108
+ position: { x: 120, y: 80 },
109
+ size: { width: 320, height: 48 },
110
+ rotation: 0,
94
111
  props: { text: "Launch", fontSize: 32 },
95
112
  style: { color: "brand" },
96
113
  },
97
114
  {
98
115
  id: "stroke",
99
116
  type: "draw",
100
- frame: { x: 120, y: 180, width: 180, height: 100, rotation: 0 },
117
+ rotation: 0,
101
118
  props: { points: [{ x: 2, y: 98, p: 0.5 }, { x: 90, y: 2, p: 0.5 }, { x: 178, y: 98, p: 0.5 }] },
102
119
  style: { color: "violet", strokeWidth: 4 },
103
120
  },
@@ -150,70 +167,86 @@ const templates = {
150
167
  "item:text": {
151
168
  id: "title",
152
169
  type: "text",
153
- frame,
170
+ position: { x: 120, y: 80 },
171
+ size: { width: 320, height: 48 },
172
+ rotation: 0,
154
173
  props: { text: "Launch plan", fontSize: 32 },
155
174
  style: { color: "brand" },
156
175
  },
157
176
  "item:image": {
158
177
  id: "hero",
159
178
  type: "image",
160
- frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
179
+ position: { x: 120, y: 160 },
180
+ size: { width: 640, height: 360 },
181
+ rotation: 0,
161
182
  source: { kind: "space-file", path: "assets/hero.webp" },
162
183
  props: {},
163
184
  },
164
185
  "item:geo": {
165
186
  id: "goal",
166
187
  type: "geo",
167
- frame: { x: 120, y: 160, width: 280, height: 140, rotation: 0 },
188
+ position: { x: 120, y: 160 },
189
+ size: { width: 280, height: 140 },
190
+ rotation: 0,
168
191
  props: { shape: "rounded", text: "Ship" },
169
192
  style: { color: "green", fillOpacity: 0.12 },
170
193
  },
171
194
  "item:frame": {
172
195
  id: "scene",
173
196
  type: "frame",
174
- frame: { x: 80, y: 60, width: 960, height: 540, rotation: 0 },
197
+ position: { x: 80, y: 60 },
198
+ size: { width: 960, height: 540 },
199
+ rotation: 0,
175
200
  props: { label: "Scene 1" },
176
201
  style: { color: "neutral" },
177
202
  },
178
203
  "item:draw": {
179
204
  id: "stroke",
180
205
  type: "draw",
181
- frame: { x: 100, y: 100, width: 180, height: 100, rotation: 0 },
182
- props: { points: [{ x: 2, y: 98, p: 0.5 }, { x: 90, y: 2, p: 0.5 }, { x: 178, y: 98, p: 0.5 }] },
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 }] },
183
208
  style: { color: "violet", strokeWidth: 4 },
184
209
  },
185
210
  "item:arrow": {
186
211
  id: "arrow",
187
212
  type: "arrow",
188
- frame: { x: 100, y: 100, width: 260, height: 100, rotation: 0 },
213
+ rotation: 0,
189
214
  props: { start: { x: 116, y: 150 }, end: { x: 344, y: 150 }, bend: 0, arrowStart: false, arrowEnd: true, label: "next" },
190
215
  style: { color: "brand", strokeWidth: 2.5 },
191
216
  },
192
217
  "item:video": {
193
218
  id: "demo-video",
194
219
  type: "video",
195
- frame: { x: 120, y: 160, width: 640, height: 360, rotation: 0 },
220
+ position: { x: 120, y: 160 },
221
+ size: { width: 640, height: 360 },
222
+ rotation: 0,
196
223
  source: { kind: "space-file", path: "assets/demo.mp4" },
197
224
  props: {},
198
225
  },
199
226
  "item:audio": {
200
227
  id: "soundtrack",
201
228
  type: "audio",
202
- frame: { x: 120, y: 540, width: 480, height: 96, rotation: 0 },
229
+ position: { x: 120, y: 540 },
230
+ size: { width: 480, height: 96 },
231
+ rotation: 0,
203
232
  source: { kind: "space-file", path: "assets/soundtrack.mp3" },
204
233
  props: {},
205
234
  },
206
235
  "item:file": {
207
236
  id: "brief",
208
237
  type: "file",
209
- frame: { x: 120, y: 160, width: 360, height: 220, rotation: 0 },
238
+ position: { x: 120, y: 160 },
239
+ size: { width: 360, height: 220 },
240
+ rotation: 0,
210
241
  source: { kind: "space-file", path: "docs/brief.md" },
211
242
  props: {},
212
243
  },
213
244
  "item:task": {
214
245
  id: "task",
215
246
  type: "task",
216
- frame: { x: 120, y: 160, width: 420, height: 240, rotation: 0 },
247
+ position: { x: 120, y: 160 },
248
+ size: { width: 420, height: 240 },
249
+ rotation: 0,
217
250
  props: { taskRunId: "task-run-id", snapshot: { taskType: "generation", status: "running", title: "Generate concept", artifactCount: 0, artifacts: [] } },
218
251
  },
219
252
  "effect:pulse": {
@@ -1,5 +1,6 @@
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
6
  import { readInput, readOptions, resolvedBoard, withJson, } from "./context.js";
@@ -38,6 +39,25 @@ 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
63
  readOptions(withJson(items.command("list <board>").alias("ls").description("List semantic Board items")))
@@ -51,10 +71,7 @@ export function registerBoardItemCommands(boards) {
51
71
  table(items.map((item) => ({
52
72
  id: item.id,
53
73
  type: item.type,
54
- x: item.frame.x,
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" },
@@ -81,10 +98,7 @@ export function registerBoardItemCommands(boards) {
81
98
  table([{
82
99
  id: item.id,
83
100
  type: item.type,
84
- x: item.frame.x,
85
- y: item.frame.y,
86
- width: item.frame.width,
87
- height: item.frame.height,
101
+ ...itemLayout(item),
88
102
  }], [
89
103
  { key: "id", label: "ID" },
90
104
  { key: "type", label: "TYPE" },
@@ -103,7 +117,7 @@ export function registerBoardItemCommands(boards) {
103
117
  .requiredOption("-i, --input <file>", "Board Item JSON; use - for stdin")
104
118
  .addHelpText("after", `
105
119
  Minimal text item:
106
- {"id":"title","type":"text","frame":{"x":120,"y":80,"width":320,"height":48,"rotation":0},"props":{"text":"Launch plan","fontSize":32},"style":{"color":"brand"}}`))
120
+ {"id":"title","type":"text","position":{"x":120,"y":80},"size":{"width":320,"height":48},"props":{"text":"Launch plan","fontSize":32},"style":{"color":"brand"}}`))
107
121
  .action(async (target, options) => {
108
122
  try {
109
123
  const value = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
@@ -122,7 +136,7 @@ Minimal text item:
122
136
  .addHelpText("after", `
123
137
  Objects merge recursively, arrays replace, and null clears optional fields.
124
138
  Move and rename a text item without replacing its size or font:
125
- {"frame":{"x":160,"y":120},"props":{"text":"Updated"}}`))
139
+ {"position":{"x":160,"y":120},"props":{"text":"Updated"}}`))
126
140
  .action(async (target, itemId, options) => {
127
141
  try {
128
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;
@@ -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" },
@@ -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: ${result.board.id}`);
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);
@@ -276,6 +277,9 @@ Apply multiple changes atomically:
276
277
  .description("Show authoring schemas and runtime capabilities")
277
278
  .addHelpText("after", `
278
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.
279
283
  Use boards examples for editable starter JSON.`))
280
284
  .action(async (target, options) => {
281
285
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.3.0",
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.6.0"
22
+ "@neta-art/cohub": "8.7.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"