@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 +17 -1
- package/dist/commands/generations.js +52 -6
- package/dist/commands/models.js +3 -0
- package/dist/commands/run.js +3 -3
- package/dist/commands/space-commerce.d.ts +2 -0
- package/dist/commands/space-commerce.js +453 -0
- package/dist/commands/spaces.js +3 -0
- package/dist/commands/work-commerce.d.ts +2 -0
- package/dist/commands/work-commerce.js +185 -0
- package/dist/commands/works.js +79 -17
- package/dist/index.js +1 -0
- package/dist/output.js +14 -1
- package/package.json +3 -3
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
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
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({
|
package/dist/commands/models.js
CHANGED
|
@@ -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)
|
package/dist/commands/run.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/commands/spaces.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/commands/works.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
224
|
-
.option("--
|
|
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:
|
|
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, --
|
|
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
|
|
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: "
|
|
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
|
|
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.
|
|
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.
|
|
16
|
+
"@neta-art/generation": "^0.1.10",
|
|
17
17
|
"commander": "^14.0.3",
|
|
18
18
|
"sharp": "^0.34.5",
|
|
19
|
-
"@neta-art/cohub": "1.
|
|
19
|
+
"@neta-art/cohub": "2.1.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|