@neta-art/cohub-cli 1.20.6 → 2.1.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 CHANGED
@@ -149,6 +149,16 @@ cohub generate "restyle this image" \
149
149
  --param size=1024x1024 \
150
150
  --json
151
151
 
152
+ cohub generate "smoothly transition from the first frame to the last frame" \
153
+ --model seedance-2-0-fast \
154
+ --image first_frame=https://example.com/first.png \
155
+ --image last_frame=https://example.com/last.png
156
+
157
+ cohub generate "keep the character identity from all reference images" \
158
+ --model seedance-2-0-fast \
159
+ --image reference_image=https://example.com/reference-1.png \
160
+ --image reference_image=https://example.com/reference-2.png
161
+
152
162
  cohub generate "a calm lake" \
153
163
  --model <model> \
154
164
  --async
@@ -160,10 +170,16 @@ Supported inputs:
160
170
 
161
171
  ```bash
162
172
  --image <path-or-url>
173
+ --image first_frame=<path-or-url>
174
+ --image last_frame=<path-or-url>
175
+ --image reference_image=<path-or-url>
163
176
  --video <path-or-url>
177
+ --video reference_video=<path-or-url>
164
178
  --audio <path-or-url>
165
179
  ```
166
180
 
181
+ Role-qualified media values add `meta.role` to that content block. Repeat `--image reference_image=...` for multiple reference images. Seedance role-qualified media should use public URL inputs. Do not mix first/last frame roles with reference roles in one request.
182
+
167
183
  Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
168
184
 
169
185
  ## Files
@@ -191,7 +207,7 @@ cohub works get <workId> --json
191
207
  cohub -s <spaceId> works publish demo --file dist/index.html
192
208
  cohub -s <spaceId> works publish site --dir dist
193
209
  cohub -s <spaceId> works publish app --port 3000
194
- cohub works update <workId> --publish-version
210
+ cohub works publish-version <workId>
195
211
  cohub works versions <workId> --json
196
212
  cohub works rm <workId> --yes
197
213
  ```
@@ -4,6 +4,14 @@ import { GenerationPolicyError, assertGenerationRequestAllowedByPolicy, parseGen
4
4
  import { createClient } from "../client.js";
5
5
  import { resolveSpace } from "../space.js";
6
6
  import { json as outJson, jsonRequested, ok, error, handleHttp, spinner } from "../output.js";
7
+ const frameMediaRoles = new Set(["first_frame", "last_frame"]);
8
+ const referenceMediaRoles = new Set(["reference_image", "reference_video"]);
9
+ const rolesByMediaType = {
10
+ image: new Set([...frameMediaRoles, "reference_image"]),
11
+ video: new Set(["reference_video"]),
12
+ audio: new Set(),
13
+ };
14
+ const mediaRoles = new Set([...frameMediaRoles, ...referenceMediaRoles]);
7
15
  const mimeByExt = {
8
16
  ".png": "image/png",
9
17
  ".jpg": "image/jpeg",
@@ -45,12 +53,47 @@ function parseParams(param, parameters) {
45
53
  }
46
54
  return Object.keys(result).length > 0 ? result : undefined;
47
55
  }
48
- async function contentFromPathOrUrl(type, value) {
49
- if (/^https?:\/\//.test(value))
50
- return { type, source: { type: "url", url: value } };
56
+ async function pathExists(path) {
57
+ return Boolean(await stat(path).catch(() => null));
58
+ }
59
+ async function parseMediaInput(type, rawValue) {
60
+ const separator = rawValue.indexOf("=");
61
+ if (separator <= 0)
62
+ return { value: rawValue };
63
+ const role = rawValue.slice(0, separator).trim();
64
+ if (!mediaRoles.has(role))
65
+ return { value: rawValue };
66
+ if (await pathExists(rawValue))
67
+ return { value: rawValue };
68
+ if (!rolesByMediaType[type].has(role)) {
69
+ return error("Invalid media role", `${role} cannot be used with --${type}`);
70
+ }
71
+ const value = rawValue.slice(separator + 1).trim();
72
+ if (!value)
73
+ return error("Invalid media input", `--${type} ${role}= requires a path or URL`);
74
+ return { value, role };
75
+ }
76
+ async function contentFromPathOrUrl(type, rawValue) {
77
+ const { value, role } = await parseMediaInput(type, rawValue);
78
+ const meta = role ? { role } : undefined;
79
+ if (/^https?:\/\//.test(value)) {
80
+ return { type, source: { type: "url", url: value }, ...(meta ? { meta } : {}) };
81
+ }
51
82
  const data = await readFile(value);
52
83
  const mediaType = mimeByExt[extname(value).toLowerCase()] ?? "application/octet-stream";
53
- return { type, source: { type: "base64", mediaType, data: data.toString("base64") } };
84
+ return {
85
+ type,
86
+ source: { type: "base64", mediaType, data: data.toString("base64") },
87
+ ...(meta ? { meta } : {}),
88
+ };
89
+ }
90
+ function validateMediaRoleModes(content) {
91
+ const roles = content.map(metaRole).filter((role) => Boolean(role));
92
+ const hasFrameRole = roles.some((role) => frameMediaRoles.has(role));
93
+ const hasReferenceRole = roles.some((role) => referenceMediaRoles.has(role));
94
+ if (hasFrameRole && hasReferenceRole) {
95
+ error("Invalid media role mix", "Use first_frame/last_frame or reference_image/reference_video, not both.");
96
+ }
54
97
  }
55
98
  async function saveOutputs(output, outputPath) {
56
99
  const outputs = output.filter((block) => block.type === "text" || block.type === "image" || block.type === "video" || block.type === "audio");
@@ -212,8 +255,8 @@ export function registerGenerations(program) {
212
255
  .description("Generate multimodal outputs")
213
256
  .argument("<prompt>", "Prompt text")
214
257
  .requiredOption("-m, --model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
215
- .option("--image <path-or-url>", "Image input file path or URL; repeatable", collect, [])
216
- .option("--video <path-or-url>", "Video input file path or URL; repeatable", collect, [])
258
+ .option("--image <path-or-url>", "Image input file path or URL; prefix with first_frame=, last_frame=, or reference_image= when needed; repeatable", collect, [])
259
+ .option("--video <path-or-url>", "Video input file path or URL; prefix with reference_video= when needed; repeatable", collect, [])
217
260
  .option("--audio <path-or-url>", "Audio input file path or URL; repeatable", collect, [])
218
261
  .option("--param <key=value>", "Generation parameter; repeatable, values may be JSON/number/boolean", collect, [])
219
262
  .option("--parameters <json>", "Generation parameters as a JSON object")
@@ -228,6 +271,8 @@ Examples:
228
271
  cohub models ls --model-type multimodal
229
272
  cohub -s <space-id> generate "A calm lake at sunrise" -m <model> -o lake.png
230
273
  COHUB_SPACE_ID=<space-id> cohub generate "Restyle this image" -m <model> --image input.png
274
+ cohub -s <space-id> generate "Smooth transition" -m seedance-2-0-fast --image first_frame=https://example.com/first.png --image last_frame=https://example.com/last.png
275
+ cohub -s <space-id> generate "Use these references" -m seedance-2-0-fast --image reference_image=https://example.com/a.png --image reference_image=https://example.com/b.png
231
276
  cohub -s <space-id> generate "A calm lake" -m <model> --async
232
277
  `)
