@neta-art/cohub-cli 8.4.0 → 9.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.
Files changed (84) hide show
  1. package/README.md +25 -2
  2. package/dist/avatar.js +3 -1
  3. package/dist/board-export.d.ts +22 -41
  4. package/dist/board-export.js +178 -115
  5. package/dist/board-fonts.d.ts +7 -0
  6. package/dist/board-fonts.js +10 -0
  7. package/dist/board-kernel/browserAll.js +2760 -0
  8. package/dist/board-kernel/getPo2TextureFromSource.js +13138 -0
  9. package/dist/board-kernel/init.js +867 -0
  10. package/dist/board-kernel/webworkerAll.js +3 -0
  11. package/dist/board-kernel.d.ts +2 -0
  12. package/dist/board-kernel.js +28812 -0
  13. package/dist/board-time.d.ts +3 -0
  14. package/dist/board-time.js +28 -0
  15. package/dist/commands/apps.js +2 -2
  16. package/dist/commands/auth.js +2 -2
  17. package/dist/commands/boards/examples.d.ts +8 -2
  18. package/dist/commands/boards/examples.js +79 -411
  19. package/dist/commands/boards.d.ts +4 -9
  20. package/dist/commands/boards.js +356 -286
  21. package/dist/commands/desktop.d.ts +1 -5
  22. package/dist/commands/desktop.js +10 -19
  23. package/dist/commands/generations.js +1 -1
  24. package/dist/commands/me.js +1 -1
  25. package/dist/commands/public.js +3 -3
  26. package/dist/commands/references.js +3 -0
  27. package/dist/commands/referrals.js +2 -2
  28. package/dist/commands/run.js +3 -3
  29. package/dist/commands/runtime.d.ts +1 -1
  30. package/dist/commands/runtime.js +12 -8
  31. package/dist/commands/search.js +15 -7
  32. package/dist/commands/space-activity.js +3 -5
  33. package/dist/commands/space-displays.d.ts +2 -0
  34. package/dist/commands/space-displays.js +347 -0
  35. package/dist/commands/space-files.d.ts +5 -0
  36. package/dist/commands/space-files.js +215 -0
  37. package/dist/commands/space-invitations.js +3 -3
  38. package/dist/commands/spaces.d.ts +3 -11
  39. package/dist/commands/spaces.js +146 -75
  40. package/dist/fonts/geist-latin-400-normal.woff2 +0 -0
  41. package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
  42. package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
  43. package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
  44. package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
  45. package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
  46. package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
  47. package/dist/index.js +7 -4
  48. package/dist/runtime/launch.d.ts +2 -0
  49. package/dist/runtime/launch.js +35 -8
  50. package/dist/runtime/presentation.d.ts +1 -0
  51. package/dist/runtime/presentation.js +3 -2
  52. package/dist/runtime/supervisor.d.ts +1 -0
  53. package/dist/runtime/supervisor.js +2 -1
  54. package/dist/self-update.d.ts +1 -0
  55. package/dist/self-update.js +52 -16
  56. package/dist/space-files/limit.d.ts +2 -0
  57. package/dist/space-files/limit.js +16 -0
  58. package/dist/space-files/operands.d.ts +16 -0
  59. package/dist/space-files/operands.js +33 -0
  60. package/dist/space-files/transfer.d.ts +53 -0
  61. package/dist/space-files/transfer.js +296 -0
  62. package/dist/space-files/upload.d.ts +14 -0
  63. package/dist/space-files/upload.js +53 -0
  64. package/dist/space.d.ts +18 -16
  65. package/dist/space.js +86 -39
  66. package/dist/web.d.ts +1 -0
  67. package/dist/web.js +6 -0
  68. package/package.json +8 -4
  69. package/dist/commands/board-domain.d.ts +0 -2
  70. package/dist/commands/board-domain.js +0 -16
  71. package/dist/commands/boards/animation.d.ts +0 -2
  72. package/dist/commands/boards/animation.js +0 -171
  73. package/dist/commands/boards/appearance.d.ts +0 -2
  74. package/dist/commands/boards/appearance.js +0 -127
  75. package/dist/commands/boards/batch.d.ts +0 -4
  76. package/dist/commands/boards/batch.js +0 -62
  77. package/dist/commands/boards/connections.d.ts +0 -2
  78. package/dist/commands/boards/connections.js +0 -95
  79. package/dist/commands/boards/context.d.ts +0 -31
  80. package/dist/commands/boards/context.js +0 -71
  81. package/dist/commands/boards/items.d.ts +0 -2
  82. package/dist/commands/boards/items.js +0 -184
  83. package/dist/commands/boards/transactions.d.ts +0 -12
  84. package/dist/commands/boards/transactions.js +0 -69
