@clickraft/cli 0.11.2 → 0.13.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/CHANGELOG.md +34 -3
- package/README.md +53 -0
- package/dist/cli.js +467 -67
- package/dist/cli.js.map +1 -1
- package/dist/index.js +467 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,43 @@ var BrandModelSummarySchema = z2.object({
|
|
|
58
58
|
var BrandModelsListResponseSchema = z2.object({
|
|
59
59
|
brandModels: z2.array(BrandModelSummarySchema)
|
|
60
60
|
}).strict();
|
|
61
|
+
var ApiError = class extends Error {
|
|
62
|
+
code;
|
|
63
|
+
meta;
|
|
64
|
+
cause;
|
|
65
|
+
constructor(code, message, meta, cause) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "ApiError";
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.meta = meta;
|
|
70
|
+
this.cause = cause;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var MAX_ISSUES = 5;
|
|
74
|
+
function formatSchemaIssues(error) {
|
|
75
|
+
const issues = error.issues;
|
|
76
|
+
const shown = issues.slice(0, MAX_ISSUES).map(formatIssue);
|
|
77
|
+
const remaining = issues.length - shown.length;
|
|
78
|
+
const joined = shown.join("; ");
|
|
79
|
+
return remaining > 0 ? `${joined} (+${remaining} more)` : joined;
|
|
80
|
+
}
|
|
81
|
+
function formatIssue(issue) {
|
|
82
|
+
const path = formatPath(issue.path);
|
|
83
|
+
if (issue.code === "invalid_type") {
|
|
84
|
+
return `${path}: ${issue.code} (expected ${issue.expected}, received ${issue.received})`;
|
|
85
|
+
}
|
|
86
|
+
if (issue.code === "invalid_string") {
|
|
87
|
+
const validation = typeof issue.validation === "string" ? issue.validation : Object.keys(issue.validation)[0] ?? "string";
|
|
88
|
+
return `${path}: ${issue.code} (${validation})`;
|
|
89
|
+
}
|
|
90
|
+
return `${path}: ${issue.code}`;
|
|
91
|
+
}
|
|
92
|
+
function formatPath(path) {
|
|
93
|
+
if (path.length === 0) return "(root)";
|
|
94
|
+
return path.map(String).join(".");
|
|
95
|
+
}
|
|
96
|
+
var UUID_RE = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
|
|
97
|
+
var uuidString = () => z3.string().regex(UUID_RE, "Must be a UUID.");
|
|
61
98
|
var ProductImageSummarySchema = z3.object({
|
|
62
99
|
id: z3.string().uuid(),
|
|
63
100
|
url: z3.string(),
|
|
@@ -79,6 +116,65 @@ var ProductsListResponseSchema = z3.object({
|
|
|
79
116
|
items: z3.array(ProductSummarySchema),
|
|
80
117
|
nextCursor: z3.string().nullable()
|
|
81
118
|
});
|
|
119
|
+
var MAX_PRODUCT_IMAGES = 10;
|
|
120
|
+
var MAX_PRODUCT_CREATE_BODY_BYTES = 32 * 1024;
|
|
121
|
+
var ProductCreateImageSchema = z3.object({
|
|
122
|
+
/**
|
|
123
|
+
* An image `library_items.id`. Uploads via `POST /assets` are one source;
|
|
124
|
+
* rows of kind 'generation' are equally admissible.
|
|
125
|
+
*/
|
|
126
|
+
assetId: uuidString(),
|
|
127
|
+
altText: z3.string().trim().max(500).optional(),
|
|
128
|
+
isPrimary: z3.boolean().optional()
|
|
129
|
+
}).strict();
|
|
130
|
+
var ProductCreateRequestSchema = z3.object({
|
|
131
|
+
name: z3.string().trim().min(1).max(200),
|
|
132
|
+
description: z3.string().trim().max(5e3).optional(),
|
|
133
|
+
images: z3.array(ProductCreateImageSchema).min(1).max(MAX_PRODUCT_IMAGES)
|
|
134
|
+
}).strict().superRefine((v, ctx) => {
|
|
135
|
+
if (v.images.filter((i) => i.isPrimary).length > 1) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: z3.ZodIssueCode.custom,
|
|
138
|
+
path: ["images"],
|
|
139
|
+
message: "At most one image may be marked isPrimary."
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
function parseProductCreateRequest(input) {
|
|
144
|
+
const parsed = ProductCreateRequestSchema.safeParse(input);
|
|
145
|
+
if (!parsed.success) {
|
|
146
|
+
throw new ApiError(
|
|
147
|
+
"INPUT_INVALID_FORMAT",
|
|
148
|
+
`Invalid product create request: ${formatSchemaIssues(parsed.error)}`,
|
|
149
|
+
// `issues` is an array of { path, code, message }, which is the shape the
|
|
150
|
+
// MCP server's summarizeValidationDetails recognizes and renders.
|
|
151
|
+
{ retryable: false, details: parsed.error.issues }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const request2 = parsed.data;
|
|
155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
156
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const img of request2.images) {
|
|
158
|
+
if (seen.has(img.assetId)) duplicates.add(img.assetId);
|
|
159
|
+
seen.add(img.assetId);
|
|
160
|
+
}
|
|
161
|
+
if (duplicates.size > 0) {
|
|
162
|
+
throw new ApiError(
|
|
163
|
+
"INPUT_INVALID_FORMAT",
|
|
164
|
+
`The same assetId appears more than once in images: ${[...duplicates].join(", ")}.`,
|
|
165
|
+
{ retryable: false }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const bytes = Buffer.byteLength(JSON.stringify(request2), "utf8");
|
|
169
|
+
if (bytes > MAX_PRODUCT_CREATE_BODY_BYTES) {
|
|
170
|
+
throw new ApiError(
|
|
171
|
+
"INPUT_INVALID_FORMAT",
|
|
172
|
+
`Request body is ${bytes} bytes; the endpoint caps at ${MAX_PRODUCT_CREATE_BODY_BYTES}.`,
|
|
173
|
+
{ retryable: false }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return request2;
|
|
177
|
+
}
|
|
82
178
|
var TokenRowSchema = z4.object({
|
|
83
179
|
id: z4.string().uuid(),
|
|
84
180
|
organizationId: z4.string().uuid(),
|
|
@@ -318,21 +414,20 @@ var SERVER_ERROR_CODES = [
|
|
|
318
414
|
"PRODUCT_SYNC_ERROR",
|
|
319
415
|
"PRODUCT_DELETED_UPSTREAM",
|
|
320
416
|
"PRODUCT_NO_PRIMARY_IMAGE",
|
|
321
|
-
"PRODUCT_SOURCE_MANAGED"
|
|
417
|
+
"PRODUCT_SOURCE_MANAGED",
|
|
418
|
+
// Product create (`POST /api/agents/v1/products`). Without these five in the
|
|
419
|
+
// allow-list the HTTP client folds each one into INTERNAL_ERROR, which loses
|
|
420
|
+
// the server's message, the actionable `details.assetIds`, and the CLI exit
|
|
421
|
+
// code the failure should map to. STORAGE_UNAVAILABLE additionally loses its
|
|
422
|
+
// retryability, turning a transient 503 the server marked retryable into a
|
|
423
|
+
// one-shot failure.
|
|
424
|
+
"PRODUCT_IMAGE_ASSET_DUPLICATE",
|
|
425
|
+
"PRODUCT_IMAGE_ASSET_NOT_FOUND",
|
|
426
|
+
"PRODUCT_IMAGE_ASSET_NOT_IMAGE",
|
|
427
|
+
"PRODUCT_IMAGE_ASSET_INCOMPLETE",
|
|
428
|
+
"STORAGE_UNAVAILABLE"
|
|
322
429
|
];
|
|
323
430
|
var KNOWN_SERVER_CODES = new Set(SERVER_ERROR_CODES);
|
|
324
|
-
var ApiError = class extends Error {
|
|
325
|
-
code;
|
|
326
|
-
meta;
|
|
327
|
-
cause;
|
|
328
|
-
constructor(code, message, meta, cause) {
|
|
329
|
-
super(message);
|
|
330
|
-
this.name = "ApiError";
|
|
331
|
-
this.code = code;
|
|
332
|
-
this.meta = meta;
|
|
333
|
-
this.cause = cause;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
431
|
async function longPoll(opts) {
|
|
337
432
|
const now = opts.now ?? Date.now;
|
|
338
433
|
const started = now();
|
|
@@ -383,29 +478,6 @@ function composeIterationSignal(userSignal, remainingMs) {
|
|
|
383
478
|
const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
|
|
384
479
|
return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
|
|
385
480
|
}
|
|
386
|
-
var MAX_ISSUES = 5;
|
|
387
|
-
function formatSchemaIssues(error) {
|
|
388
|
-
const issues = error.issues;
|
|
389
|
-
const shown = issues.slice(0, MAX_ISSUES).map(formatIssue);
|
|
390
|
-
const remaining = issues.length - shown.length;
|
|
391
|
-
const joined = shown.join("; ");
|
|
392
|
-
return remaining > 0 ? `${joined} (+${remaining} more)` : joined;
|
|
393
|
-
}
|
|
394
|
-
function formatIssue(issue) {
|
|
395
|
-
const path = formatPath(issue.path);
|
|
396
|
-
if (issue.code === "invalid_type") {
|
|
397
|
-
return `${path}: ${issue.code} (expected ${issue.expected}, received ${issue.received})`;
|
|
398
|
-
}
|
|
399
|
-
if (issue.code === "invalid_string") {
|
|
400
|
-
const validation = typeof issue.validation === "string" ? issue.validation : Object.keys(issue.validation)[0] ?? "string";
|
|
401
|
-
return `${path}: ${issue.code} (${validation})`;
|
|
402
|
-
}
|
|
403
|
-
return `${path}: ${issue.code}`;
|
|
404
|
-
}
|
|
405
|
-
function formatPath(path) {
|
|
406
|
-
if (path.length === 0) return "(root)";
|
|
407
|
-
return path.map(String).join(".");
|
|
408
|
-
}
|
|
409
481
|
function generateIdempotencyKey() {
|
|
410
482
|
return randomUUID();
|
|
411
483
|
}
|
|
@@ -1413,6 +1485,55 @@ var EXIT_CODE_MAP = {
|
|
|
1413
1485
|
retryable: false,
|
|
1414
1486
|
hint: "Product is managed by an external integration."
|
|
1415
1487
|
},
|
|
1488
|
+
// --- 5 product create codes (`POST /products`) ------------------------------
|
|
1489
|
+
//
|
|
1490
|
+
// `lib/errors/app-error.ts` in the app repo annotates the four asset codes
|
|
1491
|
+
// "CLI exit 7". That is wrong and the app repo is being corrected: exit 7 is
|
|
1492
|
+
// the `conflict` category, reserved here for idempotency and canvas-rev
|
|
1493
|
+
// conflicts, and none of these are conflicts. Exit-code policy is CLI-side by
|
|
1494
|
+
// design (see the SDK's `errors/codes.ts` header), so the mapping below is the
|
|
1495
|
+
// authority.
|
|
1496
|
+
//
|
|
1497
|
+
// The three `usage` codes are all "the caller named something unusable", which
|
|
1498
|
+
// is exit 2 for MODEL_NOT_FOUND and WORKFLOW_NOT_FOUND already.
|
|
1499
|
+
PRODUCT_IMAGE_ASSET_DUPLICATE: {
|
|
1500
|
+
exitCode: 2,
|
|
1501
|
+
category: "usage",
|
|
1502
|
+
retryable: false,
|
|
1503
|
+
hint: "The same asset id appears more than once in the images list."
|
|
1504
|
+
},
|
|
1505
|
+
PRODUCT_IMAGE_ASSET_NOT_FOUND: {
|
|
1506
|
+
exitCode: 2,
|
|
1507
|
+
category: "usage",
|
|
1508
|
+
retryable: false,
|
|
1509
|
+
hint: "One or more asset ids do not exist in your organization. Upload them first, or check the ids."
|
|
1510
|
+
},
|
|
1511
|
+
PRODUCT_IMAGE_ASSET_NOT_IMAGE: {
|
|
1512
|
+
exitCode: 2,
|
|
1513
|
+
category: "usage",
|
|
1514
|
+
retryable: false,
|
|
1515
|
+
hint: "One or more referenced assets are not images."
|
|
1516
|
+
},
|
|
1517
|
+
// Deliberately NOT `usage`. The asset row exists and belongs to the caller;
|
|
1518
|
+
// its bytes never landed, which is an upload that half-completed on our side.
|
|
1519
|
+
// Retrying the create cannot help — the asset has to be uploaded again — so
|
|
1520
|
+
// this is runtime/non-retryable rather than a caller mistake.
|
|
1521
|
+
PRODUCT_IMAGE_ASSET_INCOMPLETE: {
|
|
1522
|
+
exitCode: 1,
|
|
1523
|
+
category: "runtime",
|
|
1524
|
+
retryable: false,
|
|
1525
|
+
hint: "An asset has no uploaded bytes. Re-run `clickraft upload` for it, then retry."
|
|
1526
|
+
},
|
|
1527
|
+
// `retryable: true` is load-bearing, not decorative: this table is the single
|
|
1528
|
+
// source of retryability for the SDK client via `isRetryableServerCode`, so
|
|
1529
|
+
// flipping it to false makes the CLI ignore a 503 the server explicitly marked
|
|
1530
|
+
// retryable and sent a Retry-After for.
|
|
1531
|
+
STORAGE_UNAVAILABLE: {
|
|
1532
|
+
exitCode: 6,
|
|
1533
|
+
category: "network",
|
|
1534
|
+
retryable: true,
|
|
1535
|
+
hint: "Object storage is temporarily unavailable. Retry with the same idempotency key."
|
|
1536
|
+
},
|
|
1416
1537
|
// --- 4 CLI-synthetic codes --------------------------------------------------
|
|
1417
1538
|
NETWORK_ERROR: {
|
|
1418
1539
|
exitCode: 6,
|
|
@@ -1831,6 +1952,7 @@ async function loadConfig(options = {}) {
|
|
|
1831
1952
|
let fileProfileName;
|
|
1832
1953
|
let fileAccessToken;
|
|
1833
1954
|
let fileApiBaseUrl;
|
|
1955
|
+
let fileScopes;
|
|
1834
1956
|
if (needsFile) {
|
|
1835
1957
|
const path = options.credentialsPath ?? getCredentialsPath();
|
|
1836
1958
|
const file = await readCredentials(path);
|
|
@@ -1841,6 +1963,7 @@ async function loadConfig(options = {}) {
|
|
|
1841
1963
|
if (profile) {
|
|
1842
1964
|
fileAccessToken = profile.accessToken;
|
|
1843
1965
|
fileApiBaseUrl = profile.apiBaseUrl;
|
|
1966
|
+
fileScopes = profile.scopes;
|
|
1844
1967
|
}
|
|
1845
1968
|
}
|
|
1846
1969
|
}
|
|
@@ -1848,7 +1971,12 @@ async function loadConfig(options = {}) {
|
|
|
1848
1971
|
accessToken: tokenFromFlagOrEnv ?? fileAccessToken ?? null,
|
|
1849
1972
|
profileName: profileName ?? fileProfileName ?? DEFAULT_PROFILE_NAME,
|
|
1850
1973
|
apiBaseUrl: apiBaseFromFlagOrEnv ?? fileApiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
1851
|
-
cloudflareAccess
|
|
1974
|
+
cloudflareAccess,
|
|
1975
|
+
// Scopes describe the FILE's token. If the flag/env token won the
|
|
1976
|
+
// precedence ladder above, the scopes we happen to have on disk belong to a
|
|
1977
|
+
// different credential, so report `null` rather than something plausible
|
|
1978
|
+
// and wrong.
|
|
1979
|
+
scopes: tokenFromFlagOrEnv ? null : fileScopes ?? null
|
|
1852
1980
|
};
|
|
1853
1981
|
}
|
|
1854
1982
|
function loadCloudflareAccess(env) {
|
|
@@ -2061,13 +2189,19 @@ async function generateCreate(opts) {
|
|
|
2061
2189
|
const preflightRefs = (opts.referenceImages ?? []).map(
|
|
2062
2190
|
(v) => isUrlLike(v) ? v : PREFLIGHT_PLACEHOLDER_URL
|
|
2063
2191
|
);
|
|
2064
|
-
|
|
2192
|
+
const preflightStartFrame = opts.startFrame === void 0 ? void 0 : isUrlLike(opts.startFrame) ? opts.startFrame : PREFLIGHT_PLACEHOLDER_URL;
|
|
2193
|
+
buildRequestBody({ ...opts, referenceImages: preflightRefs, startFrame: preflightStartFrame });
|
|
2065
2194
|
const referenceImages = await resolveReferenceImages({
|
|
2066
2195
|
items: opts.referenceImages ?? [],
|
|
2067
2196
|
client,
|
|
2068
2197
|
signal: opts.signal
|
|
2069
2198
|
});
|
|
2070
|
-
const
|
|
2199
|
+
const startFrame = opts.startFrame === void 0 ? void 0 : (await resolveReferenceImages({
|
|
2200
|
+
items: [opts.startFrame],
|
|
2201
|
+
client,
|
|
2202
|
+
signal: opts.signal
|
|
2203
|
+
}))[0];
|
|
2204
|
+
const body = buildRequestBody({ ...opts, referenceImages, startFrame });
|
|
2071
2205
|
return createGeneration({
|
|
2072
2206
|
client,
|
|
2073
2207
|
body,
|
|
@@ -2102,11 +2236,12 @@ async function resolveReferenceImages(args) {
|
|
|
2102
2236
|
function buildRequestBody(args) {
|
|
2103
2237
|
const input = {};
|
|
2104
2238
|
if (args.prompt !== void 0) input.prompt = args.prompt;
|
|
2105
|
-
if (args.referenceImages.length
|
|
2106
|
-
input.referenceImageUrl = args.referenceImages[0];
|
|
2107
|
-
} else if (args.referenceImages.length > 1) {
|
|
2239
|
+
if (args.referenceImages.length > 0) {
|
|
2108
2240
|
input.referenceImages = args.referenceImages.map((url) => ({ url }));
|
|
2109
2241
|
}
|
|
2242
|
+
if (args.startFrame !== void 0) {
|
|
2243
|
+
input.referenceImageUrl = args.startFrame;
|
|
2244
|
+
}
|
|
2110
2245
|
if (args.aspectRatio !== void 0) input.aspectRatio = args.aspectRatio;
|
|
2111
2246
|
if (args.resolution !== void 0) input.resolution = args.resolution;
|
|
2112
2247
|
if (args.durationSeconds !== void 0) input.durationSeconds = args.durationSeconds;
|
|
@@ -2279,13 +2414,13 @@ var VALID_POSES = [
|
|
|
2279
2414
|
"back",
|
|
2280
2415
|
"approved"
|
|
2281
2416
|
];
|
|
2282
|
-
var
|
|
2417
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2283
2418
|
function parseBrandModelSpec(raw) {
|
|
2284
2419
|
if (!raw) throw new ArgError("--brand-model value cannot be empty.");
|
|
2285
2420
|
const colonIdx = raw.indexOf(":");
|
|
2286
2421
|
const uuid = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
|
|
2287
2422
|
const poseRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
|
|
2288
|
-
if (!
|
|
2423
|
+
if (!UUID_RE2.test(uuid)) {
|
|
2289
2424
|
throw new ArgError(
|
|
2290
2425
|
`Invalid brand model ID: '${uuid}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
|
|
2291
2426
|
);
|
|
@@ -2321,13 +2456,13 @@ function validateBrandModelSpecs(specs) {
|
|
|
2321
2456
|
}
|
|
2322
2457
|
|
|
2323
2458
|
// src/commands/generate/parse-product-spec.ts
|
|
2324
|
-
var
|
|
2459
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2325
2460
|
function parseProductSpec(raw) {
|
|
2326
2461
|
if (!raw) throw new ArgError("--product value cannot be empty.");
|
|
2327
2462
|
const colonIdx = raw.indexOf(":");
|
|
2328
2463
|
const productId = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
|
|
2329
2464
|
const imageIdRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
|
|
2330
|
-
if (!
|
|
2465
|
+
if (!UUID_RE3.test(productId)) {
|
|
2331
2466
|
throw new ArgError(
|
|
2332
2467
|
`Invalid product ID: '${productId}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
|
|
2333
2468
|
);
|
|
@@ -2338,7 +2473,7 @@ function parseProductSpec(raw) {
|
|
|
2338
2473
|
`Empty image ID for product ${productId.toLowerCase()}. Omit the colon or provide a valid UUID.`
|
|
2339
2474
|
);
|
|
2340
2475
|
}
|
|
2341
|
-
if (!
|
|
2476
|
+
if (!UUID_RE3.test(imageIdRaw)) {
|
|
2342
2477
|
throw new ArgError(
|
|
2343
2478
|
`Invalid image ID: '${imageIdRaw}' for product ${productId.toLowerCase()}. Expected a UUID.`
|
|
2344
2479
|
);
|
|
@@ -2433,7 +2568,41 @@ async function runDeviceFlow(opts) {
|
|
|
2433
2568
|
}
|
|
2434
2569
|
}
|
|
2435
2570
|
async function requestDeviceAuthorization(opts) {
|
|
2436
|
-
|
|
2571
|
+
try {
|
|
2572
|
+
return await attemptDeviceAuthorization(opts, opts.scope);
|
|
2573
|
+
} catch (err) {
|
|
2574
|
+
if (!(err instanceof ScopeRefusedError)) throw err;
|
|
2575
|
+
if (!opts.allowScopeDowngrade || opts.requiredScope === void 0) {
|
|
2576
|
+
throw new AuthError("AUTH_TOKEN_INVALID", scopeRefusalMessage(err.description));
|
|
2577
|
+
}
|
|
2578
|
+
opts.onScopeDowngrade?.({
|
|
2579
|
+
requested: opts.scope,
|
|
2580
|
+
retryingWith: opts.requiredScope,
|
|
2581
|
+
description: err.description
|
|
2582
|
+
});
|
|
2583
|
+
try {
|
|
2584
|
+
return await attemptDeviceAuthorization(opts, opts.requiredScope);
|
|
2585
|
+
} catch (retryErr) {
|
|
2586
|
+
if (retryErr instanceof ScopeRefusedError) {
|
|
2587
|
+
throw new AuthError("AUTH_TOKEN_INVALID", scopeRefusalMessage(retryErr.description));
|
|
2588
|
+
}
|
|
2589
|
+
throw retryErr;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function scopeRefusalMessage(description) {
|
|
2594
|
+
return `The authorization server refused the requested scopes${description ? `: ${description}` : "."} Re-run \`clickraft login\` without --scope to request the defaults.`;
|
|
2595
|
+
}
|
|
2596
|
+
var ScopeRefusedError = class extends Error {
|
|
2597
|
+
constructor(description) {
|
|
2598
|
+
super("invalid_scope");
|
|
2599
|
+
this.description = description;
|
|
2600
|
+
this.name = "ScopeRefusedError";
|
|
2601
|
+
}
|
|
2602
|
+
description;
|
|
2603
|
+
};
|
|
2604
|
+
async function attemptDeviceAuthorization(opts, scope) {
|
|
2605
|
+
const body = new URLSearchParams({ client_id: opts.clientId, scope }).toString();
|
|
2437
2606
|
const headers = { "content-type": FORM_CONTENT_TYPE };
|
|
2438
2607
|
if (opts.cloudflareAccess) {
|
|
2439
2608
|
headers["cf-access-client-id"] = opts.cloudflareAccess.clientId;
|
|
@@ -2464,6 +2633,12 @@ async function requestDeviceAuthorization(opts) {
|
|
|
2464
2633
|
}
|
|
2465
2634
|
const text = await resp.body.text();
|
|
2466
2635
|
if (resp.statusCode !== 200) {
|
|
2636
|
+
if (resp.statusCode === 400) {
|
|
2637
|
+
const maybe = DeviceTokenPollErrorSchema.safeParse(safeParseJson(text));
|
|
2638
|
+
if (maybe.success && maybe.data.error === "invalid_scope") {
|
|
2639
|
+
throw new ScopeRefusedError(maybe.data.error_description);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2467
2642
|
const redacted = redactTokenLikeFields(truncate(text));
|
|
2468
2643
|
throw new AuthError(
|
|
2469
2644
|
"AUTH_TOKEN_INVALID",
|
|
@@ -2661,6 +2836,13 @@ function isAbortError2(err) {
|
|
|
2661
2836
|
}
|
|
2662
2837
|
return false;
|
|
2663
2838
|
}
|
|
2839
|
+
function safeParseJson(text) {
|
|
2840
|
+
try {
|
|
2841
|
+
return JSON.parse(text);
|
|
2842
|
+
} catch {
|
|
2843
|
+
return void 0;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2664
2846
|
function parseJson(text, label) {
|
|
2665
2847
|
try {
|
|
2666
2848
|
return JSON.parse(text);
|
|
@@ -2687,7 +2869,7 @@ function safeHeaders(headers) {
|
|
|
2687
2869
|
var PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000";
|
|
2688
2870
|
var PLACEHOLDER_ORG_SLUG = "unknown";
|
|
2689
2871
|
var CLIENT_ID = "clickraft-cli";
|
|
2690
|
-
var
|
|
2872
|
+
var REQUIRED_SCOPES = [
|
|
2691
2873
|
"templates:read",
|
|
2692
2874
|
"generations:write",
|
|
2693
2875
|
"generations:read",
|
|
@@ -2699,6 +2881,8 @@ var DEFAULT_SCOPES = [
|
|
|
2699
2881
|
"assets:write",
|
|
2700
2882
|
"balance:read"
|
|
2701
2883
|
];
|
|
2884
|
+
var OPTIONAL_SCOPES = ["products:write"];
|
|
2885
|
+
var DEFAULT_SCOPES = [...REQUIRED_SCOPES, ...OPTIONAL_SCOPES];
|
|
2702
2886
|
async function login(options) {
|
|
2703
2887
|
const config = await loadConfig({
|
|
2704
2888
|
flagProfile: options.profile,
|
|
@@ -2717,12 +2901,19 @@ async function login(options) {
|
|
|
2717
2901
|
);
|
|
2718
2902
|
}
|
|
2719
2903
|
}
|
|
2720
|
-
const
|
|
2904
|
+
const explicitScope = options.scope?.trim();
|
|
2905
|
+
const scope = explicitScope || DEFAULT_SCOPES.join(" ");
|
|
2721
2906
|
const flow = options.runDeviceFlow ?? runDeviceFlow;
|
|
2722
2907
|
const token = await flow({
|
|
2723
2908
|
apiBaseUrl,
|
|
2724
2909
|
clientId: CLIENT_ID,
|
|
2725
2910
|
scope,
|
|
2911
|
+
requiredScope: REQUIRED_SCOPES.join(" "),
|
|
2912
|
+
// ONLY when the list is ours. An explicit --scope is a wholesale override,
|
|
2913
|
+
// and quietly requesting a different set than the caller asked for would
|
|
2914
|
+
// hand them a token whose powers they never chose and cannot see.
|
|
2915
|
+
allowScopeDowngrade: !explicitScope,
|
|
2916
|
+
onScopeDowngrade: options.onScopeDowngrade,
|
|
2726
2917
|
onUserCode: options.onUserCode,
|
|
2727
2918
|
onPoll: options.onPoll,
|
|
2728
2919
|
signal: options.signal,
|
|
@@ -2944,6 +3135,71 @@ async function listNodes(opts) {
|
|
|
2944
3135
|
return data;
|
|
2945
3136
|
}
|
|
2946
3137
|
|
|
3138
|
+
// src/auth/scopes.ts
|
|
3139
|
+
var PRODUCTS_WRITE_SCOPE = "products:write";
|
|
3140
|
+
function assertScope(config, scope, hint) {
|
|
3141
|
+
if (config.scopes === null) return;
|
|
3142
|
+
if (config.scopes.includes(scope)) return;
|
|
3143
|
+
throw new ApiError("AUTH_TOKEN_SCOPE_INSUFFICIENT", hint, { retryable: false, hint });
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// src/commands/product/create.ts
|
|
3147
|
+
var IDEMPOTENCY_KEY_MAX = 255;
|
|
3148
|
+
async function createProduct(options) {
|
|
3149
|
+
if (options.idempotencyKey !== void 0) {
|
|
3150
|
+
const len = options.idempotencyKey.length;
|
|
3151
|
+
if (len < 1 || len > IDEMPOTENCY_KEY_MAX) {
|
|
3152
|
+
throw new ArgError(`--idempotency-key must be 1..${IDEMPOTENCY_KEY_MAX} characters.`);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const config = await loadConfig({
|
|
3156
|
+
flagProfile: options.profile,
|
|
3157
|
+
flagApiBaseUrl: options.apiBaseUrl,
|
|
3158
|
+
flagToken: options.token,
|
|
3159
|
+
credentialsPath: options.credentialsPath
|
|
3160
|
+
});
|
|
3161
|
+
if (!config.accessToken) {
|
|
3162
|
+
throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
|
|
3163
|
+
}
|
|
3164
|
+
assertScope(config, PRODUCTS_WRITE_SCOPE, options.scopeHint);
|
|
3165
|
+
const body = buildRequestBody2(options);
|
|
3166
|
+
const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
|
|
3167
|
+
apiBaseUrl,
|
|
3168
|
+
accessToken,
|
|
3169
|
+
signal: options.signal,
|
|
3170
|
+
cloudflareAccess: config.cloudflareAccess
|
|
3171
|
+
}));
|
|
3172
|
+
const client = factory(config.apiBaseUrl, config.accessToken);
|
|
3173
|
+
return client.request({
|
|
3174
|
+
method: "POST",
|
|
3175
|
+
path: "/products",
|
|
3176
|
+
body,
|
|
3177
|
+
idempotencyKey: options.idempotencyKey,
|
|
3178
|
+
responseSchema: ProductSummarySchema,
|
|
3179
|
+
signal: options.signal
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
function buildRequestBody2(options) {
|
|
3183
|
+
if (options.images.length === 0) {
|
|
3184
|
+
throw new ArgError("At least one --image is required.");
|
|
3185
|
+
}
|
|
3186
|
+
if (options.primary !== void 0) {
|
|
3187
|
+
const known = options.images.some((img) => img.assetId === options.primary);
|
|
3188
|
+
if (!known) {
|
|
3189
|
+
throw new ArgError(`--primary ${options.primary} is not among the --image assets provided.`);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
const images = options.images.map((img) => {
|
|
3193
|
+
const entry = { assetId: img.assetId };
|
|
3194
|
+
if (img.altText !== void 0) entry.altText = img.altText;
|
|
3195
|
+
if (options.primary === img.assetId) entry.isPrimary = true;
|
|
3196
|
+
return entry;
|
|
3197
|
+
});
|
|
3198
|
+
const raw = { name: options.name, images };
|
|
3199
|
+
if (options.description !== void 0) raw.description = options.description;
|
|
3200
|
+
return parseProductCreateRequest(raw);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
2947
3203
|
// src/commands/product/list.ts
|
|
2948
3204
|
import "zod";
|
|
2949
3205
|
var DEFAULT_LIMIT = 50;
|
|
@@ -2985,6 +3241,23 @@ async function listProducts(options) {
|
|
|
2985
3241
|
};
|
|
2986
3242
|
}
|
|
2987
3243
|
|
|
3244
|
+
// src/commands/product/parse-image-spec.ts
|
|
3245
|
+
function parseImageSpec(spec) {
|
|
3246
|
+
const trimmed = spec.trim();
|
|
3247
|
+
if (trimmed.length === 0) {
|
|
3248
|
+
throw new ArgError("--image requires a value of the form <assetId>[:<altText>].");
|
|
3249
|
+
}
|
|
3250
|
+
const colon = trimmed.indexOf(":");
|
|
3251
|
+
const assetId = (colon === -1 ? trimmed : trimmed.slice(0, colon)).trim();
|
|
3252
|
+
const rawAlt = colon === -1 ? void 0 : trimmed.slice(colon + 1).trim();
|
|
3253
|
+
if (!UUID_RE.test(assetId)) {
|
|
3254
|
+
throw new ArgError(
|
|
3255
|
+
`--image expects an asset id (UUID) before the colon, got "${assetId}". Use the assetId returned by \`clickraft upload\`.`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return rawAlt ? { assetId, altText: rawAlt } : { assetId };
|
|
3259
|
+
}
|
|
3260
|
+
|
|
2988
3261
|
// src/telemetry/detect.ts
|
|
2989
3262
|
var AGENT_SIGNALS = [
|
|
2990
3263
|
{ envVar: "CLAUDE_CODE_SESSION_ID", agent: "claude-code" },
|
|
@@ -3558,6 +3831,21 @@ function renderProductsList(result, out) {
|
|
|
3558
3831
|
out.write("\nMore results available. Use --cursor to fetch the next page.\n");
|
|
3559
3832
|
}
|
|
3560
3833
|
}
|
|
3834
|
+
function renderProductCreateResult(product, out) {
|
|
3835
|
+
const lines = [
|
|
3836
|
+
`Created product "${product.title}"`,
|
|
3837
|
+
` ID: ${product.id}`,
|
|
3838
|
+
` Source: ${product.sourceType}`,
|
|
3839
|
+
` Images: ${product.images.length}`
|
|
3840
|
+
];
|
|
3841
|
+
for (const img of product.images) {
|
|
3842
|
+
lines.push(
|
|
3843
|
+
` ${img.isPrimary ? "*" : "-"} ${img.id}${img.altText ? ` ${img.altText}` : ""}`
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
lines.push("", `Use it with: clickraft generate create --product ${product.id} ...`);
|
|
3847
|
+
out.write(lines.join("\n") + "\n");
|
|
3848
|
+
}
|
|
3561
3849
|
function renderTemplatesList(result, out) {
|
|
3562
3850
|
const { templates } = result;
|
|
3563
3851
|
if (templates.length === 0) {
|
|
@@ -3936,6 +4224,18 @@ async function loginCommand(parsed, rc) {
|
|
|
3936
4224
|
},
|
|
3937
4225
|
onPoll: () => {
|
|
3938
4226
|
rc.uiHooks?.onPoll?.();
|
|
4227
|
+
},
|
|
4228
|
+
// Always warn, including in --json mode, and always on stderr: the login
|
|
4229
|
+
// succeeded, so the JSON envelope on stdout stays a clean success, but the
|
|
4230
|
+
// token is quietly less capable than the one that was asked for. Silence
|
|
4231
|
+
// here is how a user ends up debugging a scope error days later.
|
|
4232
|
+
onScopeDowngrade: (info) => {
|
|
4233
|
+
rc.stderr.write(
|
|
4234
|
+
`Warning: the server refused one or more requested scopes${info.description ? ` (${info.description})` : ""}.
|
|
4235
|
+
Retrying with the required scopes only. This token will not be able to use commands that need the refused scopes.
|
|
4236
|
+
Re-run \`clickraft login --force-reauth\` once the server grants them.
|
|
4237
|
+
`
|
|
4238
|
+
);
|
|
3939
4239
|
}
|
|
3940
4240
|
});
|
|
3941
4241
|
} finally {
|
|
@@ -4022,6 +4322,7 @@ function renderRootHelp(version) {
|
|
|
4022
4322
|
" balance Show credit balance and rate limits",
|
|
4023
4323
|
" brand-model list List available brand models",
|
|
4024
4324
|
" product list List products in your organization",
|
|
4325
|
+
" product create Create a product from uploaded assets",
|
|
4025
4326
|
" models list List available AI models",
|
|
4026
4327
|
" nodes list List available workflow node types",
|
|
4027
4328
|
" nodes describe Describe a node type's ports and fields",
|
|
@@ -4113,7 +4414,8 @@ function renderCommandHelp(command) {
|
|
|
4113
4414
|
"Usage: clickraft product <subcommand> [options]",
|
|
4114
4415
|
"",
|
|
4115
4416
|
"Subcommands:",
|
|
4116
|
-
" list
|
|
4417
|
+
" list List products in your organization",
|
|
4418
|
+
" create Create a product from uploaded assets",
|
|
4117
4419
|
""
|
|
4118
4420
|
].join("\n");
|
|
4119
4421
|
case "product list":
|
|
@@ -4134,6 +4436,34 @@ function renderCommandHelp(command) {
|
|
|
4134
4436
|
" --json",
|
|
4135
4437
|
""
|
|
4136
4438
|
].join("\n");
|
|
4439
|
+
case "product create":
|
|
4440
|
+
return [
|
|
4441
|
+
"Usage: clickraft product create --name <name> --image <assetId>[:<altText>] [options]",
|
|
4442
|
+
"",
|
|
4443
|
+
"Create a product from images already uploaded to your Clickraft library.",
|
|
4444
|
+
"Upload them first with `clickraft upload`, which prints the assetId to use here.",
|
|
4445
|
+
"",
|
|
4446
|
+
"Options:",
|
|
4447
|
+
" --name <name> Product name (required, max 200 chars)",
|
|
4448
|
+
" --image <spec> <assetId>[:<altText>] (repeatable, 1-10 images, required)",
|
|
4449
|
+
" --description <text> Product description (max 5000 chars)",
|
|
4450
|
+
" --primary <assetId> Mark this image primary (default: the first --image)",
|
|
4451
|
+
" --idempotency-key <key> Reuse a key to make a retry safe across invocations",
|
|
4452
|
+
" --profile <name>",
|
|
4453
|
+
" --api-base-url <url>",
|
|
4454
|
+
" --token <token>",
|
|
4455
|
+
" --json",
|
|
4456
|
+
"",
|
|
4457
|
+
"Examples:",
|
|
4458
|
+
' clickraft product create --name "Ceramic Mug" \\',
|
|
4459
|
+
" --image 1f7c1e8e-2a4b-4c9d-8e5f-3b2a1c0d9e8f",
|
|
4460
|
+
"",
|
|
4461
|
+
' clickraft product create --name "Ceramic Mug" \\',
|
|
4462
|
+
' --image "1f7c1e8e-2a4b-4c9d-8e5f-3b2a1c0d9e8f:Front view" \\',
|
|
4463
|
+
' --image "2a8d2f9f-3b5c-4d0e-9f6a-4c3b2d1e0f9a:Side view" \\',
|
|
4464
|
+
" --primary 2a8d2f9f-3b5c-4d0e-9f6a-4c3b2d1e0f9a",
|
|
4465
|
+
""
|
|
4466
|
+
].join("\n");
|
|
4137
4467
|
case "models":
|
|
4138
4468
|
return [
|
|
4139
4469
|
"Usage: clickraft models <subcommand> [options]",
|
|
@@ -4292,7 +4622,8 @@ function renderCommandHelp(command) {
|
|
|
4292
4622
|
" --product <spec> Product reference (repeatable)",
|
|
4293
4623
|
" Format: <uuid> or <uuid>:<imageId>",
|
|
4294
4624
|
" Server enforces reference cap per model",
|
|
4295
|
-
" --reference-image <v> URL or local path (repeatable, max 8)",
|
|
4625
|
+
" --reference-image <v> Reference image: URL or local path (repeatable, max 8)",
|
|
4626
|
+
" --start-frame <v> Image-to-video start frame: URL or local path",
|
|
4296
4627
|
" --aspect-ratio <a:b> e.g. 16:9",
|
|
4297
4628
|
" --resolution <size> e.g. 1024x768, 720p, 1K",
|
|
4298
4629
|
" --duration-seconds <n> For video/audio models (1-60)",
|
|
@@ -4480,6 +4811,7 @@ function matchNewCommand(argv) {
|
|
|
4480
4811
|
if (verb === "product") {
|
|
4481
4812
|
const sub = argv[1];
|
|
4482
4813
|
if (sub === "list") return { kind: "cmd", path: "product.list", verbTokenCount: 2 };
|
|
4814
|
+
if (sub === "create") return { kind: "cmd", path: "product.create", verbTokenCount: 2 };
|
|
4483
4815
|
if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "product" };
|
|
4484
4816
|
return { kind: "unknown-sub", verb: "product", sub };
|
|
4485
4817
|
}
|
|
@@ -4563,6 +4895,8 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
|
|
|
4563
4895
|
return await runTemplateFind(argvAfterVerb, ctx, rc);
|
|
4564
4896
|
case "product.list":
|
|
4565
4897
|
return await runProductList(argvAfterVerb, ctx, rc);
|
|
4898
|
+
case "product.create":
|
|
4899
|
+
return await runProductCreate(argvAfterVerb, ctx, rc);
|
|
4566
4900
|
case "models.list":
|
|
4567
4901
|
return await runModelsList(argvAfterVerb, ctx, rc);
|
|
4568
4902
|
case "nodes.list":
|
|
@@ -4607,6 +4941,7 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4607
4941
|
...COMMON_RICH_STRING_FLAGS,
|
|
4608
4942
|
"prompt",
|
|
4609
4943
|
"model-slug",
|
|
4944
|
+
"start-frame",
|
|
4610
4945
|
"aspect-ratio",
|
|
4611
4946
|
"resolution",
|
|
4612
4947
|
"duration-seconds",
|
|
@@ -4629,6 +4964,7 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4629
4964
|
prompt: parsed.string.prompt ?? parsed.positional[0],
|
|
4630
4965
|
modelSlug,
|
|
4631
4966
|
referenceImages: parsed.array["reference-image"],
|
|
4967
|
+
startFrame: parsed.string["start-frame"],
|
|
4632
4968
|
aspectRatio: parsed.string["aspect-ratio"],
|
|
4633
4969
|
resolution: parsed.string.resolution,
|
|
4634
4970
|
durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
|
|
@@ -4865,7 +5201,20 @@ function emitTemplateDetail(detail, ctx, rc) {
|
|
|
4865
5201
|
}
|
|
4866
5202
|
renderTemplateDetail(detail, rc.stdout);
|
|
4867
5203
|
}
|
|
4868
|
-
|
|
5204
|
+
function scopeHintFor(scope) {
|
|
5205
|
+
return `Your CLI token doesn't include ${scope}. Run \`clickraft login --force-reauth\` to re-authenticate with the new scope.`;
|
|
5206
|
+
}
|
|
5207
|
+
async function withScopeHint(scope, fn) {
|
|
5208
|
+
try {
|
|
5209
|
+
return await fn();
|
|
5210
|
+
} catch (err) {
|
|
5211
|
+
if (err instanceof ApiError && err.code === "AUTH_TOKEN_SCOPE_INSUFFICIENT") {
|
|
5212
|
+
const hint = scopeHintFor(scope);
|
|
5213
|
+
throw new ApiError(err.code, hint, { ...err.meta, hint }, err.cause);
|
|
5214
|
+
}
|
|
5215
|
+
throw err;
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
4869
5218
|
async function runProductList(argv, ctx, rc) {
|
|
4870
5219
|
const parsed = parseArgs(argv, {
|
|
4871
5220
|
string: [
|
|
@@ -4882,8 +5231,9 @@ async function runProductList(argv, ctx, rc) {
|
|
|
4882
5231
|
if (limit !== void 0 && (limit < 1 || limit > 100)) {
|
|
4883
5232
|
throw new ArgError("--limit must be between 1 and 100.");
|
|
4884
5233
|
}
|
|
4885
|
-
|
|
4886
|
-
|
|
5234
|
+
const result = await withScopeHint(
|
|
5235
|
+
"products:read",
|
|
5236
|
+
() => listProducts({
|
|
4887
5237
|
cursor: parsed.string.cursor,
|
|
4888
5238
|
limit,
|
|
4889
5239
|
syncStatus: parsed.string["sync-status"],
|
|
@@ -4893,21 +5243,71 @@ async function runProductList(argv, ctx, rc) {
|
|
|
4893
5243
|
apiBaseUrl: parsed.string["api-base-url"],
|
|
4894
5244
|
token: parsed.string.token,
|
|
4895
5245
|
signal: rc.signal
|
|
4896
|
-
})
|
|
4897
|
-
|
|
4898
|
-
|
|
5246
|
+
})
|
|
5247
|
+
);
|
|
5248
|
+
ctx.nextCursor = result.nextCursor ?? void 0;
|
|
5249
|
+
emitProductList(result, ctx, rc);
|
|
5250
|
+
return 0;
|
|
5251
|
+
}
|
|
5252
|
+
var PRODUCT_CREATE_NOT_ENABLED = "Product creation is not enabled on this deployment yet. The endpoint answers 404 while the feature flag is off.";
|
|
5253
|
+
function remapProductCreateError(err) {
|
|
5254
|
+
if (!(err instanceof ApiError)) return err;
|
|
5255
|
+
if (err.code === "NOT_FOUND") {
|
|
5256
|
+
return new ApiError(
|
|
5257
|
+
err.code,
|
|
5258
|
+
PRODUCT_CREATE_NOT_ENABLED,
|
|
5259
|
+
{ ...err.meta, hint: PRODUCT_CREATE_NOT_ENABLED },
|
|
5260
|
+
err.cause
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
if (err.code === "IDEMPOTENCY_KEY_CONFLICT") {
|
|
5264
|
+
const hint = /different token/i.test(err.message) ? "This idempotency key was used by a different token. Use a key of your own." : "This idempotency key was already used with a different request body. Use a new key.";
|
|
5265
|
+
return new ApiError(err.code, err.message, { ...err.meta, hint }, err.cause);
|
|
5266
|
+
}
|
|
5267
|
+
return err;
|
|
5268
|
+
}
|
|
5269
|
+
async function runProductCreate(argv, ctx, rc) {
|
|
5270
|
+
const parsed = parseArgs(argv, {
|
|
5271
|
+
string: [...COMMON_RICH_STRING_FLAGS, "name", "description", "primary", "idempotency-key"],
|
|
5272
|
+
boolean: [...COMMON_RICH_BOOL_FLAGS],
|
|
5273
|
+
array: ["image"]
|
|
5274
|
+
});
|
|
5275
|
+
const name = parsed.string.name;
|
|
5276
|
+
if (!name) throw new ArgError("--name is required.");
|
|
5277
|
+
const rawImages = parsed.array["image"] ?? [];
|
|
5278
|
+
if (rawImages.length === 0) throw new ArgError("At least one --image is required.");
|
|
5279
|
+
const images = rawImages.map(parseImageSpec);
|
|
5280
|
+
try {
|
|
5281
|
+
const product = await withScopeHint(
|
|
5282
|
+
"products:write",
|
|
5283
|
+
() => createProduct({
|
|
5284
|
+
name,
|
|
5285
|
+
description: parsed.string.description,
|
|
5286
|
+
images,
|
|
5287
|
+
primary: parsed.string.primary,
|
|
5288
|
+
idempotencyKey: parsed.string["idempotency-key"],
|
|
5289
|
+
profile: parsed.string.profile,
|
|
5290
|
+
apiBaseUrl: parsed.string["api-base-url"],
|
|
5291
|
+
token: parsed.string.token,
|
|
5292
|
+
signal: rc.signal,
|
|
5293
|
+
scopeHint: scopeHintFor("products:write")
|
|
5294
|
+
})
|
|
5295
|
+
);
|
|
5296
|
+
emitProductCreate(product, ctx, rc);
|
|
4899
5297
|
return 0;
|
|
4900
5298
|
} catch (err) {
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
5299
|
+
throw remapProductCreateError(err);
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
function emitProductCreate(product, ctx, rc) {
|
|
5303
|
+
if (rc.signal?.aborted) {
|
|
5304
|
+
throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
|
|
5305
|
+
}
|
|
5306
|
+
if (rc.jsonMode) {
|
|
5307
|
+
rc.stdout.write(JSON.stringify(wrapSuccess(product, ctx)) + "\n");
|
|
5308
|
+
return;
|
|
4910
5309
|
}
|
|
5310
|
+
renderProductCreateResult(product, rc.stdout);
|
|
4911
5311
|
}
|
|
4912
5312
|
function emitProductList(result, ctx, rc) {
|
|
4913
5313
|
if (rc.signal?.aborted) {
|