@neta-art/cohub-cli 2.7.0 → 3.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.
- package/README.md +49 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +12 -8
- package/dist/commands/boards.d.ts +14 -0
- package/dist/commands/boards.js +336 -0
- package/dist/commands/spaces.js +44 -3
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -121,6 +121,55 @@ cohub -s <spaceId> spaces sessions rename <sessionId> "<new title>"
|
|
|
121
121
|
|
|
122
122
|
Use `spaces prompt --session <sessionId>` to send to a Chat.
|
|
123
123
|
|
|
124
|
+
## Boards
|
|
125
|
+
|
|
126
|
+
Board commands use the selected Space and support `-h` at every level:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cohub boards -h
|
|
130
|
+
cohub boards inspect -h
|
|
131
|
+
cohub -s <spaceId> boards create boards/plan.board --title "Plan"
|
|
132
|
+
cohub -s <spaceId> boards inspect <boardId> --json
|
|
133
|
+
cohub -s <spaceId> boards capabilities <boardId>
|
|
134
|
+
cohub -s <spaceId> boards watch <boardId> --json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Pass nodes, effects, and sequences as JSON when creating a Board. The path and
|
|
138
|
+
title stay explicit in the command:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cohub -s <spaceId> boards create boards/plan.board \
|
|
142
|
+
--title "Plan" \
|
|
143
|
+
--input board-content.json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Transactions are JSON objects without `boardId`; the bound Board supplies it.
|
|
147
|
+
`txId` is generated when omitted, while `baseVersion` must be provided in the
|
|
148
|
+
input or with `--base-version`:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"baseVersion": 3,
|
|
153
|
+
"operations": [
|
|
154
|
+
{
|
|
155
|
+
"type": "board.patch",
|
|
156
|
+
"payload": { "patch": { "title": "Updated plan" } }
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cohub -s <spaceId> boards validate <boardId> --input transaction.json
|
|
164
|
+
cat transaction.json | cohub -s <spaceId> boards apply <boardId> --input - --json
|
|
165
|
+
cohub -s <spaceId> boards play <boardId> <sequenceId>
|
|
166
|
+
cohub -s <spaceId> boards seek <boardId> <playbackId> 400
|
|
167
|
+
cohub -s <spaceId> boards stop <boardId> <playbackId>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Pass `--tx-id` or `--command-id` when a script needs a stable idempotency key
|
|
171
|
+
across retries.
|
|
172
|
+
|
|
124
173
|
## Search
|
|
125
174
|
|
|
126
175
|
Search Spaces, Chats, and prior turns:
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { CohubHttpClient, readRequestSourceFromEnv } from "@neta-art/cohub";
|
|
1
|
+
import { CohubClient, CohubHttpClient, readRequestSourceFromEnv } from "@neta-art/cohub";
|
|
2
2
|
import { clearAuthSession, resolveAccessToken } from "./auth.js";
|
|
3
|
+
const clientOptions = () => ({
|
|
4
|
+
getAccessToken: resolveAccessToken,
|
|
5
|
+
onUnauthorized: clearAuthSession,
|
|
6
|
+
requestSource: () => readRequestSourceFromEnv(process.env, { via: "cli" }) ?? {
|
|
7
|
+
via: "cli",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
3
10
|
export function createClient() {
|
|
4
|
-
return new CohubHttpClient(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
via: "cli",
|
|
9
|
-
},
|
|
10
|
-
});
|
|
11
|
+
return new CohubHttpClient(clientOptions());
|
|
12
|
+
}
|
|
13
|
+
export function createRealtimeClient() {
|
|
14
|
+
return new CohubClient(clientOptions());
|
|
11
15
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BoardInspectInput, BoardTransactionInput } from "@neta-art/cohub";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
declare const INSPECT_SECTIONS: readonly ["nodes", "effects", "sequences", "clips", "playback"];
|
|
4
|
+
type InspectSection = (typeof INSPECT_SECTIONS)[number];
|
|
5
|
+
export declare function parseJsonObject(text: string, source?: string): Record<string, unknown>;
|
|
6
|
+
export declare function readJsonObject(source: string): Promise<Record<string, unknown>>;
|
|
7
|
+
export declare function parseInspectSections(value?: string): InspectSection[] | undefined;
|
|
8
|
+
export declare function parseViewport(value?: string): BoardInspectInput["viewport"];
|
|
9
|
+
export declare function createTransactionInput(input: Record<string, unknown>, options: {
|
|
10
|
+
txId?: string;
|
|
11
|
+
baseVersion?: string;
|
|
12
|
+
}): BoardTransactionInput;
|
|
13
|
+
export declare function registerBoards(program: Command): Command;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createClient, createRealtimeClient } from "../client.js";
|
|
4
|
+
import { handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
5
|
+
import { resolveSpace } from "../space.js";
|
|
6
|
+
const INSPECT_SECTIONS = ["nodes", "effects", "sequences", "clips", "playback"];
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
export function parseJsonObject(text, source = "input") {
|
|
11
|
+
if (!text.trim())
|
|
12
|
+
throw new Error(`${source} is empty`);
|
|
13
|
+
let value;
|
|
14
|
+
try {
|
|
15
|
+
value = JSON.parse(text);
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
throw new Error(`${source} must contain valid JSON`, { cause });
|
|
19
|
+
}
|
|
20
|
+
if (!isObject(value))
|
|
21
|
+
throw new Error(`${source} must contain a JSON object`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
async function readStdin() {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
for await (const chunk of process.stdin)
|
|
27
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
28
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
export async function readJsonObject(source) {
|
|
31
|
+
const text = source === "-" ? await readStdin() : await readFile(source, "utf8");
|
|
32
|
+
return parseJsonObject(text, source === "-" ? "stdin" : source);
|
|
33
|
+
}
|
|
34
|
+
function parseNumber(value, name, options = {}) {
|
|
35
|
+
if (!value.trim())
|
|
36
|
+
throw new Error(`${name} must be a finite number`);
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
if (!Number.isFinite(parsed))
|
|
39
|
+
throw new Error(`${name} must be a finite number`);
|
|
40
|
+
if (options.integer && !Number.isSafeInteger(parsed))
|
|
41
|
+
throw new Error(`${name} must be an integer`);
|
|
42
|
+
if (options.min !== undefined && parsed < options.min)
|
|
43
|
+
throw new Error(`${name} must be at least ${options.min}`);
|
|
44
|
+
if (options.max !== undefined && parsed > options.max)
|
|
45
|
+
throw new Error(`${name} must be at most ${options.max}`);
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
export function parseInspectSections(value) {
|
|
49
|
+
if (!value)
|
|
50
|
+
return undefined;
|
|
51
|
+
const sections = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
52
|
+
const unknown = sections.filter((section) => !INSPECT_SECTIONS.includes(section));
|
|
53
|
+
if (unknown.length > 0)
|
|
54
|
+
throw new Error(`Unknown Board section: ${unknown.join(", ")}`);
|
|
55
|
+
return [...new Set(sections)];
|
|
56
|
+
}
|
|
57
|
+
export function parseViewport(value) {
|
|
58
|
+
if (!value)
|
|
59
|
+
return undefined;
|
|
60
|
+
const parts = value.split(",").map((part) => part.trim());
|
|
61
|
+
if (parts.length !== 4)
|
|
62
|
+
throw new Error("viewport must be x,y,width,height");
|
|
63
|
+
const [xValue, yValue, widthValue, heightValue] = parts;
|
|
64
|
+
if (xValue === undefined || yValue === undefined || widthValue === undefined || heightValue === undefined) {
|
|
65
|
+
throw new Error("viewport must be x,y,width,height");
|
|
66
|
+
}
|
|
67
|
+
const x = parseNumber(xValue, "viewport x");
|
|
68
|
+
const y = parseNumber(yValue, "viewport y");
|
|
69
|
+
const width = parseNumber(widthValue, "viewport width");
|
|
70
|
+
const height = parseNumber(heightValue, "viewport height");
|
|
71
|
+
if (width <= 0 || height <= 0)
|
|
72
|
+
throw new Error("viewport width and height must be greater than zero");
|
|
73
|
+
return { x, y, width, height };
|
|
74
|
+
}
|
|
75
|
+
export function createTransactionInput(input, options) {
|
|
76
|
+
if ("boardId" in input)
|
|
77
|
+
throw new Error("transaction input must not contain boardId");
|
|
78
|
+
if (!Array.isArray(input.operations))
|
|
79
|
+
throw new Error("transaction input must contain an operations array");
|
|
80
|
+
const rawBaseVersion = options.baseVersion ?? input.baseVersion;
|
|
81
|
+
if (rawBaseVersion === undefined)
|
|
82
|
+
throw new Error("baseVersion is required in input or --base-version");
|
|
83
|
+
const baseVersion = parseNumber(String(rawBaseVersion), "baseVersion", { min: 0, integer: true });
|
|
84
|
+
const rawTxId = options.txId ?? input.txId;
|
|
85
|
+
if (rawTxId !== undefined && (typeof rawTxId !== "string" || !rawTxId.trim())) {
|
|
86
|
+
throw new Error("txId must be a non-empty string");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...input,
|
|
90
|
+
txId: typeof rawTxId === "string" ? rawTxId : randomUUID(),
|
|
91
|
+
baseVersion,
|
|
92
|
+
operations: input.operations,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function showBoard(result) {
|
|
96
|
+
table([
|
|
97
|
+
{
|
|
98
|
+
id: result.board.id,
|
|
99
|
+
title: result.board.title,
|
|
100
|
+
version: result.board.version,
|
|
101
|
+
nodes: result.nodes.length,
|
|
102
|
+
effects: result.effects.length,
|
|
103
|
+
sequences: result.sequences.length,
|
|
104
|
+
clips: result.clips.length,
|
|
105
|
+
},
|
|
106
|
+
], [
|
|
107
|
+
{ key: "id", label: "ID" },
|
|
108
|
+
{ key: "title", label: "Title" },
|
|
109
|
+
{ key: "version", label: "Version" },
|
|
110
|
+
{ key: "nodes", label: "Nodes" },
|
|
111
|
+
{ key: "effects", label: "Effects" },
|
|
112
|
+
{ key: "sequences", label: "Sequences" },
|
|
113
|
+
{ key: "clips", label: "Clips" },
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
function showValidation(result) {
|
|
117
|
+
table([{ valid: result.valid, diagnostics: result.diagnostics.length }], [
|
|
118
|
+
{ key: "valid", label: "Valid" },
|
|
119
|
+
{ key: "diagnostics", label: "Diagnostics" },
|
|
120
|
+
]);
|
|
121
|
+
if (result.diagnostics.length > 0) {
|
|
122
|
+
console.log();
|
|
123
|
+
table(result.diagnostics, [
|
|
124
|
+
{ key: "severity", label: "Severity" },
|
|
125
|
+
{ key: "code", label: "Code" },
|
|
126
|
+
{ key: "path", label: "Path" },
|
|
127
|
+
{ key: "message", label: "Message" },
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
table([result.peakCost], Object.keys(result.peakCost).map((key) => ({ key, label: key })));
|
|
132
|
+
}
|
|
133
|
+
function showPlayback(result) {
|
|
134
|
+
table([result], [
|
|
135
|
+
{ key: "playbackId", label: "Playback ID" },
|
|
136
|
+
{ key: "sequenceId", label: "Sequence" },
|
|
137
|
+
{ key: "status", label: "Status" },
|
|
138
|
+
{ key: "position", label: "Position" },
|
|
139
|
+
{ key: "timeScale", label: "Time Scale" },
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
function withJson(command) {
|
|
143
|
+
return command.option("--json", "Output as JSON");
|
|
144
|
+
}
|
|
145
|
+
function registerTransactionCommand(boards, name) {
|
|
146
|
+
withJson(boards.command(`${name} <board-id>`)
|
|
147
|
+
.description(name === "validate" ? "Validate a transaction" : "Apply a transaction")
|
|
148
|
+
.requiredOption("-i, --input <file>", "Transaction JSON file; use - for stdin")
|
|
149
|
+
.option("--tx-id <id>", "Override txId; generated when omitted")
|
|
150
|
+
.option("--base-version <version>", "Override baseVersion"))
|
|
151
|
+
.action(async (boardId, options) => {
|
|
152
|
+
try {
|
|
153
|
+
const transaction = createTransactionInput(await readJsonObject(options.input), options);
|
|
154
|
+
const board = createClient().space(resolveSpace(boards)).board(boardId);
|
|
155
|
+
const result = await board[name](transaction);
|
|
156
|
+
if (jsonRequested(options))
|
|
157
|
+
return outJson(result);
|
|
158
|
+
if (name === "validate")
|
|
159
|
+
showValidation(result);
|
|
160
|
+
else {
|
|
161
|
+
ok(`Board updated to version ${result.board.version}`);
|
|
162
|
+
showBoard(result);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
handleHttp(cause);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function commandId(options) {
|
|
171
|
+
return options.commandId?.trim() || randomUUID();
|
|
172
|
+
}
|
|
173
|
+
export function registerBoards(program) {
|
|
174
|
+
const boards = program
|
|
175
|
+
.command("boards")
|
|
176
|
+
.description("Inspect and update Boards")
|
|
177
|
+
.hook("preAction", () => { resolveSpace(boards); });
|
|
178
|
+
withJson(boards.command("create <path>")
|
|
179
|
+
.description("Create a Board")
|
|
180
|
+
.option("--title <title>", "Board title")
|
|
181
|
+
.option("-i, --input <file>", "Board content JSON file; use - for stdin"))
|
|
182
|
+
.action(async (path, options) => {
|
|
183
|
+
try {
|
|
184
|
+
const content = options.input ? await readJsonObject(options.input) : {};
|
|
185
|
+
if ("path" in content || "title" in content) {
|
|
186
|
+
throw new Error("create input must not contain path or title; use the command argument and --title");
|
|
187
|
+
}
|
|
188
|
+
const input = { ...content, path, ...(options.title ? { title: options.title } : {}) };
|
|
189
|
+
const result = await createClient().space(resolveSpace(boards)).boards.create(input);
|
|
190
|
+
if (jsonRequested(options))
|
|
191
|
+
return outJson(result);
|
|
192
|
+
ok(`Board created: ${result.board.id}`);
|
|
193
|
+
showBoard(result);
|
|
194
|
+
}
|
|
195
|
+
catch (cause) {
|
|
196
|
+
handleHttp(cause);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
withJson(boards.command("inspect <board-id>")
|
|
200
|
+
.alias("get")
|
|
201
|
+
.description("Inspect a Board")
|
|
202
|
+
.option("--include <sections>", "Comma-separated nodes,effects,sequences,clips,playback")
|
|
203
|
+
.option("--viewport <rect>", "Viewport as x,y,width,height"))
|
|
204
|
+
.action(async (boardId, options) => {
|
|
205
|
+
try {
|
|
206
|
+
const result = await createClient().space(resolveSpace(boards)).board(boardId).inspect({
|
|
207
|
+
include: parseInspectSections(options.include),
|
|
208
|
+
viewport: parseViewport(options.viewport),
|
|
209
|
+
});
|
|
210
|
+
if (jsonRequested(options))
|
|
211
|
+
return outJson(result);
|
|
212
|
+
showBoard(result);
|
|
213
|
+
}
|
|
214
|
+
catch (cause) {
|
|
215
|
+
handleHttp(cause);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
withJson(boards.command("capabilities <board-id>")
|
|
219
|
+
.description("Show supported capabilities"))
|
|
220
|
+
.action(async (boardId, options) => {
|
|
221
|
+
try {
|
|
222
|
+
const result = await createClient().space(resolveSpace(boards)).board(boardId).capabilities();
|
|
223
|
+
if (jsonRequested(options))
|
|
224
|
+
return outJson(result);
|
|
225
|
+
table(result.capabilities.map((capability) => ({
|
|
226
|
+
...capability,
|
|
227
|
+
renderers: capability.renderers?.join(", ") ?? "",
|
|
228
|
+
})), [
|
|
229
|
+
{ key: "kind", label: "Kind" },
|
|
230
|
+
{ key: "id", label: "ID" },
|
|
231
|
+
{ key: "version", label: "Version" },
|
|
232
|
+
{ key: "renderers", label: "Renderers" },
|
|
233
|
+
{ key: "digest", label: "Digest" },
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
catch (cause) {
|
|
237
|
+
handleHttp(cause);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
registerTransactionCommand(boards, "validate");
|
|
241
|
+
registerTransactionCommand(boards, "apply");
|
|
242
|
+
withJson(boards.command("play <board-id> <sequence-id>")
|
|
243
|
+
.description("Start shared playback")
|
|
244
|
+
.option("--position <time>", "Initial position in milliseconds")
|
|
245
|
+
.option("--time-scale <scale>", "Playback speed from 0 to 4")
|
|
246
|
+
.option("--seed <seed>", "Deterministic playback seed")
|
|
247
|
+
.option("--command-id <id>", "Idempotency command ID"))
|
|
248
|
+
.action(async (boardId, sequenceId, options) => {
|
|
249
|
+
try {
|
|
250
|
+
const result = await createClient().space(resolveSpace(boards)).board(boardId).play({
|
|
251
|
+
commandId: commandId(options),
|
|
252
|
+
type: "play",
|
|
253
|
+
sequenceId,
|
|
254
|
+
shared: true,
|
|
255
|
+
...(options.position === undefined ? {} : { position: parseNumber(options.position, "position", { min: 0 }) }),
|
|
256
|
+
...(options.timeScale === undefined ? {} : { timeScale: parseNumber(options.timeScale, "timeScale", { min: Number.EPSILON, max: 4 }) }),
|
|
257
|
+
...(options.seed ? { seed: options.seed } : {}),
|
|
258
|
+
});
|
|
259
|
+
if (jsonRequested(options))
|
|
260
|
+
return outJson(result);
|
|
261
|
+
showPlayback(result);
|
|
262
|
+
}
|
|
263
|
+
catch (cause) {
|
|
264
|
+
handleHttp(cause);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const playbackAction = (type) => async (boardId, playbackId, options) => {
|
|
268
|
+
try {
|
|
269
|
+
const board = createClient().space(resolveSpace(boards)).board(boardId);
|
|
270
|
+
const id = commandId(options);
|
|
271
|
+
const result = type === "pause"
|
|
272
|
+
? await board.pause({ commandId: id, type: "pause", playbackId })
|
|
273
|
+
: await board.stop({ commandId: id, type: "stop", playbackId });
|
|
274
|
+
if (jsonRequested(options))
|
|
275
|
+
return outJson(result);
|
|
276
|
+
showPlayback(result);
|
|
277
|
+
}
|
|
278
|
+
catch (cause) {
|
|
279
|
+
handleHttp(cause);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
withJson(boards.command("pause <board-id> <playback-id>")
|
|
283
|
+
.description("Pause playback")
|
|
284
|
+
.option("--command-id <id>", "Idempotency command ID"))
|
|
285
|
+
.action(playbackAction("pause"));
|
|
286
|
+
withJson(boards.command("seek <board-id> <playback-id> <position>")
|
|
287
|
+
.description("Seek playback")
|
|
288
|
+
.option("--command-id <id>", "Idempotency command ID"))
|
|
289
|
+
.action(async (boardId, playbackId, position, options) => {
|
|
290
|
+
try {
|
|
291
|
+
const result = await createClient().space(resolveSpace(boards)).board(boardId).seek({
|
|
292
|
+
commandId: commandId(options),
|
|
293
|
+
type: "seek",
|
|
294
|
+
playbackId,
|
|
295
|
+
position: parseNumber(position, "position", { min: 0 }),
|
|
296
|
+
});
|
|
297
|
+
if (jsonRequested(options))
|
|
298
|
+
return outJson(result);
|
|
299
|
+
showPlayback(result);
|
|
300
|
+
}
|
|
301
|
+
catch (cause) {
|
|
302
|
+
handleHttp(cause);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
withJson(boards.command("stop <board-id> <playback-id>")
|
|
306
|
+
.description("Stop playback")
|
|
307
|
+
.option("--command-id <id>", "Idempotency command ID"))
|
|
308
|
+
.action(playbackAction("stop"));
|
|
309
|
+
withJson(boards.command("watch <board-id>")
|
|
310
|
+
.description("Stream Board events"))
|
|
311
|
+
.action((boardId, options) => {
|
|
312
|
+
try {
|
|
313
|
+
const board = createRealtimeClient().space(resolveSpace(boards)).board(boardId);
|
|
314
|
+
if (!jsonRequested(options))
|
|
315
|
+
process.stderr.write(`Listening for Board ${boardId} events...\n`);
|
|
316
|
+
board.subscribe({
|
|
317
|
+
event(event) {
|
|
318
|
+
if (jsonRequested(options)) {
|
|
319
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (event.type === "board.transaction.applied") {
|
|
323
|
+
process.stdout.write(`version ${event.payload.version} transaction ${event.payload.txId} operations ${event.payload.operations.length}\n`);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
process.stdout.write(`${event.payload.status} sequence ${event.payload.sequenceId} position ${event.payload.position}\n`);
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (cause) {
|
|
332
|
+
handleHttp(cause);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return boards;
|
|
336
|
+
}
|
package/dist/commands/spaces.js
CHANGED
|
@@ -381,16 +381,30 @@ export function registerSpaces(program) {
|
|
|
381
381
|
.command("ls")
|
|
382
382
|
.alias("list")
|
|
383
383
|
.description("List all spaces")
|
|
384
|
+
.option("--mine", "Only spaces you own")
|
|
385
|
+
.option("--pinned", "Only pinned spaces")
|
|
384
386
|
.option("--json", "Output as JSON")
|
|
385
387
|
.action(async (opts) => {
|
|
386
388
|
const client = createClient();
|
|
387
389
|
try {
|
|
388
|
-
const items = await
|
|
390
|
+
const [items, me] = await Promise.all([
|
|
391
|
+
client.spaces.list(),
|
|
392
|
+
opts.mine ? client.user.getMe() : Promise.resolve(null),
|
|
393
|
+
]);
|
|
394
|
+
const myUuid = me?.uuid ?? null;
|
|
395
|
+
const filtered = items.filter((item) => {
|
|
396
|
+
if (opts.mine && myUuid && item.userUuid !== myUuid)
|
|
397
|
+
return false;
|
|
398
|
+
if (opts.pinned && !item.isPinned)
|
|
399
|
+
return false;
|
|
400
|
+
return true;
|
|
401
|
+
});
|
|
389
402
|
if (jsonRequested(opts))
|
|
390
|
-
return outJson(
|
|
391
|
-
table(
|
|
403
|
+
return outJson(filtered);
|
|
404
|
+
table(filtered, [
|
|
392
405
|
{ key: "id", label: "ID" },
|
|
393
406
|
{ key: "name", label: "Name" },
|
|
407
|
+
{ key: "isPinned", label: "Pinned" },
|
|
394
408
|
{ key: "createdAt", label: "Created" },
|
|
395
409
|
]);
|
|
396
410
|
}
|
|
@@ -605,6 +619,33 @@ export function registerSpaces(program) {
|
|
|
605
619
|
registerMods(spacesCmd);
|
|
606
620
|
// ── spaces labels ──
|
|
607
621
|
registerLabels(spacesCmd);
|
|
622
|
+
// ── spaces pin / unpin (user-scope label convenience) ──
|
|
623
|
+
spacesCmd
|
|
624
|
+
.command("pin <id>")
|
|
625
|
+
.description("Pin a space (add the Pinned user label)")
|
|
626
|
+
.action(async (id) => {
|
|
627
|
+
const client = createClient();
|
|
628
|
+
try {
|
|
629
|
+
await client.user.labels.patchResourceLabels("space", id.trim(), { addLabelRefs: ["Pinned"] });
|
|
630
|
+
ok("Space pinned");
|
|
631
|
+
}
|
|
632
|
+
catch (e) {
|
|
633
|
+
handleHttp(e);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
spacesCmd
|
|
637
|
+
.command("unpin <id>")
|
|
638
|
+
.description("Unpin a space (remove the Pinned user label)")
|
|
639
|
+
.action(async (id) => {
|
|
640
|
+
const client = createClient();
|
|
641
|
+
try {
|
|
642
|
+
await client.user.labels.patchResourceLabels("space", id.trim(), { removeLabelRefs: ["Pinned"] });
|
|
643
|
+
ok("Space unpinned");
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
handleHttp(e);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
608
649
|
// ── spaces commerce ──
|
|
609
650
|
registerSpaceCommerce(spacesCmd);
|
|
610
651
|
// ── spaces usage ──
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { registerAuth } from "./commands/auth.js";
|
|
5
|
+
import { registerBoards } from "./commands/boards.js";
|
|
5
6
|
import { registerChannels } from "./commands/channels.js";
|
|
6
7
|
import { registerCronJobs } from "./commands/cron-jobs.js";
|
|
7
8
|
import { registerGenerations } from "./commands/generations.js";
|
|
@@ -48,6 +49,7 @@ Common commands:
|
|
|
48
49
|
cohub -s <space-id> run -- git status
|
|
49
50
|
cohub sandbox up ./my-project
|
|
50
51
|
cohub search "release notes"
|
|
52
|
+
cohub -s <space-id> boards inspect <board-id>
|
|
51
53
|
cohub -s <space-id> spaces sessions turns ls <session-id>
|
|
52
54
|
cohub -s <space-id> spaces files ls
|
|
53
55
|
cohub -s <space-id> works publish demo --file dist/index.html
|
|
@@ -61,6 +63,7 @@ Environment:
|
|
|
61
63
|
ENV=dev Use the development Cohub environment
|
|
62
64
|
`);
|
|
63
65
|
registerAuth(program);
|
|
66
|
+
registerBoards(program);
|
|
64
67
|
registerProfile(program);
|
|
65
68
|
registerMe(program);
|
|
66
69
|
registerPrompt(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@neta-art/generation": "^0.1.16",
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"sharp": "^0.35.3",
|
|
21
|
-
"@neta-art/cohub": "
|
|
21
|
+
"@neta-art/cohub": "3.0.0"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|