@neta-art/cohub-cli 1.17.4 → 1.18.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 +24 -1
- package/dist/avatar.d.ts +15 -0
- package/dist/avatar.js +23 -18
- package/dist/commands/spaces.js +51 -7
- package/dist/commands/works.d.ts +2 -0
- package/dist/commands/works.js +266 -0
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -173,6 +173,29 @@ cohub -s <spaceId> spaces files rm <path>
|
|
|
173
173
|
|
|
174
174
|
Confirm before deleting files or directories.
|
|
175
175
|
|
|
176
|
+
## Works
|
|
177
|
+
|
|
178
|
+
Publish and manage Work entries from a Space workspace.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cohub -s <spaceId> works ls --json
|
|
182
|
+
cohub works get <workId> --json
|
|
183
|
+
cohub -s <spaceId> works publish demo --file dist/index.html
|
|
184
|
+
cohub -s <spaceId> works publish site --dir dist
|
|
185
|
+
cohub -s <spaceId> works publish app --port 3000
|
|
186
|
+
cohub works update <workId> --publish-version
|
|
187
|
+
cohub works versions <workId> --json
|
|
188
|
+
cohub works rm <workId> --yes
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Resolve a published Work by public identity:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
cohub works resolve <workSlug> --owner <username> --space-slug <spaceSlug>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use `--json` for machine-readable output. The resolve command requires both `--owner` and `--space-slug` so missing public profile data fails with a clear message.
|
|
198
|
+
|
|
176
199
|
## Saves
|
|
177
200
|
|
|
178
201
|
```bash
|
|
@@ -210,7 +233,7 @@ Confirm before enabling, disabling, or deleting recurring scheduled prompts.
|
|
|
210
233
|
|
|
211
234
|
Confirm before:
|
|
212
235
|
|
|
213
|
-
- deleting files or
|
|
236
|
+
- deleting files, directories, or Works
|
|
214
237
|
- creating scheduled or recurring prompts with side effects
|
|
215
238
|
- enabling, disabling, or deleting recurring scheduled prompts
|
|
216
239
|
- changing access policies, member roles, or membership
|
package/dist/avatar.d.ts
CHANGED
|
@@ -13,3 +13,18 @@ export declare function uploadAvatarAsset(input: {
|
|
|
13
13
|
uploadUrl: string;
|
|
14
14
|
uploadFields: Record<string, string>;
|
|
15
15
|
}>;
|
|
16
|
+
export declare function normalizeChatImageFile(path: string): Promise<Buffer>;
|
|
17
|
+
export declare function uploadChatImageAsset(input: {
|
|
18
|
+
client: CohubHttpClient;
|
|
19
|
+
spaceId: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
path: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
purpose: PublicAssetPurpose;
|
|
24
|
+
objectKey: string;
|
|
25
|
+
publicUrl: string;
|
|
26
|
+
uploadMethod: "POST";
|
|
27
|
+
uploadUrl: string;
|
|
28
|
+
uploadFields: Record<string, string>;
|
|
29
|
+
size: number;
|
|
30
|
+
}>;
|
package/dist/avatar.js
CHANGED
|
@@ -10,26 +10,31 @@ export async function normalizeAvatarFile(path) {
|
|
|
10
10
|
}
|
|
11
11
|
export async function uploadAvatarAsset(input) {
|
|
12
12
|
const body = await normalizeAvatarFile(input.path);
|
|
13
|
-
|
|
13
|
+
return input.client.publicAssets.upload({
|
|
14
14
|
purpose: input.purpose,
|
|
15
15
|
spaceId: input.spaceId,
|
|
16
|
-
file: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
16
|
+
file: new Blob([new Uint8Array(body)], { type: "image/webp" }),
|
|
17
|
+
mimeType: "image/webp",
|
|
18
|
+
filename: "avatar.webp",
|
|
20
19
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
}
|
|
21
|
+
const CHAT_IMAGE_MAX_EDGE = 2160;
|
|
22
|
+
const CHAT_IMAGE_QUALITY = 86;
|
|
23
|
+
export async function normalizeChatImageFile(path) {
|
|
24
|
+
return sharp(path)
|
|
25
|
+
.rotate()
|
|
26
|
+
.resize(CHAT_IMAGE_MAX_EDGE, CHAT_IMAGE_MAX_EDGE, { fit: "inside", withoutEnlargement: true })
|
|
27
|
+
.webp({ quality: CHAT_IMAGE_QUALITY })
|
|
28
|
+
.toBuffer();
|
|
29
|
+
}
|
|
30
|
+
export async function uploadChatImageAsset(input) {
|
|
31
|
+
const body = await normalizeChatImageFile(input.path);
|
|
32
|
+
const asset = await input.client.publicAssets.uploadChatImageAttachment({
|
|
33
|
+
spaceId: input.spaceId,
|
|
34
|
+
sessionId: input.sessionId,
|
|
35
|
+
file: new Blob([new Uint8Array(body)], { type: "image/webp" }),
|
|
36
|
+
mimeType: "image/webp",
|
|
37
|
+
filename: "image.webp",
|
|
29
38
|
});
|
|
30
|
-
|
|
31
|
-
const detail = await response.text().catch(() => "");
|
|
32
|
-
throw new Error(`Avatar upload failed: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`);
|
|
33
|
-
}
|
|
34
|
-
return plan.asset;
|
|
39
|
+
return { ...asset, size: body.byteLength };
|
|
35
40
|
}
|
package/dist/commands/spaces.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createReadStream } from "node:fs";
|
|
|
3
3
|
import { readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { resolveCohubEnvironment } from "@neta-art/cohub";
|
|
6
|
-
import { uploadAvatarAsset } from "../avatar.js";
|
|
6
|
+
import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
|
|
7
7
|
import { createClient } from "../client.js";
|
|
8
8
|
import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
|
|
9
9
|
import { resolveSpace } from "../space.js";
|
|
@@ -26,6 +26,21 @@ function parseInteger(value, name, options = {}) {
|
|
|
26
26
|
function collectOption(value, previous = []) {
|
|
27
27
|
return [...previous, value];
|
|
28
28
|
}
|
|
29
|
+
function parseEnvOptions(values) {
|
|
30
|
+
if (!values?.length)
|
|
31
|
+
return undefined;
|
|
32
|
+
const env = {};
|
|
33
|
+
for (const value of values) {
|
|
34
|
+
const index = value.indexOf("=");
|
|
35
|
+
if (index <= 0)
|
|
36
|
+
return error("Invalid env", "Use --env KEY=value");
|
|
37
|
+
const name = value.slice(0, index).trim();
|
|
38
|
+
if (!name)
|
|
39
|
+
return error("Invalid env", "Env name is required");
|
|
40
|
+
env[name] = value.slice(index + 1);
|
|
41
|
+
}
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
29
44
|
function parseChoice(value, name, choices) {
|
|
30
45
|
if (choices.includes(value))
|
|
31
46
|
return value;
|
|
@@ -156,7 +171,7 @@ async function confirmRestart(opts) {
|
|
|
156
171
|
if (answer !== "y" && answer !== "yes")
|
|
157
172
|
return error("Cancelled");
|
|
158
173
|
}
|
|
159
|
-
async function readPromptContent(words) {
|
|
174
|
+
async function readPromptContent(words, options = {}) {
|
|
160
175
|
let content = words.join(" ");
|
|
161
176
|
if (!content && !process.stdin.isTTY) {
|
|
162
177
|
const chunks = [];
|
|
@@ -164,12 +179,12 @@ async function readPromptContent(words) {
|
|
|
164
179
|
chunks.push(chunk);
|
|
165
180
|
content = Buffer.concat(chunks).toString().trim();
|
|
166
181
|
}
|
|
167
|
-
if (!content)
|
|
182
|
+
if (!content && !options.allowEmpty)
|
|
168
183
|
return error("No content", "Pass as argument or pipe via stdin");
|
|
169
184
|
return content;
|
|
170
185
|
}
|
|
171
186
|
async function sendPrompt(command, words, opts) {
|
|
172
|
-
const content = await readPromptContent(words);
|
|
187
|
+
const content = await readPromptContent(words, { allowEmpty: Boolean(opts.image?.length) });
|
|
173
188
|
const scheduleFlags = [opts.delayMs, opts.at, opts.cron].filter((value) => value !== undefined);
|
|
174
189
|
if (scheduleFlags.length > 1)
|
|
175
190
|
return error("Conflicting schedule", "Use only one of --delay-ms, --at, or --cron");
|
|
@@ -185,15 +200,40 @@ async function sendPrompt(command, words, opts) {
|
|
|
185
200
|
: opts.cron
|
|
186
201
|
? { mode: "repeat", cronExpression: opts.cron, timezone: opts.timezone }
|
|
187
202
|
: undefined;
|
|
203
|
+
const sessionId = opts.session;
|
|
204
|
+
const imagePaths = opts.image ?? [];
|
|
205
|
+
const imageSessionId = imagePaths.length
|
|
206
|
+
? sessionId ?? error("Missing session", "Pass --session when attaching images.")
|
|
207
|
+
: "";
|
|
208
|
+
const imageBlocks = imagePaths.length
|
|
209
|
+
? await Promise.all(imagePaths.map(async (path) => {
|
|
210
|
+
const asset = await uploadChatImageAsset({ client, spaceId, sessionId: imageSessionId, path });
|
|
211
|
+
return {
|
|
212
|
+
type: "image",
|
|
213
|
+
source: { type: "url", url: asset.publicUrl },
|
|
214
|
+
_meta: {
|
|
215
|
+
filename: basename(path),
|
|
216
|
+
mediaType: "image/webp",
|
|
217
|
+
size: asset.size,
|
|
218
|
+
objectKey: asset.objectKey,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}))
|
|
222
|
+
: [];
|
|
223
|
+
const promptContent = [
|
|
224
|
+
...(content ? [{ type: "text", text: content }] : []),
|
|
225
|
+
...imageBlocks,
|
|
226
|
+
];
|
|
188
227
|
const result = await client.space(spaceId).prompt({
|
|
189
|
-
sessionId
|
|
190
|
-
title: opts.title,
|
|
228
|
+
sessionId,
|
|
229
|
+
title: sessionId === opts.session ? opts.title : undefined,
|
|
191
230
|
source: opts.source?.trim() || "cli",
|
|
192
|
-
content:
|
|
231
|
+
content: promptContent,
|
|
193
232
|
model: opts.model,
|
|
194
233
|
provider: opts.provider,
|
|
195
234
|
accessMode: opts.readOnly ? "read_only" : "full_access",
|
|
196
235
|
intent: opts.steer ? "steer" : undefined,
|
|
236
|
+
env: parseEnvOptions(opts.env),
|
|
197
237
|
schedule,
|
|
198
238
|
labelRefs: opts.label?.length ? opts.label : undefined,
|
|
199
239
|
});
|
|
@@ -225,6 +265,8 @@ export function registerPrompt(program) {
|
|
|
225
265
|
.option("--cron <expression>", "Repeat using a 5-field cron expression")
|
|
226
266
|
.option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
|
|
227
267
|
.option("--label <ref>", "Attach a label, e.g. Bug or Area/Frontend", collectOption, [])
|
|
268
|
+
.option("--env <key=value>", "Set an environment variable for this turn", collectOption, [])
|
|
269
|
+
.option("--image <path>", "Attach an image", collectOption, [])
|
|
228
270
|
.option("--json", "Output as JSON")
|
|
229
271
|
.action((words, opts) => sendPrompt(program, words, opts));
|
|
230
272
|
}
|
|
@@ -385,6 +427,8 @@ export function registerSpaces(program) {
|
|
|
385
427
|
.option("--cron <expression>", "Repeat using a 5-field cron expression")
|
|
386
428
|
.option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
|
|
387
429
|
.option("--label <ref>", "Attach a label, e.g. Bug or Area/Frontend", collectOption, [])
|
|
430
|
+
.option("--env <key=value>", "Set an environment variable for this turn", collectOption, [])
|
|
431
|
+
.option("--image <path>", "Attach an image", collectOption, [])
|
|
388
432
|
.option("--json", "Output as JSON")
|
|
389
433
|
.action((words, opts) => sendPrompt(spacesCmd, words, opts));
|
|
390
434
|
// ── spaces files ──
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createClient } from "../client.js";
|
|
2
|
+
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
|
+
import { resolveSpace } from "../space.js";
|
|
4
|
+
const WORK_STATUSES = ["draft", "published", "disabled"];
|
|
5
|
+
const collectOption = (value, previous = []) => [...previous, value];
|
|
6
|
+
function parseChoice(value, name, choices) {
|
|
7
|
+
if (choices.includes(value))
|
|
8
|
+
return value;
|
|
9
|
+
return error(`Invalid ${name}`, `Use one of: ${choices.join(", ")}`);
|
|
10
|
+
}
|
|
11
|
+
function parseJsonObject(value, name) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(value);
|
|
16
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// handled below
|
|
21
|
+
}
|
|
22
|
+
return error(`Invalid ${name}`, `${name} must be a JSON object`);
|
|
23
|
+
}
|
|
24
|
+
function compactObject(input) {
|
|
25
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
26
|
+
}
|
|
27
|
+
function resolveTarget(opts) {
|
|
28
|
+
const targets = [
|
|
29
|
+
opts.file ? { targetType: "file", targetRef: opts.file } : null,
|
|
30
|
+
opts.dir ? { targetType: "directory", targetRef: opts.dir } : null,
|
|
31
|
+
opts.port ? { targetType: "port", targetRef: opts.port } : null,
|
|
32
|
+
].filter((target) => Boolean(target));
|
|
33
|
+
if (targets.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
if (targets.length > 1)
|
|
36
|
+
return error("Conflicting target", "Use only one of --file, --dir, or --port");
|
|
37
|
+
return targets[0] ?? null;
|
|
38
|
+
}
|
|
39
|
+
function resolveStatus(opts) {
|
|
40
|
+
const values = [opts.status, opts.draft ? "draft" : undefined, opts.disabled ? "disabled" : undefined].filter(Boolean);
|
|
41
|
+
if (values.length > 1)
|
|
42
|
+
return error("Conflicting status", "Use only one of --status, --draft, or --disabled");
|
|
43
|
+
return values[0] ? parseChoice(values[0], "status", WORK_STATUSES) : "published";
|
|
44
|
+
}
|
|
45
|
+
function printWork(work) {
|
|
46
|
+
table([work], [
|
|
47
|
+
{ key: "id", label: "ID" },
|
|
48
|
+
{ key: "slug", label: "Slug" },
|
|
49
|
+
{ key: "status", label: "Status" },
|
|
50
|
+
{ key: "targetType", label: "Target" },
|
|
51
|
+
{ key: "targetRef", label: "Ref" },
|
|
52
|
+
{ key: "latestVersion", label: "Version" },
|
|
53
|
+
{ key: "publishedAt", label: "Published" },
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
async function confirmDelete(opts) {
|
|
57
|
+
if (opts.yes)
|
|
58
|
+
return;
|
|
59
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
60
|
+
return error("Confirmation required", "Pass --yes to delete the work.");
|
|
61
|
+
process.stdout.write("Deleting a work also removes its versions and viewer grants. Continue? [y/N] ");
|
|
62
|
+
const chunks = [];
|
|
63
|
+
for await (const chunk of process.stdin) {
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
|
|
68
|
+
if (answer !== "y" && answer !== "yes")
|
|
69
|
+
return error("Cancelled");
|
|
70
|
+
}
|
|
71
|
+
export function registerWorks(program) {
|
|
72
|
+
const worksCmd = program.command("works").description("Work management");
|
|
73
|
+
worksCmd
|
|
74
|
+
.command("ls")
|
|
75
|
+
.alias("list")
|
|
76
|
+
.description("List works in the target space")
|
|
77
|
+
.option("--json", "Output as JSON")
|
|
78
|
+
.action(async (opts) => {
|
|
79
|
+
const spaceId = resolveSpace(worksCmd);
|
|
80
|
+
const client = createClient();
|
|
81
|
+
try {
|
|
82
|
+
const result = await client.works.listBySpace(spaceId);
|
|
83
|
+
if (jsonRequested(opts))
|
|
84
|
+
return outJson(result);
|
|
85
|
+
table(result.works, [
|
|
86
|
+
{ key: "id", label: "ID" },
|
|
87
|
+
{ key: "slug", label: "Slug" },
|
|
88
|
+
{ key: "status", label: "Status" },
|
|
89
|
+
{ key: "targetType", label: "Target" },
|
|
90
|
+
{ key: "targetRef", label: "Ref" },
|
|
91
|
+
{ key: "latestVersion", label: "Version" },
|
|
92
|
+
{ key: "publishedAt", label: "Published" },
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
handleHttp(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
worksCmd
|
|
100
|
+
.command("get <id>")
|
|
101
|
+
.description("Show work details")
|
|
102
|
+
.option("--json", "Output as JSON")
|
|
103
|
+
.action(async (id, opts) => {
|
|
104
|
+
const client = createClient();
|
|
105
|
+
try {
|
|
106
|
+
const result = await client.works.get(id);
|
|
107
|
+
if (jsonRequested(opts))
|
|
108
|
+
return outJson(result);
|
|
109
|
+
printWork(result.work);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
handleHttp(e);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
worksCmd
|
|
116
|
+
.command("resolve <workSlug>")
|
|
117
|
+
.description("Resolve a published work by owner and space slug")
|
|
118
|
+
.option("--owner <username>", "Owner username")
|
|
119
|
+
.option("--space-slug <slug>", "Space slug")
|
|
120
|
+
.option("--json", "Output as JSON")
|
|
121
|
+
.action(async (workSlug, opts) => {
|
|
122
|
+
if (!opts.owner?.trim())
|
|
123
|
+
return error("Missing owner username", "Pass --owner <username>.");
|
|
124
|
+
if (!opts.spaceSlug?.trim())
|
|
125
|
+
return error("Missing space slug", "Pass --space-slug <slug>.");
|
|
126
|
+
const client = createClient();
|
|
127
|
+
try {
|
|
128
|
+
const result = await client.works.getBySlug(opts.owner.trim(), opts.spaceSlug.trim(), workSlug);
|
|
129
|
+
if (jsonRequested(opts))
|
|
130
|
+
return outJson(result);
|
|
131
|
+
printWork(result.work);
|
|
132
|
+
if (result.content?.url)
|
|
133
|
+
console.log(`\nURL: ${result.content.url}`);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
handleHttp(e);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
worksCmd
|
|
140
|
+
.command("publish <slug>")
|
|
141
|
+
.description("Create or publish a work in the target space")
|
|
142
|
+
.option("--file <path>", "Publish a HTML file")
|
|
143
|
+
.option("--dir <path>", "Publish a directory site")
|
|
144
|
+
.option("--port <port>", "Publish a public sandbox port")
|
|
145
|
+
.option("--draft", "Create as draft")
|
|
146
|
+
.option("--disabled", "Create as disabled")
|
|
147
|
+
.option("--status <status>", "Work status: draft, published, disabled")
|
|
148
|
+
.option("--work-scope <scope>", "Scope granted to the work runtime", collectOption, [])
|
|
149
|
+
.option("--viewer-scope <scope>", "Scope viewers may request", collectOption, [])
|
|
150
|
+
.option("--meta <json>", "Work metadata as a JSON object")
|
|
151
|
+
.option("--json", "Output as JSON")
|
|
152
|
+
.action(async (slug, opts) => {
|
|
153
|
+
const target = resolveTarget(opts);
|
|
154
|
+
if (!target)
|
|
155
|
+
return error("Missing target", "Use one of --file, --dir, or --port.");
|
|
156
|
+
const spaceId = resolveSpace(worksCmd);
|
|
157
|
+
const client = createClient();
|
|
158
|
+
const status = resolveStatus(opts);
|
|
159
|
+
const input = {
|
|
160
|
+
spaceId,
|
|
161
|
+
slug,
|
|
162
|
+
status,
|
|
163
|
+
targetType: target.targetType,
|
|
164
|
+
targetRef: target.targetRef,
|
|
165
|
+
workScopes: opts.workScope,
|
|
166
|
+
allowedViewerScopes: opts.viewerScope,
|
|
167
|
+
meta: parseJsonObject(opts.meta, "meta"),
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const result = await client.works.create(input);
|
|
171
|
+
if (jsonRequested(opts))
|
|
172
|
+
return outJson(result);
|
|
173
|
+
ok(`Work published: ${result.work.id}`);
|
|
174
|
+
printWork(result.work);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
handleHttp(e);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
worksCmd
|
|
181
|
+
.command("update <id>")
|
|
182
|
+
.description("Update work settings or publish a new version")
|
|
183
|
+
.option("--slug <slug>", "New work slug")
|
|
184
|
+
.option("--file <path>", "Use a HTML file target")
|
|
185
|
+
.option("--dir <path>", "Use a directory site target")
|
|
186
|
+
.option("--port <port>", "Use a public sandbox port target")
|
|
187
|
+
.option("--draft", "Set status to draft")
|
|
188
|
+
.option("--disabled", "Set status to disabled")
|
|
189
|
+
.option("--status <status>", "Work status: draft, published, disabled")
|
|
190
|
+
.option("--publish-version", "Force publishing a new version")
|
|
191
|
+
.option("--work-scope <scope>", "Scope granted to the work runtime", collectOption, [])
|
|
192
|
+
.option("--viewer-scope <scope>", "Scope viewers may request", collectOption, [])
|
|
193
|
+
.option("--clear-work-scopes", "Clear work runtime scopes")
|
|
194
|
+
.option("--clear-viewer-scopes", "Clear viewer-requestable scopes")
|
|
195
|
+
.option("--meta <json>", "Work metadata as a JSON object")
|
|
196
|
+
.option("--json", "Output as JSON")
|
|
197
|
+
.action(async (id, opts) => {
|
|
198
|
+
const target = resolveTarget(opts);
|
|
199
|
+
if (opts.clearWorkScopes && opts.workScope?.length)
|
|
200
|
+
return error("Conflicting work scopes", "Use either --work-scope or --clear-work-scopes.");
|
|
201
|
+
if (opts.clearViewerScopes && opts.viewerScope?.length)
|
|
202
|
+
return error("Conflicting viewer scopes", "Use either --viewer-scope or --clear-viewer-scopes.");
|
|
203
|
+
const input = compactObject({
|
|
204
|
+
slug: opts.slug,
|
|
205
|
+
status: opts.status || opts.draft || opts.disabled ? resolveStatus(opts) : undefined,
|
|
206
|
+
targetType: target?.targetType,
|
|
207
|
+
targetRef: target?.targetRef,
|
|
208
|
+
publishVersion: opts.publishVersion || undefined,
|
|
209
|
+
workScopes: opts.clearWorkScopes ? [] : opts.workScope?.length ? opts.workScope : undefined,
|
|
210
|
+
allowedViewerScopes: opts.clearViewerScopes ? [] : opts.viewerScope?.length ? opts.viewerScope : undefined,
|
|
211
|
+
meta: opts.meta !== undefined ? parseJsonObject(opts.meta, "meta") ?? null : undefined,
|
|
212
|
+
});
|
|
213
|
+
if (Object.keys(input).length === 0)
|
|
214
|
+
return error("Nothing to update", "Pass --slug, --file, --dir, --port, --status, --publish-version, --work-scope, --viewer-scope, --clear-work-scopes, --clear-viewer-scopes, or --meta.");
|
|
215
|
+
const client = createClient();
|
|
216
|
+
try {
|
|
217
|
+
const result = await client.works.update(id, input);
|
|
218
|
+
if (jsonRequested(opts))
|
|
219
|
+
return outJson(result);
|
|
220
|
+
ok("Work updated");
|
|
221
|
+
printWork(result.work);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
handleHttp(e);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
worksCmd
|
|
228
|
+
.command("versions <id>")
|
|
229
|
+
.description("List work versions")
|
|
230
|
+
.option("--json", "Output as JSON")
|
|
231
|
+
.action(async (id, opts) => {
|
|
232
|
+
const client = createClient();
|
|
233
|
+
try {
|
|
234
|
+
const result = await client.works.listVersions(id);
|
|
235
|
+
if (jsonRequested(opts))
|
|
236
|
+
return outJson(result);
|
|
237
|
+
table(result.versions, [
|
|
238
|
+
{ key: "version", label: "Version" },
|
|
239
|
+
{ key: "id", label: "ID" },
|
|
240
|
+
{ key: "status", label: "Status" },
|
|
241
|
+
{ key: "targetType", label: "Target" },
|
|
242
|
+
{ key: "targetRef", label: "Ref" },
|
|
243
|
+
{ key: "publishedAt", label: "Published" },
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
handleHttp(e);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
worksCmd
|
|
251
|
+
.command("rm <id>")
|
|
252
|
+
.alias("delete")
|
|
253
|
+
.description("Delete a work")
|
|
254
|
+
.option("-y, --yes", "Confirm deletion")
|
|
255
|
+
.action(async (id, opts) => {
|
|
256
|
+
await confirmDelete(opts);
|
|
257
|
+
const client = createClient();
|
|
258
|
+
try {
|
|
259
|
+
await client.works.delete(id);
|
|
260
|
+
ok("Work deleted");
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
handleHttp(e);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerProfile } from "./commands/profile.js";
|
|
|
10
10
|
import { registerSearch } from "./commands/search.js";
|
|
11
11
|
import { registerPrompt, registerSpaces } from "./commands/spaces.js";
|
|
12
12
|
import { registerTasks } from "./commands/tasks.js";
|
|
13
|
+
import { registerWorks } from "./commands/works.js";
|
|
13
14
|
import { ensureCliSelfUpdated } from "./self-update.js";
|
|
14
15
|
const VERSION = (() => {
|
|
15
16
|
try {
|
|
@@ -39,6 +40,7 @@ Common commands:
|
|
|
39
40
|
cohub search "release notes"
|
|
40
41
|
cohub -s <space-id> spaces sessions turns ls <session-id>
|
|
41
42
|
cohub -s <space-id> spaces files ls
|
|
43
|
+
cohub -s <space-id> works publish demo --file dist/index.html
|
|
42
44
|
cohub models ls
|
|
43
45
|
cohub models ls --model-type multimodal
|
|
44
46
|
cohub generate "A calm lake at sunrise" --model <model> --output lake.png
|
|
@@ -57,6 +59,7 @@ registerModels(program);
|
|
|
57
59
|
registerSearch(program);
|
|
58
60
|
registerTasks(program);
|
|
59
61
|
registerCronJobs(program);
|
|
62
|
+
registerWorks(program);
|
|
60
63
|
const isVersionRequest = (argv) => argv.some((arg) => arg === "-v" || arg === "--version");
|
|
61
64
|
try {
|
|
62
65
|
if (!isVersionRequest(process.argv.slice(2))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@neta-art/generation": "^0.1.5",
|
|
17
17
|
"commander": "^14.0.3",
|
|
18
18
|
"sharp": "^0.34.5",
|
|
19
|
-
"@neta-art/cohub": "1.
|
|
19
|
+
"@neta-art/cohub": "1.29.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|