233
278
  .action(async (prompt, opts) => {
@@ -237,6 +282,7 @@ Examples:
237
282
  content.push(...await Promise.all(opts.image.map((value) => contentFromPathOrUrl("image", value))));
238
283
  content.push(...await Promise.all(opts.video.map((value) => contentFromPathOrUrl("video", value))));
239
284
  content.push(...await Promise.all(opts.audio.map((value) => contentFromPathOrUrl("audio", value))));
285
+ validateMediaRoleModes(content);
240
286
  const parameters = parseParams(opts.param, opts.parameters);
241
287
  try {
242
288
  assertGenerationRequestAllowedByPolicy({
@@ -17,6 +17,7 @@ function printSection(title, lines) {
17
17
  }
18
18
  function formatContentSpec(spec) {
19
19
  const details = [];
20
+ const roles = spec.roles;
20
21
  details.push(spec.required === false ? "optional" : "required");
21
22
  if (typeof spec.min === "number")
22
23
  details.push(`min ${spec.min}`);
@@ -24,6 +25,8 @@ function formatContentSpec(spec) {
24
25
  details.push(`max ${spec.max}`);
25
26
  if (spec.sources?.length)
26
27
  details.push(`sources: ${spec.sources.join(", ")}`);
28
+ if (roles?.length)
29
+ details.push(`roles: ${roles.join(", ")}`);
27
30
  if (spec.merge)
28
31
  details.push(`merge: ${spec.merge}`);
29
32
  if (spec.description)
@@ -14,7 +14,7 @@ function shellJoin(values) {
14
14
  }
15
15
  function topLevelRunIndex(argv) {
16
16
  for (let index = 0; index < argv.length; index += 1) {
17
- const token = argv[index];
17
+ const token = argv[index] ?? "";
18
18
  if (token === "--")
19
19
  return -1;
20
20
  if (token === "-s" || token === "--space") {
@@ -31,7 +31,7 @@ function topLevelRunIndex(argv) {
31
31
  }
32
32
  function parseSpaceId(tokens) {
33
33
  for (let index = 0; index < tokens.length; index += 1) {
34
- const token = tokens[index];
34
+ const token = tokens[index] ?? "";
35
35
  if (token === "-s" || token === "--space") {
36
36
  const value = tokens[index + 1];
37
37
  if (!value)
@@ -55,7 +55,7 @@ function parseRunCliOptions(argv) {
55
55
  let commandOption = null;
56
56
  let positionalTokens = [];
57
57
  for (let index = 0; index < afterRun.length; index += 1) {
58
- const token = afterRun[index];
58
+ const token = afterRun[index] ?? "";
59
59
  if (token === "-h" || token === "--help") {
60
60
  printRunHelp();
61
61
  process.exit(0);
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerSpaceCommerce(spacesCmd: Command): void;
@@ -0,0 +1,453 @@
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 PRODUCT_STATUSES = ["draft", "active"];
5
+ const PRODUCT_UPDATE_STATUSES = ["draft", "active", "archived"];
6
+ const PRODUCT_VISIBILITIES = ["public", "private"];
7
+ const BENEFIT_STATUSES = ["active", "archived"];
8
+ function requireText(value, label, flag) {
9
+ const text = value?.trim();
10
+ if (text)
11
+ return text;
12
+ return error(`Missing ${label}`, `Pass ${flag}.`);
13
+ }
14
+ function parseChoice(value, label, choices) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ if (choices.includes(value))
18
+ return value;
19
+ return error(`Invalid ${label}`, `Use one of: ${choices.join(", ")}.`);
20
+ }
21
+ function parseInteger(value, label, options) {
22
+ if (value === undefined)
23
+ return options.fallback;
24
+ if (!/^-?\d+$/.test(value.trim()))
25
+ return error(`Invalid ${label}`, `${label} must be an integer.`);
26
+ const parsed = Number.parseInt(value, 10);
27
+ if (!Number.isSafeInteger(parsed))
28
+ return error(`Invalid ${label}`, `${label} must be a safe integer.`);
29
+ if (options.min !== undefined && parsed < options.min)
30
+ return error(`Invalid ${label}`, `${label} must be at least ${options.min}.`);
31
+ if (options.max !== undefined && parsed > options.max)
32
+ return error(`Invalid ${label}`, `${label} must be at most ${options.max}.`);
33
+ return parsed;
34
+ }
35
+ function parseAmountUsd(value) {
36
+ const text = requireText(value, "amount", "--amount-usd <amount>");
37
+ if (!/^(?:\d+|\d+\.\d{1,2}|\.\d{1,2})$/.test(text)) {
38
+ return error("Invalid amount", "--amount-usd must be a non-negative USD amount with at most 2 decimals.");
39
+ }
40
+ const amount = Number(text);
41
+ if (!Number.isFinite(amount))
42
+ return error("Invalid amount", "--amount-usd must be a finite USD amount.");
43
+ return amount;
44
+ }
45
+ function parseMetadataJson(value) {
46
+ if (value === undefined)
47
+ return undefined;
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(value);
51
+ }
52
+ catch {
53
+ return error("Invalid metadata", "--metadata-json must be a JSON object.");
54
+ }
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
56
+ return error("Invalid metadata", "--metadata-json must be a JSON object.");
57
+ }
58
+ const metadata = {};
59
+ for (const [key, item] of Object.entries(parsed)) {
60
+ if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
61
+ return error("Invalid metadata", `Metadata value for "${key}" must be string, number, or boolean.`);
62
+ }
63
+ if (typeof item === "number" && !Number.isFinite(item)) {
64
+ return error("Invalid metadata", `Metadata value for "${key}" must be a finite number.`);
65
+ }
66
+ metadata[key] = item;
67
+ }
68
+ return metadata;
69
+ }
70
+ function compact(input) {
71
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
72
+ }
73
+ function formatUsd(amount) {
74
+ return `$${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
75
+ }
76
+ function formatMinorUsd(amountMinor) {
77
+ return formatUsd(amountMinor / 100);
78
+ }
79
+ function printProducts(products) {
80
+ table(products.map((product) => ({
81
+ key: product.key,
82
+ name: product.name,
83
+ status: product.status,
84
+ visibility: product.visibility,
85
+ price: formatUsd(product.pricing.amountUsd),
86
+ })), [
87
+ { key: "key", label: "Key" },
88
+ { key: "name", label: "Name" },
89
+ { key: "status", label: "Status" },
90
+ { key: "visibility", label: "Visibility" },
91
+ { key: "price", label: "Price" },
92
+ ]);
93
+ }
94
+ function printBenefits(benefits) {
95
+ table(benefits.map((benefit) => ({
96
+ key: benefit.key,
97
+ name: benefit.name,
98
+ status: benefit.status,
99
+ description: benefit.description ?? "",
100
+ })), [
101
+ { key: "key", label: "Key" },
102
+ { key: "name", label: "Name" },
103
+ { key: "status", label: "Status" },
104
+ { key: "description", label: "Description" },
105
+ ]);
106
+ }
107
+ function printOrders(orders) {
108
+ table(orders.map((order) => ({
109
+ id: order.id,
110
+ product: order.productKeySnapshot,
111
+ status: order.status,
112
+ amount: formatMinorUsd(order.amountSnapshot),
113
+ created: order.createdAt,
114
+ })), [
115
+ { key: "id", label: "ID" },
116
+ { key: "product", label: "Product" },
117
+ { key: "status", label: "Status" },
118
+ { key: "amount", label: "Amount" },
119
+ { key: "created", label: "Created" },
120
+ ]);
121
+ }
122
+ async function confirmDanger(opts, detail) {
123
+ if (opts.yes)
124
+ return;
125
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
126
+ return error("Confirmation required", `Pass --yes to ${detail}.`);
127
+ process.stdout.write(`${detail[0]?.toUpperCase() ?? "C"}${detail.slice(1)}? [y/N] `);
128
+ const chunks = [];
129
+ for await (const chunk of process.stdin) {
130
+ chunks.push(chunk);
131
+ break;
132
+ }
133
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
134
+ if (answer !== "y" && answer !== "yes")
135
+ return error("Cancelled");
136
+ }
137
+ function commerceClient(spacesCmd) {
138
+ const spaceId = resolveSpace(spacesCmd);
139
+ return { spaceId, commerce: createClient().space(spaceId).commerce };
140
+ }
141
+ export function registerSpaceCommerce(spacesCmd) {
142
+ const commerceCmd = spacesCmd
143
+ .command("commerce")
144
+ .description("Manage space commerce")
145
+ .addHelpText("after", `
146
+ Examples:
147
+ cohub -s <space-id> spaces commerce setup
148
+ cohub -s <space-id> spaces commerce products list
149
+ COHUB_SPACE_ID=<space-id> cohub spaces commerce orders list --limit 10
150
+ `);
151
+ commerceCmd
152
+ .command("setup")
153
+ .description("Initialize commerce for the target space")
154
+ .option("--json", "Output as JSON")
155
+ .action(async (opts) => {
156
+ const { commerce } = commerceClient(spacesCmd);
157
+ try {
158
+ const result = await commerce.setup();
159
+ if (jsonRequested(opts))
160
+ return outJson(result);
161
+ ok(`Commerce initialized: ${result.businessKey}`);
162
+ }
163
+ catch (e) {
164
+ handleHttp(e);
165
+ }
166
+ });
167
+ const productsCmd = commerceCmd
168
+ .command("products")
169
+ .description("Manage products")
170
+ .addHelpText("after", `
171
+ Examples:
172
+ cohub -s <space-id> spaces commerce products list
173
+ cohub -s <space-id> spaces commerce products create --name "Pro Pack" --amount-usd 9.99
174
+ `);
175
+ productsCmd
176
+ .command("list")
177
+ .description("List products")
178
+ .option("--json", "Output as JSON")
179
+ .action(async (opts) => {
180
+ const { commerce } = commerceClient(spacesCmd);
181
+ try {
182
+ const result = await commerce.listProducts();
183
+ if (jsonRequested(opts))
184
+ return outJson(result);
185
+ printProducts(result.products);
186
+ }
187
+ catch (e) {
188
+ handleHttp(e);
189
+ }
190
+ });
191
+ productsCmd
192
+ .command("create")
193
+ .description("Create a product")
194
+ .option("--product-key <key>", "Product key override")
195
+ .option("--name <name>", "Product name")
196
+ .option("--amount-usd <amount>", "One-time price in USD")
197
+ .option("--description <text>", "Product description")
198
+ .option("--status <status>", "Product status: draft, active")
199
+ .option("--visibility <visibility>", "Product visibility: public, private")
200
+ .option("--json", "Output as JSON")
201
+ .action(async (opts) => {
202
+ const input = {
203
+ key: opts.productKey?.trim() || undefined,
204
+ name: requireText(opts.name, "name", "--name <name>"),
205
+ description: opts.description,
206
+ amountUsd: parseAmountUsd(opts.amountUsd),
207
+ status: parseChoice(opts.status, "status", PRODUCT_STATUSES),
208
+ visibility: parseChoice(opts.visibility, "visibility", PRODUCT_VISIBILITIES),
209
+ };
210
+ const { commerce } = commerceClient(spacesCmd);
211
+ try {
212
+ const result = await commerce.createProduct(input);
213
+ if (jsonRequested(opts))
214
+ return outJson(result);
215
+ ok(`Product created: ${result.product.key}`);
216
+ printProducts([result.product]);
217
+ }
218
+ catch (e) {
219
+ handleHttp(e);
220
+ }
221
+ });
222
+ productsCmd
223
+ .command("update")
224
+ .description("Update a product")
225
+ .option("--product-key <key>", "Product key")
226
+ .option("--name <name>", "Product name")
227
+ .option("--description <text>", "Product description")
228
+ .option("--clear-description", "Clear the product description")
229
+ .option("--status <status>", "Product status: draft, active, archived")
230
+ .option("--visibility <visibility>", "Product visibility: public, private")
231
+ .option("--json", "Output as JSON")
232
+ .action(async (opts) => {
233
+ if (opts.description !== undefined && opts.clearDescription)
234
+ return error("Conflicting description", "Use either --description or --clear-description.");
235
+ const productKey = requireText(opts.productKey, "product key", "--product-key <key>");
236
+ const input = compact({
237
+ name: opts.name,
238
+ description: opts.clearDescription ? null : opts.description,
239
+ status: parseChoice(opts.status, "status", PRODUCT_UPDATE_STATUSES),
240
+ visibility: parseChoice(opts.visibility, "visibility", PRODUCT_VISIBILITIES),
241
+ });
242
+ if (Object.keys(input).length === 0)
243
+ return error("Nothing to update", "Pass --name, --description, --clear-description, --status, or --visibility.");
244
+ const { commerce } = commerceClient(spacesCmd);
245
+ try {
246
+ const result = await commerce.updateProduct(productKey, input);
247
+ if (jsonRequested(opts))
248
+ return outJson(result);
249
+ ok(`Product updated: ${result.product.key}`);
250
+ printProducts([result.product]);
251
+ }
252
+ catch (e) {
253
+ handleHttp(e);
254
+ }
255
+ });
256
+ productsCmd
257
+ .command("archive")
258
+ .description("Archive a product")
259
+ .option("--product-key <key>", "Product key")
260
+ .option("-y, --yes", "Confirm archive")
261
+ .option("--json", "Output as JSON")
262
+ .action(async (opts) => {
263
+ const productKey = requireText(opts.productKey, "product key", "--product-key <key>");
264
+ await confirmDanger(opts, `archive product "${productKey}"`);
265
+ const { commerce } = commerceClient(spacesCmd);
266
+ try {
267
+ const result = await commerce.updateProduct(productKey, { status: "archived" });
268
+ if (jsonRequested(opts))
269
+ return outJson(result);
270
+ ok(`Product archived: ${result.product.key}`);
271
+ }
272
+ catch (e) {
273
+ handleHttp(e);
274
+ }
275
+ });
276
+ const benefitsCmd = commerceCmd
277
+ .command("benefits")
278
+ .description("Manage benefits")
279
+ .addHelpText("after", `
280
+ Examples:
281
+ cohub -s <space-id> spaces commerce benefits list
282
+ cohub -s <space-id> spaces commerce benefits create --name "Premium Export" --metadata-json '{"limit":100}'
283
+ `);
284
+ benefitsCmd
285
+ .command("list")
286
+ .description("List benefits")
287
+ .option("--json", "Output as JSON")
288
+ .action(async (opts) => {
289
+ const { commerce } = commerceClient(spacesCmd);
290
+ try {
291
+ const result = await commerce.listBenefits();
292
+ if (jsonRequested(opts))
293
+ return outJson(result);
294
+ printBenefits(result.benefits);
295
+ }
296
+ catch (e) {
297
+ handleHttp(e);
298
+ }
299
+ });
300
+ benefitsCmd
301
+ .command("create")
302
+ .description("Create a benefit")
303
+ .option("--benefit-key <key>", "Benefit key override")
304
+ .option("--name <name>", "Benefit name")
305
+ .option("--description <text>", "Benefit description")
306
+ .option("--metadata-json <json>", "Benefit metadata JSON object")
307
+ .option("--json", "Output as JSON")
308
+ .action(async (opts) => {
309
+ const input = {
310
+ key: opts.benefitKey?.trim() || undefined,
311
+ name: requireText(opts.name, "name", "--name <name>"),
312
+ description: opts.description,
313
+ metadata: parseMetadataJson(opts.metadataJson),
314
+ };
315
+ const { commerce } = commerceClient(spacesCmd);
316
+ try {
317
+ const result = await commerce.createBenefit(input);
318
+ if (jsonRequested(opts))
319
+ return outJson(result);
320
+ ok(`Benefit created: ${result.benefit.key}`);
321
+ printBenefits([result.benefit]);
322
+ }
323
+ catch (e) {
324
+ handleHttp(e);
325
+ }
326
+ });
327
+ benefitsCmd
328
+ .command("update")
329
+ .description("Update a benefit")
330
+ .option("--benefit-key <key>", "Benefit key")
331
+ .option("--name <name>", "Benefit name")
332
+ .option("--description <text>", "Benefit description")
333
+ .option("--clear-description", "Clear the benefit description")
334
+ .option("--status <status>", "Benefit status: active, archived")
335
+ .option("--metadata-json <json>", "Replace metadata with a JSON object")
336
+ .option("--json", "Output as JSON")
337
+ .action(async (opts) => {
338
+ if (opts.description !== undefined && opts.clearDescription)
339
+ return error("Conflicting description", "Use either --description or --clear-description.");
340
+ const benefitKey = requireText(opts.benefitKey, "benefit key", "--benefit-key <key>");
341
+ const input = compact({
342
+ name: opts.name,
343
+ description: opts.clearDescription ? null : opts.description,
344
+ status: parseChoice(opts.status, "status", BENEFIT_STATUSES),
345
+ metadata: parseMetadataJson(opts.metadataJson),
346
+ });
347
+ if (Object.keys(input).length === 0)
348
+ return error("Nothing to update", "Pass --name, --description, --clear-description, --status, or --metadata-json.");
349
+ const { commerce } = commerceClient(spacesCmd);
350
+ try {
351
+ const result = await commerce.updateBenefit(benefitKey, input);
352
+ if (jsonRequested(opts))
353
+ return outJson(result);
354
+ ok(`Benefit updated: ${result.benefit.key}`);
355
+ printBenefits([result.benefit]);
356
+ }
357
+ catch (e) {
358
+ handleHttp(e);
359
+ }
360
+ });
361
+ benefitsCmd
362
+ .command("archive")
363
+ .description("Archive a benefit")
364
+ .option("--benefit-key <key>", "Benefit key")
365
+ .option("-y, --yes", "Confirm archive")
366
+ .option("--json", "Output as JSON")
367
+ .action(async (opts) => {
368
+ const benefitKey = requireText(opts.benefitKey, "benefit key", "--benefit-key <key>");
369
+ await confirmDanger(opts, `archive benefit "${benefitKey}"`);
370
+ const { commerce } = commerceClient(spacesCmd);
371
+ try {
372
+ const result = await commerce.updateBenefit(benefitKey, { status: "archived" });
373
+ if (jsonRequested(opts))
374
+ return outJson(result);
375
+ ok(`Benefit archived: ${result.benefit.key}`);
376
+ }
377
+ catch (e) {
378
+ handleHttp(e);
379
+ }
380
+ });
381
+ commerceCmd
382
+ .command("bind")
383
+ .description("Bind a benefit to a product")
384
+ .option("--product-key <key>", "Product key")
385
+ .option("--benefit-key <key>", "Benefit key")
386
+ .option("--json", "Output as JSON")
387
+ .action(async (opts) => {
388
+ const input = {
389
+ productKey: requireText(opts.productKey, "product key", "--product-key <key>"),
390
+ benefitKey: requireText(opts.benefitKey, "benefit key", "--benefit-key <key>"),
391
+ };
392
+ const { commerce } = commerceClient(spacesCmd);
393
+ try {
394
+ const result = await commerce.bindProductBenefit(input);
395
+ if (jsonRequested(opts))
396
+ return outJson(result);
397
+ ok(`Benefit bound: ${input.benefitKey} -> ${input.productKey}`);
398
+ }
399
+ catch (e) {
400
+ handleHttp(e);
401
+ }
402
+ });
403
+ commerceCmd
404
+ .command("unbind")
405
+ .description("Unbind a benefit from a product")
406
+ .option("--product-key <key>", "Product key")
407
+ .option("--benefit-key <key>", "Benefit key")
408
+ .option("-y, --yes", "Confirm unbind")
409
+ .option("--json", "Output as JSON")
410
+ .action(async (opts) => {
411
+ const input = {
412
+ productKey: requireText(opts.productKey, "product key", "--product-key <key>"),
413
+ benefitKey: requireText(opts.benefitKey, "benefit key", "--benefit-key <key>"),
414
+ };
415
+ await confirmDanger(opts, `unbind benefit "${input.benefitKey}" from product "${input.productKey}"`);
416
+ const { commerce } = commerceClient(spacesCmd);
417
+ try {
418
+ const result = await commerce.unbindProductBenefit(input);
419
+ if (jsonRequested(opts))
420
+ return outJson(result);
421
+ ok(`Benefit unbound: ${input.benefitKey} -> ${input.productKey}`);
422
+ }
423
+ catch (e) {
424
+ handleHttp(e);
425
+ }
426
+ });
427
+ const ordersCmd = commerceCmd
428
+ .command("orders")
429
+ .description("Manage orders");
430
+ ordersCmd
431
+ .command("list")
432
+ .description("List recent orders")
433
+ .option("--page <page>", "Page number")
434
+ .option("--limit <limit>", "Page size, max 50")
435
+ .option("--json", "Output as JSON")
436
+ .action(async (opts) => {
437
+ const { commerce } = commerceClient(spacesCmd);
438
+ try {
439
+ const result = await commerce.listOrders({
440
+ page: parseInteger(opts.page, "page", { fallback: 1, min: 1 }),
441
+ limit: parseInteger(opts.limit, "limit", { fallback: 20, min: 1, max: 50 }),
442
+ });
443
+ if (jsonRequested(opts))
444
+ return outJson(result);
445
+ printOrders(result.orders);
446
+ if (result.pagination.hasMore)
447
+ console.log(`\nNext page: ${result.pagination.nextPage}`);
448
+ }
449
+ catch (e) {
450
+ handleHttp(e);
451
+ }
452
+ });
453
+ }
@@ -7,6 +7,7 @@ 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";
10
+ import { registerSpaceCommerce } from "./space-commerce.js";
10
11
  const cliEnv = resolveCohubEnvironment();
11
12
  const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
12
13
  const SPACE_ROLES = ["host", "builder", "guest"];
@@ -478,6 +479,8 @@ export function registerSpaces(program) {
478
479
  registerMods(spacesCmd);
479
480
  // ── spaces labels ──
480
481
  registerLabels(spacesCmd);
482
+ // ── spaces commerce ──
483
+ registerSpaceCommerce(spacesCmd);
481
484
  // ── spaces usage ──
482
485
  spacesCmd
483
486
  .command("usage [days]")
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerWorkCommerce(worksCmd: Command): void;
@@ -0,0 +1,185 @@
1
+ import { createClient } from "../client.js";
2
+ import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
3
+ function collectOption(value, previous = []) {
4
+ return [...previous, value];
5
+ }
6
+ function requireText(value, label, flag) {
7
+ const text = value?.trim();
8
+ if (text)
9
+ return text;
10
+ return error(`Missing ${label}`, `Pass ${flag}.`);
11
+ }
12
+ function requireList(values, label, flag) {
13
+ const items = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
14
+ if (items.length > 0)
15
+ return items;
16
+ return error(`Missing ${label}`, `Pass ${flag}.`);
17
+ }
18
+ function formatUsd(amount) {
19
+ return `$${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
20
+ }
21
+ function formatMinorUsd(amountMinor) {
22
+ return formatUsd(amountMinor / 100);
23
+ }
24
+ function printProducts(products) {
25
+ table(products.map((product) => ({
26
+ key: product.key,
27
+ name: product.name,
28
+ status: product.status,
29
+ visibility: product.visibility,
30
+ price: formatUsd(product.pricing.amountUsd),
31
+ })), [
32
+ { key: "key", label: "Key" },
33
+ { key: "name", label: "Name" },
34
+ { key: "status", label: "Status" },
35
+ { key: "visibility", label: "Visibility" },
36
+ { key: "price", label: "Price" },
37
+ ]);
38
+ }
39
+ function printEntitlements(entitlements) {
40
+ table(entitlements.map((item) => ({
41
+ benefitKey: item.benefitKey,
42
+ enabled: item.enabled ? "yes" : "no",
43
+ reason: item.reason,
44
+ metadata: item.metadata ? JSON.stringify(item.metadata) : "",
45
+ })), [
46
+ { key: "benefitKey", label: "Benefit" },
47
+ { key: "enabled", label: "Enabled" },
48
+ { key: "reason", label: "Reason" },
49
+ { key: "metadata", label: "Metadata" },
50
+ ]);
51
+ }
52
+ function printOrder(order) {
53
+ table([{
54
+ id: order.id,
55
+ product: order.productKeySnapshot,
56
+ status: order.status,
57
+ amount: formatMinorUsd(order.amountSnapshot),
58
+ paid: formatMinorUsd(order.paidAmountSnapshot),
59
+ created: order.createdAt,
60
+ paidAt: order.paidAt ?? "",
61
+ }], [
62
+ { key: "id", label: "ID" },
63
+ { key: "product", label: "Product" },
64
+ { key: "status", label: "Status" },
65
+ { key: "amount", label: "Amount" },
66
+ { key: "paid", label: "Paid" },
67
+ { key: "created", label: "Created" },
68
+ { key: "paidAt", label: "Paid At" },
69
+ ]);
70
+ }
71
+ export function registerWorkCommerce(worksCmd) {
72
+ const commerceCmd = worksCmd
73
+ .command("commerce")
74
+ .description("Debug work commerce")
75
+ .addHelpText("after", `
76
+ Examples:
77
+ cohub works commerce products resolve --work-id <work-id> --product-key pro_pack
78
+ cohub works commerce entitlements check --work-id <work-id> --benefit-key premium_export
79
+ `);
80
+ const productsCmd = commerceCmd
81
+ .command("products")
82
+ .description("Debug commerce products");
83
+ productsCmd
84
+ .command("resolve")
85
+ .description("Resolve public products for a work")
86
+ .option("--work-id <id>", "Work ID")
87
+ .option("--product-key <key>", "Product key", collectOption)
88
+ .option("--json", "Output as JSON")
89
+ .action(async (opts) => {
90
+ const workId = requireText(opts.workId, "work ID", "--work-id <id>");
91
+ const productKeys = requireList(opts.productKey, "product key", "--product-key <key>");
92
+ const client = createClient();
93
+ try {
94
+ const result = await client.workCommerce.resolveProducts(workId, { productKeys });
95
+ if (jsonRequested(opts))
96
+ return outJson(result);
97
+ printProducts(result.products);
98
+ }
99
+ catch (e) {
100
+ handleHttp(e);
101
+ }
102
+ });
103
+ const entitlementsCmd = commerceCmd
104
+ .command("entitlements")
105
+ .description("Debug commerce entitlements");
106
+ entitlementsCmd
107
+ .command("check")
108
+ .description("Check viewer entitlements for a work")
109
+ .option("--work-id <id>", "Work ID")
110
+ .option("--benefit-key <key>", "Benefit key", collectOption)
111
+ .option("--json", "Output as JSON")
112
+ .action(async (opts) => {
113
+ const workId = requireText(opts.workId, "work ID", "--work-id <id>");
114
+ const benefitKeys = requireList(opts.benefitKey, "benefit key", "--benefit-key <key>");
115
+ const client = createClient();
116
+ try {
117
+ const result = await client.workCommerce.checkEntitlements(workId, { benefitKeys });
118
+ if (jsonRequested(opts))
119
+ return outJson(result);
120
+ printEntitlements(result.entitlements);
121
+ console.log(`\nChecked at: ${result.checkedAt}`);
122
+ }
123
+ catch (e) {
124
+ handleHttp(e);
125
+ }
126
+ });
127
+ commerceCmd
128
+ .command("purchase")
129
+ .description("Create a work purchase checkout")
130
+ .option("--work-id <id>", "Work ID")
131
+ .option("--product-key <key>", "Product key")
132
+ .option("--json", "Output as JSON")
133
+ .action(async (opts) => {
134
+ const workId = requireText(opts.workId, "work ID", "--work-id <id>");
135
+ const productKey = requireText(opts.productKey, "product key", "--product-key <key>");
136
+ const client = createClient();
137
+ try {
138
+ const result = await client.workCommerce.purchase(workId, { productKey });
139
+ if (jsonRequested(opts))
140
+ return outJson(result);
141
+ ok(`Checkout created: ${result.checkout.orderId}`);
142
+ table([{
143
+ orderId: result.checkout.orderId,
144
+ productKey: result.checkout.productKey,
145
+ usable: result.checkout.checkoutUsable ? "yes" : "no",
146
+ status: result.checkout.status ?? "",
147
+ checkoutUrl: result.checkout.checkoutUrl ?? "",
148
+ message: result.checkout.message ?? "",
149
+ }], [
150
+ { key: "orderId", label: "Order" },
151
+ { key: "productKey", label: "Product" },
152
+ { key: "usable", label: "Usable" },
153
+ { key: "status", label: "Status" },
154
+ { key: "checkoutUrl", label: "Checkout URL" },
155
+ { key: "message", label: "Message" },
156
+ ]);
157
+ }
158
+ catch (e) {
159
+ handleHttp(e);
160
+ }
161
+ });
162
+ const ordersCmd = commerceCmd
163
+ .command("orders")
164
+ .description("Debug commerce orders");
165
+ ordersCmd
166
+ .command("get")
167
+ .description("Show a work commerce order")
168
+ .option("--work-id <id>", "Work ID")
169
+ .option("--order-id <id>", "Order ID")
170
+ .option("--json", "Output as JSON")
171
+ .action(async (opts) => {
172
+ const workId = requireText(opts.workId, "work ID", "--work-id <id>");
173
+ const orderId = requireText(opts.orderId, "order ID", "--order-id <id>");
174
+ const client = createClient();
175
+ try {
176
+ const result = await client.workCommerce.getOrder(workId, orderId);
177
+ if (jsonRequested(opts))
178
+ return outJson(result);
179
+ printOrder(result.order);
180
+ }
181
+ catch (e) {
182
+ handleHttp(e);
183
+ }
184
+ });
185
+ }
@@ -1,7 +1,10 @@
1
+ import { HttpError } from "@neta-art/cohub";
1
2
  import { createClient } from "../client.js";
2
3
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
3
4
  import { resolveSpace } from "../space.js";
4
- const WORK_STATUSES = ["draft", "published", "disabled"];
5
+ import { registerWorkCommerce } from "./work-commerce.js";
6
+ const WORK_STATUSES = ["published", "disabled"];
7
+ const WORK_VISIBILITIES = ["public", "space"];
5
8
  const collectOption = (value, previous = []) => [...previous, value];
6
9
  function parseChoice(value, name, choices) {
7
10
  if (choices.includes(value))
@@ -54,16 +57,20 @@ function resolveTarget(opts) {
54
57
  return targets[0] ?? null;
55
58
  }
56
59
  function resolveStatus(opts) {
57
- const values = [opts.status, opts.draft ? "draft" : undefined, opts.disabled ? "disabled" : undefined].filter(Boolean);
60
+ const values = [opts.status, opts.disabled ? "disabled" : undefined].filter(Boolean);
58
61
  if (values.length > 1)
59
- return error("Conflicting status", "Use only one of --status, --draft, or --disabled");
62
+ return error("Conflicting status", "Use only one of --status or --disabled");
60
63
  return values[0] ? parseChoice(values[0], "status", WORK_STATUSES) : "published";
61
64
  }
65
+ function resolveVisibility(value) {
66
+ return value ? parseChoice(value, "visibility", WORK_VISIBILITIES) : undefined;
67
+ }
62
68
  function printWork(work) {
63
69
  table([work], [
64
70
  { key: "id", label: "ID" },
65
71
  { key: "slug", label: "Slug" },
66
72
  { key: "status", label: "Status" },
73
+ { key: "visibility", label: "Visibility" },
67
74
  { key: "targetType", label: "Target" },
68
75
  { key: "targetRef", label: "Ref" },
69
76
  { key: "latestVersion", label: "Version" },
@@ -93,6 +100,19 @@ async function confirmDelete(opts) {
93
100
  if (answer !== "y" && answer !== "yes")
94
101
  return error("Cancelled");
95
102
  }
103
+ async function publishWorkVersion(id, opts) {
104
+ const client = createClient();
105
+ try {
106
+ const result = await client.works.publishVersion(id);
107
+ if (jsonRequested(opts))
108
+ return outJson(result);
109
+ ok(`Work version updated: v${result.version.version}`);
110
+ printWork(result.work);
111
+ }
112
+ catch (e) {
113
+ handleHttp(e);
114
+ }
115
+ }
96
116
  export function registerWorks(program) {
97
117
  const worksCmd = program.command("works").description("Work management");
98
118
  worksCmd
@@ -111,6 +131,7 @@ export function registerWorks(program) {
111
131
  { key: "id", label: "ID" },
112
132
  { key: "slug", label: "Slug" },
113
133
  { key: "status", label: "Status" },
134
+ { key: "visibility", label: "Visibility" },
114
135
  { key: "targetType", label: "Target" },
115
136
  { key: "targetRef", label: "Ref" },
116
137
  { key: "latestVersion", label: "Version" },
@@ -167,9 +188,9 @@ export function registerWorks(program) {
167
188
  .option("--file <path>", "Publish a HTML file")
168
189
  .option("--dir <path>", "Publish a directory site")
169
190
  .option("--port <port>", "Publish a public sandbox port")
170
- .option("--draft", "Create as draft")
171
191
  .option("--disabled", "Create as disabled")
172
- .option("--status <status>", "Work status: draft, published, disabled")
192
+ .option("--status <status>", "Work status: published, disabled")
193
+ .option("--visibility <visibility>", "Work visibility: public, space")
173
194
  .option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
174
195
  .option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
175
196
  .option("--meta <json>", "Work metadata as a JSON object")
@@ -194,6 +215,7 @@ export function registerWorks(program) {
194
215
  spaceId,
195
216
  slug,
196
217
  status,
218
+ visibility: resolveVisibility(opts.visibility),
197
219
  targetType: target.targetType,
198
220
  targetRef: target.targetRef,
199
221
  workScopes: opts.workScope,
@@ -208,20 +230,44 @@ export function registerWorks(program) {
208
230
  printWork(result.work);
209
231
  }
210
232
  catch (e) {
211
- handleHttp(e);
233
+ if (!(e instanceof HttpError) || e.status !== 409)
234
+ handleHttp(e);
235
+ try {
236
+ const { works } = await client.works.listBySpace(spaceId);
237
+ const existingWork = works.find((work) => work.slug === slug);
238
+ if (!existingWork)
239
+ return handleHttp(e);
240
+ const { work } = await client.works.update(existingWork.id, {
241
+ status: status === "published" && existingWork.status !== "published" ? existingWork.status : status,
242
+ visibility: resolveVisibility(opts.visibility),
243
+ targetType: target.targetType,
244
+ targetRef: target.targetRef,
245
+ workScopes: opts.workScope,
246
+ allowedViewerScopes: opts.viewerScope,
247
+ meta,
248
+ });
249
+ const publishedVersion = status === "published" ? await client.works.publishVersion(work.id) : null;
250
+ const result = publishedVersion ?? { work };
251
+ if (jsonRequested(opts))
252
+ return outJson(result);
253
+ ok(status === "published" && publishedVersion ? `Work version updated: v${publishedVersion.version.version}` : `Work updated: ${work.id}`);
254
+ printWork(result.work);
255
+ }
256
+ catch (fallbackError) {
257
+ handleHttp(fallbackError);
258
+ }
212
259
  }
213
260
  });
214
261
  worksCmd
215
262
  .command("update <id>")
216
- .description("Update work settings or publish a new version")
263
+ .description("Update work settings")
217
264
  .option("--slug <slug>", "New work slug")
218
265
  .option("--file <path>", "Use a HTML file target")
219
266
  .option("--dir <path>", "Use a directory site target")
220
267
  .option("--port <port>", "Use a public sandbox port target")
221
- .option("--draft", "Set status to draft")
222
268
  .option("--disabled", "Set status to disabled")
223
- .option("--status <status>", "Work status: draft, published, disabled")
224
- .option("--publish-version", "Force publishing a new version")
269
+ .option("--status <status>", "Work status: published, disabled")
270
+ .option("--visibility <visibility>", "Work visibility: public, space")
225
271
  .option("--work-scope <scope>", "Scope granted to the work runtime (space.view, session.view, file.view, taskrun.view)", collectOption, [])
226
272
  .option("--viewer-scope <scope>", "Scope viewers may request (session.prompt.readonly, session.prompt.fullaccess, generation.create, user.space.list, user.session.list, user.usage.read)", collectOption, [])
227
273
  .option("--clear-work-scopes", "Clear work runtime scopes")
@@ -257,29 +303,45 @@ export function registerWorks(program) {
257
303
  showCohubBar: opts.showCohubBar,
258
304
  });
259
305
  }
306
+ const nextStatus = opts.status || opts.disabled ? resolveStatus(opts) : undefined;
307
+ const currentWork = nextStatus === "published" ? (await client.works.get(id)).work : null;
260
308
  const input = compactObject({
261
309
  slug: opts.slug,
262
- status: opts.status || opts.draft || opts.disabled ? resolveStatus(opts) : undefined,
310
+ status: nextStatus === "published" && currentWork?.status !== "published" ? currentWork?.status : nextStatus,
311
+ visibility: resolveVisibility(opts.visibility),
263
312
  targetType: target?.targetType,
264
313
  targetRef: target?.targetRef,
265
- publishVersion: opts.publishVersion || undefined,
266
314
  workScopes: opts.clearWorkScopes ? [] : opts.workScope?.length ? opts.workScope : undefined,
267
315
  allowedViewerScopes: opts.clearViewerScopes ? [] : opts.viewerScope?.length ? opts.viewerScope : undefined,
268
316
  meta,
269
317
  });
270
318
  if (Object.keys(input).length === 0)
271
- return error("Nothing to update", "Pass --slug, --file, --dir, --port, --status, --publish-version, --work-scope, --viewer-scope, --clear-work-scopes, --clear-viewer-scopes, --meta, --hide-cohub-bar, or --show-cohub-bar.");
319
+ return error("Nothing to update", "Pass --slug, --file, --dir, --port, --status, --visibility, --work-scope, --viewer-scope, --clear-work-scopes, --clear-viewer-scopes, --meta, --hide-cohub-bar, or --show-cohub-bar.");
272
320
  try {
273
- const result = await client.works.update(id, input);
321
+ const updated = await client.works.update(id, input);
322
+ const publishedVersion = nextStatus === "published" && currentWork?.status !== "published"
323
+ ? await client.works.publishVersion(updated.work.id)
324
+ : null;
325
+ const result = publishedVersion ?? updated;
274
326
  if (jsonRequested(opts))
275
327
  return outJson(result);
276
- ok("Work updated");
328
+ ok(publishedVersion ? `Work published: v${publishedVersion.version.version}` : "Work updated");
277
329
  printWork(result.work);
278
330
  }
279
331
  catch (e) {
280
332
  handleHttp(e);
281
333
  }
282
334
  });
335
+ worksCmd
336
+ .command("publish-version <id>")
337
+ .description("Publish or update the current work version")
338
+ .option("--json", "Output as JSON")
339
+ .action(publishWorkVersion);
340
+ worksCmd
341
+ .command("release <id>", { hidden: true })
342
+ .description("Deprecated alias for publish-version")
343
+ .option("--json", "Output as JSON")
344
+ .action(publishWorkVersion);
283
345
  worksCmd
284
346
  .command("versions <id>")
285
347
  .description("List work versions")
@@ -293,16 +355,16 @@ export function registerWorks(program) {
293
355
  table(result.versions, [
294
356
  { key: "version", label: "Version" },
295
357
  { key: "id", label: "ID" },
296
- { key: "status", label: "Status" },
297
358
  { key: "targetType", label: "Target" },
298
359
  { key: "targetRef", label: "Ref" },
299
- { key: "publishedAt", label: "Published" },
360
+ { key: "createdAt", label: "Created" },
300
361
  ]);
301
362
  }
302
363
  catch (e) {
303
364
  handleHttp(e);
304
365
  }
305
366
  });
367
+ registerWorkCommerce(worksCmd);
306
368
  worksCmd
307
369
  .command("rm <id>")
308
370
  .alias("delete")
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ Common commands:
44
44
  cohub -s <space-id> spaces sessions turns ls <session-id>
45
45
  cohub -s <space-id> spaces files ls
46
46
  cohub -s <space-id> works publish demo --file dist/index.html
47
+ cohub -s <space-id> spaces commerce products list
47
48
  cohub models ls
48
49
  cohub models ls --model-type multimodal
49
50
  cohub generate "A calm lake at sunrise" --model <model> --output lake.png
package/dist/output.js CHANGED
@@ -85,15 +85,28 @@ function fetchFailureDetail(e) {
85
85
  ? `Network request failed (${parts.join(" · ")}). Check DNS/proxy/firewall settings and try again.`
86
86
  : "Network request failed. Check DNS/proxy/firewall settings and try again.";
87
87
  }
88
+ function errorPresentationFromHttpError(e) {
89
+ const httpError = e;
90
+ if (httpError.code === "space_commerce_not_initialized") {
91
+ return {
92
+ message: "space commerce is not initialized",
93
+ detail: "run `cohub -s <space-id> spaces commerce setup` first",
94
+ };
95
+ }
96
+ return null;
97
+ }
88
98
  export function handleHttp(e) {
89
99
  if (e instanceof Error && e.name === "AuthRequiredError") {
90
100
  return error("not authenticated", "run `cohub auth login`");
91
101
  }
92
102
  const status = e.status;
93
103
  const body = e.body;
94
- const message = errorMessageFromBody(body) ?? (e instanceof Error ? e.message : String(e));
104
+ const presentation = errorPresentationFromHttpError(e);
105
+ const message = presentation?.message ?? errorMessageFromBody(body) ?? (e instanceof Error ? e.message : String(e));
95
106
  const fetchDetail = fetchFailureDetail(e);
96
107
  const detailParts = [];
108
+ if (presentation?.detail)
109
+ detailParts.push(presentation.detail);
97
110
  if (process.env.COHUB_DEBUG_ERRORS) {
98
111
  if (status)
99
112
  detailParts.push(`HTTP ${status}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "1.20.6",
3
+ "version": "2.1.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -13,10 +13,10 @@
13
13
  "README.md"
14
14
  ],
15
15
  "dependencies": {
16
- "@neta-art/generation": "^0.1.9",
16
+ "@neta-art/generation": "^0.1.10",
17
17
  "commander": "^14.0.3",
18
18
  "sharp": "^0.34.5",
19
- "@neta-art/cohub": "1.34.0"
19
+ "@neta-art/cohub": "2.1.0"
20
20
  },
21
21
  "publishConfig": {
22
22
  "access": "public"