package/dist/space.js CHANGED
@@ -1,15 +1,26 @@
1
1
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { resolveCohubEnvironment } from "@neta-art/cohub";
4
+ import { HOME_SPACE_SLUG, parseSpaceRef, resolveCohubEnvironment } from "@neta-art/cohub";
5
5
  import { readAuthSession } from "./auth.js";
6
6
  import { createClient } from "./client.js";
7
7
  import { getRuntimeSpaceBinding } from "./runtime/space-binding.js";
8
8
  import { error, handleHttp } from "./output.js";
9
9
  const CONFIG_DIR = join(homedir(), ".config", "cohub");
10
- const CACHE_PATH = join(CONFIG_DIR, "default-space.json");
10
+ const CACHE_PATH = join(CONFIG_DIR, "home-space.json");
11
+ /** Legacy cache that may name another user's Home; never read. */
12
+ const LEGACY_CACHE_PATH = join(CONFIG_DIR, "default-space.json");
11
13
  /** Home space is stable; a one-day TTL bounds how long a stale hit survives. */
12
14
  const CACHE_TTL_MS = 86_400_000;
15
+ const SPACE_REF_HINT = "Use a Space ID, a slug you own such as home, or username/slug.";
16
+ export class SpaceTargetError extends Error {
17
+ detail;
18
+ name = "SpaceTargetError";
19
+ constructor(message, detail) {
20
+ super(message);
21
+ this.detail = detail;
22
+ }
23
+ }
13
24
  function jwtClaim(token, key) {
14
25
  const payload = token?.split(".")[1];
15
26
  if (!payload)
@@ -46,7 +57,7 @@ export function currentIdentityKey() {
46
57
  });
47
58
  }
48
59
  /** Exported for tests; production always uses `CACHE_PATH`. */
49
- export function readDefaultSpaceCache(path, key, now = Date.now()) {
60
+ export function readHomeSpaceCache(path, key, now = Date.now()) {
50
61
  try {
51
62
  const cache = JSON.parse(readFileSync(path, "utf-8"));
52
63
  if (cache.key !== key || typeof cache.spaceId !== "string" || typeof cache.cachedAt !== "number")
@@ -59,21 +70,25 @@ export function readDefaultSpaceCache(path, key, now = Date.now()) {
59
70
  return null;
60
71
  }
61
72
  }
62
- function writeCachedDefaultSpace(key, spaceId) {
73
+ function writeHomeSpaceCache(key, spaceId) {
63
74
  try {
64
75
  mkdirSync(CONFIG_DIR, { recursive: true });
65
76
  const cache = { key, spaceId, cachedAt: Date.now() };
66
77
  writeFileSync(CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
78
+ rmSync(LEGACY_CACHE_PATH, { force: true });
67
79
  }
68
80
  catch {
69
81
  // Cache is best-effort; never fail the command over it.
70
82
  }
71
83
  }
72
- let defaultSpacePromise = null;
73
- export function clearDefaultSpaceCache() {
74
- defaultSpacePromise = null;
84
+ let homeSpacePromise = null;
85
+ const spaceRefs = new Map();
86
+ export function clearHomeSpaceCache() {
87
+ homeSpacePromise = null;
88
+ spaceRefs.clear();
75
89
  try {
76
90
  rmSync(CACHE_PATH, { force: true });
91
+ rmSync(LEGACY_CACHE_PATH, { force: true });
77
92
  }
78
93
  catch {
79
94
  // Best-effort, same as writes.
@@ -90,48 +105,80 @@ export function explicitSpace(program) {
90
105
  }
91
106
  return process.env.COHUB_SPACE_ID?.trim() || null;
92
107
  }
93
- /**
94
- * Resolve the user's home space when no target is given. Cached locally per
95
- * identity so repeated invocations skip the network entirely, and memoized
96
- * in-process so preAction hooks and actions share a single lookup.
97
- * Network and auth failures propagate so callers can report them faithfully.
98
- */
99
- export function resolveDefaultSpace() {
100
- defaultSpacePromise ??= (async () => {
108
+ /** Cached per identity and memoized per process. */
109
+ export function resolveHomeSpace() {
110
+ homeSpacePromise ??= (async () => {
101
111
  const key = currentIdentityKey();
102
- if (key) {
103
- const cached = readDefaultSpaceCache(CACHE_PATH, key);
104
- if (cached)
105
- return cached;
106
- }
107
- const space = (await createClient().spaces.getDefault()).space ?? null;
108
- // Recent-space fallback from getDefault() is not stable enough to cache.
109
- if (space?.id && space.slug === "home" && key)
110
- writeCachedDefaultSpace(key, space.id);
111
- return space?.id ?? null;
112
+ const cached = key ? readHomeSpaceCache(CACHE_PATH, key) : null;
113
+ if (cached)
114
+ return cached;
115
+ const space = await createClient().spaces.ensureHome();
116
+ if (key)
117
+ writeHomeSpaceCache(key, space.id);
118
+ return space.id;
112
119
  })();
113
- return defaultSpacePromise;
120
+ return homeSpacePromise;
121
+ }
122
+ async function lookupSpaceRef(value) {
123
+ const ref = parseSpaceRef(value);
124
+ if (!ref)
125
+ throw new SpaceTargetError(`Invalid space '${value}'`, SPACE_REF_HINT);
126
+ if (ref.kind === "id")
127
+ return ref.id;
128
+ if (ref.kind === "owned" && ref.slug === HOME_SPACE_SLUG)
129
+ return resolveHomeSpace();
130
+ const spaces = createClient().spaces;
131
+ try {
132
+ const space = ref.kind === "owned"
133
+ ? await spaces.getOwnedBySlug(ref.slug)
134
+ : await spaces.getBySlug(ref.username, ref.slug);
135
+ return space.id;
136
+ }
137
+ catch (e) {
138
+ if (e.status === 404)
139
+ throw new SpaceTargetError(`Space '${value}' not found`, SPACE_REF_HINT);
140
+ throw e;
141
+ }
142
+ }
143
+ export function resolveSpaceRef(value) {
144
+ const key = value.trim();
145
+ let resolved = spaceRefs.get(key);
146
+ if (!resolved) {
147
+ resolved = lookupSpaceRef(key);
148
+ spaceRefs.set(key, resolved);
149
+ }
150
+ return resolved;
151
+ }
152
+ export function failSpaceTarget(e) {
153
+ if (e instanceof SpaceTargetError)
154
+ return error(e.message, e.detail);
155
+ return handleHttp(e);
114
156
  }
115
157
  /** Shared exit for commands that need a space but resolved none. */
116
158
  export function missingSpaceError() {
117
- return error("No target space", "Add -s, --space <id> or set COHUB_SPACE_ID. Run `cohub auth login` to use your home space.");
159
+ return error("No target space", "Pass -s <space> (an ID, home, or username/slug), set COHUB_SPACE_ID, or run in a directory bound with `cohub runtime up`.");
118
160
  }
119
- /**
120
- * Resolve a Space from an optional explicit target, the current directory
121
- * binding, and finally the user's Home Space.
122
- */
123
161
  export async function resolveBoundSpace(options = {}) {
124
162
  const bound = await getRuntimeSpaceBinding(options.cwd ?? process.cwd(), currentIdentityKey(), options.bindingsPath).catch(handleHttp);
125
163
  return bound?.spaceId ?? null;
126
164
  }
165
+ /** Explicit target or `COHUB_SPACE_ID`, then the directory binding, then Home when allowed. */
127
166
  export async function resolveSpaceTarget(target, options = {}) {
128
167
  const explicit = target?.trim() || process.env.COHUB_SPACE_ID?.trim() || null;
129
- if (explicit)
130
- return explicit;
131
- return (await resolveBoundSpace(options))
132
- ?? (await resolveDefaultSpace().catch(handleHttp))
133
- ?? missingSpaceError();
134
- }
135
- export async function resolveSpace(program) {
136
- return resolveSpaceTarget(explicitSpace(program));
168
+ try {
169
+ if (explicit)
170
+ return await resolveSpaceRef(explicit);
171
+ const bound = await resolveBoundSpace(options);
172
+ if (bound)
173
+ return bound;
174
+ if (options.home)
175
+ return await resolveHomeSpace();
176
+ }
177
+ catch (e) {
178
+ return failSpaceTarget(e);
179
+ }
180
+ return missingSpaceError();
181
+ }
182
+ export function resolveSpace(command, options = {}) {
183
+ return resolveSpaceTarget(explicitSpace(command), options);
137
184
  }
package/dist/web.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function webUrl(path: string): string;
package/dist/web.js ADDED
@@ -0,0 +1,6 @@
1
+ import { resolveCohubEnvironment } from "@neta-art/cohub";
2
+ export function webUrl(path) {
3
+ const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ??
4
+ `https://${resolveCohubEnvironment() === "prod" ? "" : "dev."}cohub.live`;
5
+ return `${origin}${path}`;
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "8.4.0",
3
+ "version": "9.0.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,25 +20,29 @@
20
20
  "dependencies": {
21
21
  "@neta-art/generation": "^0.1.30",
22
22
  "commander": "^15.0.0",
23
- "pixi.js": "^8.20.1",
24
23
  "sharp": "^0.35.4",
25
24
  "ws": "^8.21.3",
26
- "@neta-art/cohub": "8.23.0"
25
+ "@neta-art/cohub": "9.0.0"
27
26
  },
28
27
  "publishConfig": {
29
28
  "access": "public"
30
29
  },
31
30
  "devDependencies": {
31
+ "@fontsource/geist": "^5.3.0",
32
+ "@fontsource/geist-mono": "^5.3.0",
32
33
  "@types/node": "^26.4.0",
33
34
  "@types/ws": "^8.18.1",
35
+ "pixi.js": "^8.20.1",
36
+ "tsdown": "^0.22.14",
34
37
  "typescript": "^7.0.2"
35
38
  },
36
39
  "optionalDependencies": {
37
40
  "@napi-rs/canvas": "^1.0.8"
38
41
  },
39
42
  "scripts": {
40
- "build": "tsc -p tsconfig.build.json",
43
+ "build": "tsc -p tsconfig.build.json && tsdown",
41
44
  "verify:sandboxd": "node --import tsx scripts/verify-sandboxd-release.ts",
45
+ "verify:board-kernel": "node --import tsx scripts/verify-board-kernel.ts",
42
46
  "test": "node ../../scripts/test/run.mjs 'tests/**/*.test.ts'",
43
47
  "test:runtime": "node --import tsx --test tests/runtime-*.integration.ts",
44
48
  "test:runtime:native": "node --import tsx tests/runtime-native.smoke.ts",
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerBoardDomainCommands(boards: Command): void;
@@ -1,16 +0,0 @@
1
- import { registerBoardAnimationCommands } from "./boards/animation.js";
2
- import { registerBoardAppearanceCommands } from "./boards/appearance.js";
3
- import { registerBoardExampleCommands } from "./boards/examples.js";
4
- import { registerBoardBatchCommand } from "./boards/batch.js";
5
- import { registerBoardConnectionCommands } from "./boards/connections.js";
6
- import { registerBoardItemCommands } from "./boards/items.js";
7
- import { registerBoardTransactionCommands } from "./boards/transactions.js";
8
- export function registerBoardDomainCommands(boards) {
9
- registerBoardExampleCommands(boards);
10
- registerBoardAppearanceCommands(boards);
11
- registerBoardBatchCommand(boards);
12
- registerBoardConnectionCommands(boards);
13
- registerBoardItemCommands(boards);
14
- registerBoardAnimationCommands(boards);
15
- registerBoardTransactionCommands(boards);
16
- }
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerBoardAnimationCommands(boards: Command): void;
@@ -1,171 +0,0 @@
1
- import { parseBoardEffectInput, parseBoardCompositionInput, } from "@neta-art/cohub";
2
- import { BOARD_DOMAIN_INPUT_MAX_BYTES, readBoardJsonObject, } from "../../board-command-support.js";
3
- import { handleHttp, json, jsonRequested, table } from "../../output.js";
4
- import { mutateSemantic, readInput, readOptions, resolvedBoard, showUpdated, withJson, } from "./context.js";
5
- export function registerBoardAnimationCommands(boards) {
6
- const effects = boards.command("effects").description("Manage Board effects");
7
- readOptions(withJson(effects.command("list <board>").alias("ls").description("List effects")))
8
- .action(async (target, options) => {
9
- try {
10
- const board = await resolvedBoard(boards, target);
11
- const result = await board.authoring(readInput(options, "effects"));
12
- const effects = result.effects ?? [];
13
- if (jsonRequested(options))
14
- return json(effects);
15
- table(effects, [
16
- { key: "id", label: "ID" },
17
- { key: "kind", label: "KIND" },
18
- { key: "enabled", label: "ENABLED" },
19
- { key: "layer", label: "LAYER" },
20
- ]);
21
- }
22
- catch (cause) {
23
- handleHttp(cause);
24
- }
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
- });
47
- withJson(effects.command("apply <board>")
48
- .description("Atomically create or replace an effect")
49
- .requiredOption("-i, --input <file>", "Board effect JSON; use - for stdin")
50
- .addHelpText("after", `
51
- Create a template:
52
- cohub boards examples effect pulse > effect.json
53
-
54
- Discover supported effect kinds:
55
- cohub boards capabilities <board> --json`))
56
- .action(async (target, options) => {
57
- try {
58
- const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
59
- const effect = parseBoardEffectInput(input);
60
- const board = await resolvedBoard(boards, target);
61
- showUpdated(await mutateSemantic(board, [{ type: "effect.apply", effect }]), options);
62
- }
63
- catch (cause) {
64
- handleHttp(cause);
65
- }
66
- });
67
- withJson(effects.command("delete <board> <effect-id>").alias("rm").description("Delete an effect"))
68
- .action(async (target, effectId, options) => {
69
- try {
70
- const board = await resolvedBoard(boards, target);
71
- showUpdated(await mutateSemantic(board, [{ type: "effect.delete", effectId }]), options);
72
- }
73
- catch (cause) {
74
- handleHttp(cause);
75
- }
76
- });
77
- const compositions = boards
78
- .command("compositions")
79
- .description("Manage atomic Board animation compositions");
80
- readOptions(withJson(compositions.command("list <board>").alias("ls").description("List compositions")))
81
- .action(async (target, options) => {
82
- try {
83
- const board = await resolvedBoard(boards, target);
84
- const result = await board.authoring(readInput(options, "compositions"));
85
- const compositions = result.compositions ?? [];
86
- if (jsonRequested(options))
87
- return json(compositions);
88
- table(compositions.map((composition) => ({
89
- id: composition.id,
90
- name: composition.name,
91
- duration: composition.timeline.duration,
92
- tracks: composition.timeline.tracks.length,
93
- clips: composition.timeline.clips.length,
94
- revision: composition.revision,
95
- })), [
96
- { key: "id", label: "ID" },
97
- { key: "name", label: "NAME" },
98
- { key: "duration", label: "DURATION" },
99
- { key: "tracks", label: "TRACKS" },
100
- { key: "clips", label: "CLIPS" },
101
- { key: "revision", label: "REVISION" },
102
- ]);
103
- }
104
- catch (cause) {
105
- handleHttp(cause);
106
- }
107
- });
108
- withJson(compositions.command("get <board> <composition-id>").description("Get one complete composition"))
109
- .action(async (target, compositionId, options) => {
110
- try {
111
- const board = await resolvedBoard(boards, target);
112
- const result = await board.authoring({ include: ["compositions"], compositionIds: [compositionId] });
113
- const composition = result.compositions?.[0];
114
- if (!composition)
115
- throw new Error(`Composition not found: ${compositionId}`);
116
- if (jsonRequested(options))
117
- return json(composition);
118
- table([{
119
- id: composition.id,
120
- name: composition.name,
121
- duration: composition.timeline.duration,
122
- tracks: composition.timeline.tracks.length,
123
- clips: composition.timeline.clips.length,
124
- revision: composition.revision,
125
- }], [
126
- { key: "id", label: "ID" },
127
- { key: "name", label: "NAME" },
128
- { key: "duration", label: "DURATION" },
129
- { key: "tracks", label: "TRACKS" },
130
- { key: "clips", label: "CLIPS" },
131
- { key: "revision", label: "REVISION" },
132
- ]);
133
- }
134
- catch (cause) {
135
- handleHttp(cause);
136
- }
137
- });
138
- withJson(compositions.command("apply <board>")
139
- .description("Atomically create or replace a composition")
140
- .requiredOption("-i, --input <file>", "BoardComposition JSON; use - for stdin")
141
- .addHelpText("after", `
142
- Property changes use timeline.tracks with registered channels and keyframes.
143
- Procedural behavior such as text reveal, particles, and camera focus uses timeline.clips.
144
- Run boards capabilities to discover channels and clip schemas.
145
- Create an editable template with:
146
- cohub boards examples composition fade > intro.json
147
-
148
- Minimal fade composition:
149
- {"id":"intro","name":"Intro","timeline":{"duration":800,"tracks":[{"id":"title-opacity","target":{"type":"item","itemId":"title"},"channel":"style.opacity","fill":"both","keyframes":[{"time":0,"value":0},{"time":800,"value":1,"easing":"ease-out-cubic"}]}],"clips":[],"markers":[]},"playback":{"loop":false,"endBehavior":"hold","reducedMotion":{"mode":"base"}}}`))
150
- .action(async (target, options) => {
151
- try {
152
- const input = await readBoardJsonObject(options.input, BOARD_DOMAIN_INPUT_MAX_BYTES);
153
- const composition = parseBoardCompositionInput(input);
154
- const board = await resolvedBoard(boards, target);
155
- showUpdated(await mutateSemantic(board, [{ type: "composition.apply", composition }]), options);
156
- }
157
- catch (cause) {
158
- handleHttp(cause);
159
- }
160
- });
161
- withJson(compositions.command("delete <board> <composition-id>").alias("rm").description("Delete a composition"))
162
- .action(async (target, compositionId, options) => {
163
- try {
164
- const board = await resolvedBoard(boards, target);
165
- showUpdated(await mutateSemantic(board, [{ type: "composition.delete", compositionId }]), options);
166
- }
167
- catch (cause) {
168
- handleHttp(cause);
169
- }
170
- });
171
- }
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerBoardAppearanceCommands(boards: Command): void;
@@ -1,127 +0,0 @@
1
- import { BoardAppearanceSchema, DEFAULT_BOARD_APPEARANCE, patchBoardAppearance, } from "@neta-art/cohub/board";
2
- import { handleHttp } from "../../output.js";
3
- import { finite, mutateSemantic, resolvedBoard, showUpdated, withJson, } from "./context.js";
4
- function appearanceFrom(metadata) {
5
- const parsed = BoardAppearanceSchema.safeParse(metadata.appearance);
6
- return parsed.success ? parsed.data : DEFAULT_BOARD_APPEARANCE;
7
- }
8
- export function registerBoardAppearanceCommands(boards) {
9
- withJson(boards.command("rename <board> <title>").description("Rename a Board"))
10
- .action(async (target, title, options) => {
11
- try {
12
- const board = await resolvedBoard(boards, target);
13
- showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch: { title } }]), options);
14
- }
15
- catch (cause) {
16
- handleHttp(cause);
17
- }
18
- });
19
- withJson(boards.command("background <board>")
20
- .description("Configure the Board background")
21
- .option("--color <color>", "Solid CSS color")
22
- .option("--image <url>", "Public image URL")
23
- .option("--fit <mode>", "cover, contain, or repeat", "cover")
24
- .option("--position <position>", "center, top, bottom, left, or right", "center")
25
- .option("--opacity <value>", "Image opacity from 0 to 1", "1")
26
- .option("--reset", "Restore the default background")
27
- .addHelpText("after", `
28
- Examples:
29
- cohub boards background plan.board --color "#123456"
30
- cohub boards background plan.board --image https://example.com/bg.webp --fit cover --opacity 0.8`))
31
- .action(async (target, options) => {
32
- try {
33
- const selected = [options.color, options.image, options.reset ? "reset" : undefined].filter(Boolean);
34
- if (selected.length !== 1)
35
- throw new Error("Choose one of --color, --image, or --reset");
36
- if (!["cover", "contain", "repeat"].includes(options.fit))
37
- throw new Error("--fit must be cover, contain, or repeat");
38
- if (!["center", "top", "bottom", "left", "right"].includes(options.position))
39
- throw new Error("--position must be center, top, bottom, left, or right");
40
- const opacity = finite(options.opacity, "opacity");
41
- if (opacity < 0 || opacity > 1)
42
- throw new Error("opacity must be between 0 and 1");
43
- const board = await resolvedBoard(boards, target);
44
- const current = await board.summary();
45
- const appearance = appearanceFrom(current.board.metadata);
46
- const background = options.reset
47
- ? { kind: "solid" }
48
- : options.color
49
- ? { kind: "solid", color: options.color }
50
- : {
51
- kind: "image",
52
- imageUrl: options.image,
53
- fit: options.fit,
54
- position: options.position,
55
- opacity,
56
- color: appearance.background.color,
57
- };
58
- const result = await mutateSemantic(board, [{
59
- type: "board.patch",
60
- patch: { metadataPatch: { appearance: patchBoardAppearance(appearance, { background }) } },
61
- }], { baseVersion: current.board.version });
62
- showUpdated(result, options);
63
- }
64
- catch (cause) {
65
- handleHttp(cause);
66
- }
67
- });
68
- withJson(boards.command("motion <board>")
69
- .description("Configure the default Board motion for newly added nodes")
70
- .option("--preset <preset>", "Enter motion preset (deal)")
71
- .option("--clear", "Disable the default enter motion")
72
- .addHelpText("after", `
73
- Examples:
74
- cohub boards motion plan.board --preset deal
75
- cohub boards motion plan.board --clear`))
76
- .action(async (target, options) => {
77
- try {
78
- if (Boolean(options.clear) === Boolean(options.preset))
79
- throw new Error("Use --preset or --clear");
80
- if (options.preset && options.preset !== "deal")
81
- throw new Error("--preset must be deal");
82
- const board = await resolvedBoard(boards, target);
83
- const current = await board.summary();
84
- const appearance = appearanceFrom(current.board.metadata);
85
- const next = patchBoardAppearance(appearance, {
86
- motion: options.clear
87
- ? undefined
88
- : { enter: { kind: "effects.deal", kindVersion: 1, params: {} } },
89
- });
90
- showUpdated(await mutateSemantic(board, [{
91
- type: "board.patch",
92
- patch: { metadataPatch: { appearance: next } },
93
- }], { baseVersion: current.board.version }), options);
94
- }
95
- catch (cause) {
96
- handleHttp(cause);
97
- }
98
- });
99
- withJson(boards.command("playback-policy <board>")
100
- .description("Configure automatic Board playback")
101
- .option("--composition <id>", "Composition to play")
102
- .option("--delay <ms>", "Delay before playback", "0")
103
- .option("--clear", "Remove the playback policy"))
104
- .action(async (target, options) => {
105
- try {
106
- if (options.clear === Boolean(options.composition))
107
- throw new Error("Use --composition or --clear");
108
- const delayMs = finite(options.delay, "delay");
109
- if (delayMs < 0)
110
- throw new Error("delay must be non-negative");
111
- const policy = options.clear
112
- ? null
113
- : { compositionId: options.composition, delayMs };
114
- const board = await resolvedBoard(boards, target);
115
- const current = await board.summary();
116
- const patch = policy
117
- ? { metadataPatch: { playback: policy } }
118
- : { metadata: { ...current.board.metadata, playback: undefined } };
119
- if (!policy)
120
- delete patch.metadata.playback;
121
- showUpdated(await mutateSemantic(board, [{ type: "board.patch", patch }], { baseVersion: current.board.version }), options);
122
- }
123
- catch (cause) {
124
- handleHttp(cause);
125
- }
126
- });
127
- }
@@ -1,4 +0,0 @@
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;
@@ -1,62 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerBoardConnectionCommands(boards: Command): void;