@clickraft/cli 0.12.0 → 0.14.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 +4 -9
- package/README.md +63 -0
- package/dist/cli.js +459 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.js +459 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.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(),
|
|
@@ -139,6 +235,10 @@ var GenerateCreateRequestSchema = z6.object({
|
|
|
139
235
|
"resolution must match WxH (e.g. 1280x720), p-token (e.g. 720p), or K-token (e.g. 1K)"
|
|
140
236
|
).optional(),
|
|
141
237
|
durationSeconds: z6.number().int().min(1).max(60).optional(),
|
|
238
|
+
// The model's quality tier (e.g. 'low' | 'medium' | 'high'). The valid set is
|
|
239
|
+
// per model, so it is checked server-side: an unoffered value returns 400
|
|
240
|
+
// INPUT_INVALID_FORMAT with the supported options in `details.allowed`.
|
|
241
|
+
quality: z6.string().min(1).max(40).optional(),
|
|
142
242
|
width: z6.number().int().min(64).max(8192).optional(),
|
|
143
243
|
height: z6.number().int().min(64).max(8192).optional(),
|
|
144
244
|
providerParams: z6.record(z6.string(), z6.unknown()).optional()
|
|
@@ -318,21 +418,20 @@ var SERVER_ERROR_CODES = [
|
|
|
318
418
|
"PRODUCT_SYNC_ERROR",
|
|
319
419
|
"PRODUCT_DELETED_UPSTREAM",
|
|
320
420
|
"PRODUCT_NO_PRIMARY_IMAGE",
|
|
321
|
-
"PRODUCT_SOURCE_MANAGED"
|
|
421
|
+
"PRODUCT_SOURCE_MANAGED",
|
|
422
|
+
// Product create (`POST /api/agents/v1/products`). Without these five in the
|
|
423
|
+
// allow-list the HTTP client folds each one into INTERNAL_ERROR, which loses
|
|
424
|
+
// the server's message, the actionable `details.assetIds`, and the CLI exit
|
|
425
|
+
// code the failure should map to. STORAGE_UNAVAILABLE additionally loses its
|
|
426
|
+
// retryability, turning a transient 503 the server marked retryable into a
|
|
427
|
+
// one-shot failure.
|
|
428
|
+
"PRODUCT_IMAGE_ASSET_DUPLICATE",
|
|
429
|
+
"PRODUCT_IMAGE_ASSET_NOT_FOUND",
|
|
430
|
+
"PRODUCT_IMAGE_ASSET_NOT_IMAGE",
|
|
431
|
+
"PRODUCT_IMAGE_ASSET_INCOMPLETE",
|
|
432
|
+
"STORAGE_UNAVAILABLE"
|
|
322
433
|
];
|
|
323
434
|
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
435
|
async function longPoll(opts) {
|
|
337
436
|
const now = opts.now ?? Date.now;
|
|
338
437
|
const started = now();
|
|
@@ -383,29 +482,6 @@ function composeIterationSignal(userSignal, remainingMs) {
|
|
|
383
482
|
const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
|
|
384
483
|
return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
|
|
385
484
|
}
|
|
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
485
|
function generateIdempotencyKey() {
|
|
410
486
|
return randomUUID();
|
|
411
487
|
}
|
|
@@ -1413,6 +1489,55 @@ var EXIT_CODE_MAP = {
|
|
|
1413
1489
|
retryable: false,
|
|
1414
1490
|
hint: "Product is managed by an external integration."
|
|
1415
1491
|
},
|
|
1492
|
+
// --- 5 product create codes (`POST /products`) ------------------------------
|
|
1493
|
+
//
|
|
1494
|
+
// `lib/errors/app-error.ts` in the app repo annotates the four asset codes
|
|
1495
|
+
// "CLI exit 7". That is wrong and the app repo is being corrected: exit 7 is
|
|
1496
|
+
// the `conflict` category, reserved here for idempotency and canvas-rev
|
|
1497
|
+
// conflicts, and none of these are conflicts. Exit-code policy is CLI-side by
|
|
1498
|
+
// design (see the SDK's `errors/codes.ts` header), so the mapping below is the
|
|
1499
|
+
// authority.
|
|
1500
|
+
//
|
|
1501
|
+
// The three `usage` codes are all "the caller named something unusable", which
|
|
1502
|
+
// is exit 2 for MODEL_NOT_FOUND and WORKFLOW_NOT_FOUND already.
|
|
1503
|
+
PRODUCT_IMAGE_ASSET_DUPLICATE: {
|
|
1504
|
+
exitCode: 2,
|
|
1505
|
+
category: "usage",
|
|
1506
|
+
retryable: false,
|
|
1507
|
+
hint: "The same asset id appears more than once in the images list."
|
|
1508
|
+
},
|
|
1509
|
+
PRODUCT_IMAGE_ASSET_NOT_FOUND: {
|
|
1510
|
+
exitCode: 2,
|
|
1511
|
+
category: "usage",
|
|
1512
|
+
retryable: false,
|
|
1513
|
+
hint: "One or more asset ids do not exist in your organization. Upload them first, or check the ids."
|
|
1514
|
+
},
|
|
1515
|
+
PRODUCT_IMAGE_ASSET_NOT_IMAGE: {
|
|
1516
|
+
exitCode: 2,
|
|
1517
|
+
category: "usage",
|
|
1518
|
+
retryable: false,
|
|
1519
|
+
hint: "One or more referenced assets are not images."
|
|
1520
|
+
},
|
|
1521
|
+
// Deliberately NOT `usage`. The asset row exists and belongs to the caller;
|
|
1522
|
+
// its bytes never landed, which is an upload that half-completed on our side.
|
|
1523
|
+
// Retrying the create cannot help — the asset has to be uploaded again — so
|
|
1524
|
+
// this is runtime/non-retryable rather than a caller mistake.
|
|
1525
|
+
PRODUCT_IMAGE_ASSET_INCOMPLETE: {
|
|
1526
|
+
exitCode: 1,
|
|
1527
|
+
category: "runtime",
|
|
1528
|
+
retryable: false,
|
|
1529
|
+
hint: "An asset has no uploaded bytes. Re-run `clickraft upload` for it, then retry."
|
|
1530
|
+
},
|
|
1531
|
+
// `retryable: true` is load-bearing, not decorative: this table is the single
|
|
1532
|
+
// source of retryability for the SDK client via `isRetryableServerCode`, so
|
|
1533
|
+
// flipping it to false makes the CLI ignore a 503 the server explicitly marked
|
|
1534
|
+
// retryable and sent a Retry-After for.
|
|
1535
|
+
STORAGE_UNAVAILABLE: {
|
|
1536
|
+
exitCode: 6,
|
|
1537
|
+
category: "network",
|
|
1538
|
+
retryable: true,
|
|
1539
|
+
hint: "Object storage is temporarily unavailable. Retry with the same idempotency key."
|
|
1540
|
+
},
|
|
1416
1541
|
// --- 4 CLI-synthetic codes --------------------------------------------------
|
|
1417
1542
|
NETWORK_ERROR: {
|
|
1418
1543
|
exitCode: 6,
|
|
@@ -1831,6 +1956,7 @@ async function loadConfig(options = {}) {
|
|
|
1831
1956
|
let fileProfileName;
|
|
1832
1957
|
let fileAccessToken;
|
|
1833
1958
|
let fileApiBaseUrl;
|
|
1959
|
+
let fileScopes;
|
|
1834
1960
|
if (needsFile) {
|
|
1835
1961
|
const path = options.credentialsPath ?? getCredentialsPath();
|
|
1836
1962
|
const file = await readCredentials(path);
|
|
@@ -1841,6 +1967,7 @@ async function loadConfig(options = {}) {
|
|
|
1841
1967
|
if (profile) {
|
|
1842
1968
|
fileAccessToken = profile.accessToken;
|
|
1843
1969
|
fileApiBaseUrl = profile.apiBaseUrl;
|
|
1970
|
+
fileScopes = profile.scopes;
|
|
1844
1971
|
}
|
|
1845
1972
|
}
|
|
1846
1973
|
}
|
|
@@ -1848,7 +1975,12 @@ async function loadConfig(options = {}) {
|
|
|
1848
1975
|
accessToken: tokenFromFlagOrEnv ?? fileAccessToken ?? null,
|
|
1849
1976
|
profileName: profileName ?? fileProfileName ?? DEFAULT_PROFILE_NAME,
|
|
1850
1977
|
apiBaseUrl: apiBaseFromFlagOrEnv ?? fileApiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
1851
|
-
cloudflareAccess
|
|
1978
|
+
cloudflareAccess,
|
|
1979
|
+
// Scopes describe the FILE's token. If the flag/env token won the
|
|
1980
|
+
// precedence ladder above, the scopes we happen to have on disk belong to a
|
|
1981
|
+
// different credential, so report `null` rather than something plausible
|
|
1982
|
+
// and wrong.
|
|
1983
|
+
scopes: tokenFromFlagOrEnv ? null : fileScopes ?? null
|
|
1852
1984
|
};
|
|
1853
1985
|
}
|
|
1854
1986
|
function loadCloudflareAccess(env) {
|
|
@@ -2117,6 +2249,7 @@ function buildRequestBody(args) {
|
|
|
2117
2249
|
if (args.aspectRatio !== void 0) input.aspectRatio = args.aspectRatio;
|
|
2118
2250
|
if (args.resolution !== void 0) input.resolution = args.resolution;
|
|
2119
2251
|
if (args.durationSeconds !== void 0) input.durationSeconds = args.durationSeconds;
|
|
2252
|
+
if (args.quality !== void 0) input.quality = args.quality;
|
|
2120
2253
|
if (args.brandModels !== void 0 && args.brandModels.length > 0) {
|
|
2121
2254
|
input.providerParams = {
|
|
2122
2255
|
brandModels: args.brandModels.map((spec) => {
|
|
@@ -2286,13 +2419,13 @@ var VALID_POSES = [
|
|
|
2286
2419
|
"back",
|
|
2287
2420
|
"approved"
|
|
2288
2421
|
];
|
|
2289
|
-
var
|
|
2422
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2290
2423
|
function parseBrandModelSpec(raw) {
|
|
2291
2424
|
if (!raw) throw new ArgError("--brand-model value cannot be empty.");
|
|
2292
2425
|
const colonIdx = raw.indexOf(":");
|
|
2293
2426
|
const uuid = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
|
|
2294
2427
|
const poseRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
|
|
2295
|
-
if (!
|
|
2428
|
+
if (!UUID_RE2.test(uuid)) {
|
|
2296
2429
|
throw new ArgError(
|
|
2297
2430
|
`Invalid brand model ID: '${uuid}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
|
|
2298
2431
|
);
|
|
@@ -2328,13 +2461,13 @@ function validateBrandModelSpecs(specs) {
|
|
|
2328
2461
|
}
|
|
2329
2462
|
|
|
2330
2463
|
// src/commands/generate/parse-product-spec.ts
|
|
2331
|
-
var
|
|
2464
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2332
2465
|
function parseProductSpec(raw) {
|
|
2333
2466
|
if (!raw) throw new ArgError("--product value cannot be empty.");
|
|
2334
2467
|
const colonIdx = raw.indexOf(":");
|
|
2335
2468
|
const productId = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
|
|
2336
2469
|
const imageIdRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
|
|
2337
|
-
if (!
|
|
2470
|
+
if (!UUID_RE3.test(productId)) {
|
|
2338
2471
|
throw new ArgError(
|
|
2339
2472
|
`Invalid product ID: '${productId}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
|
|
2340
2473
|
);
|
|
@@ -2345,7 +2478,7 @@ function parseProductSpec(raw) {
|
|
|
2345
2478
|
`Empty image ID for product ${productId.toLowerCase()}. Omit the colon or provide a valid UUID.`
|
|
2346
2479
|
);
|
|
2347
2480
|
}
|
|
2348
|
-
if (!
|
|
2481
|
+
if (!UUID_RE3.test(imageIdRaw)) {
|
|
2349
2482
|
throw new ArgError(
|
|
2350
2483
|
`Invalid image ID: '${imageIdRaw}' for product ${productId.toLowerCase()}. Expected a UUID.`
|
|
2351
2484
|
);
|
|
@@ -2440,7 +2573,41 @@ async function runDeviceFlow(opts) {
|
|
|
2440
2573
|
}
|
|
2441
2574
|
}
|
|
2442
2575
|
async function requestDeviceAuthorization(opts) {
|
|
2443
|
-
|
|
2576
|
+
try {
|
|
2577
|
+
return await attemptDeviceAuthorization(opts, opts.scope);
|
|
2578
|
+
} catch (err) {
|
|
2579
|
+
if (!(err instanceof ScopeRefusedError)) throw err;
|
|
2580
|
+
if (!opts.allowScopeDowngrade || opts.requiredScope === void 0) {
|
|
2581
|
+
throw new AuthError("AUTH_TOKEN_INVALID", scopeRefusalMessage(err.description));
|
|
2582
|
+
}
|
|
2583
|
+
opts.onScopeDowngrade?.({
|
|
2584
|
+
requested: opts.scope,
|
|
2585
|
+
retryingWith: opts.requiredScope,
|
|
2586
|
+
description: err.description
|
|
2587
|
+
});
|
|
2588
|
+
try {
|
|
2589
|
+
return await attemptDeviceAuthorization(opts, opts.requiredScope);
|
|
2590
|
+
} catch (retryErr) {
|
|
2591
|
+
if (retryErr instanceof ScopeRefusedError) {
|
|
2592
|
+
throw new AuthError("AUTH_TOKEN_INVALID", scopeRefusalMessage(retryErr.description));
|
|
2593
|
+
}
|
|
2594
|
+
throw retryErr;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function scopeRefusalMessage(description) {
|
|
2599
|
+
return `The authorization server refused the requested scopes${description ? `: ${description}` : "."} Re-run \`clickraft login\` without --scope to request the defaults.`;
|
|
2600
|
+
}
|
|
2601
|
+
var ScopeRefusedError = class extends Error {
|
|
2602
|
+
constructor(description) {
|
|
2603
|
+
super("invalid_scope");
|
|
2604
|
+
this.description = description;
|
|
2605
|
+
this.name = "ScopeRefusedError";
|
|
2606
|
+
}
|
|
2607
|
+
description;
|
|
2608
|
+
};
|
|
2609
|
+
async function attemptDeviceAuthorization(opts, scope) {
|
|
2610
|
+
const body = new URLSearchParams({ client_id: opts.clientId, scope }).toString();
|
|
2444
2611
|
const headers = { "content-type": FORM_CONTENT_TYPE };
|
|
2445
2612
|
if (opts.cloudflareAccess) {
|
|
2446
2613
|
headers["cf-access-client-id"] = opts.cloudflareAccess.clientId;
|
|
@@ -2471,6 +2638,12 @@ async function requestDeviceAuthorization(opts) {
|
|
|
2471
2638
|
}
|
|
2472
2639
|
const text = await resp.body.text();
|
|
2473
2640
|
if (resp.statusCode !== 200) {
|
|
2641
|
+
if (resp.statusCode === 400) {
|
|
2642
|
+
const maybe = DeviceTokenPollErrorSchema.safeParse(safeParseJson(text));
|
|
2643
|
+
if (maybe.success && maybe.data.error === "invalid_scope") {
|
|
2644
|
+
throw new ScopeRefusedError(maybe.data.error_description);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2474
2647
|
const redacted = redactTokenLikeFields(truncate(text));
|
|
2475
2648
|
throw new AuthError(
|
|
2476
2649
|
"AUTH_TOKEN_INVALID",
|
|
@@ -2668,6 +2841,13 @@ function isAbortError2(err) {
|
|
|
2668
2841
|
}
|
|
2669
2842
|
return false;
|
|
2670
2843
|
}
|
|
2844
|
+
function safeParseJson(text) {
|
|
2845
|
+
try {
|
|
2846
|
+
return JSON.parse(text);
|
|
2847
|
+
} catch {
|
|
2848
|
+
return void 0;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2671
2851
|
function parseJson(text, label) {
|
|
2672
2852
|
try {
|
|
2673
2853
|
return JSON.parse(text);
|
|
@@ -2694,7 +2874,7 @@ function safeHeaders(headers) {
|
|
|
2694
2874
|
var PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000";
|
|
2695
2875
|
var PLACEHOLDER_ORG_SLUG = "unknown";
|
|
2696
2876
|
var CLIENT_ID = "clickraft-cli";
|
|
2697
|
-
var
|
|
2877
|
+
var REQUIRED_SCOPES = [
|
|
2698
2878
|
"templates:read",
|
|
2699
2879
|
"generations:write",
|
|
2700
2880
|
"generations:read",
|
|
@@ -2706,6 +2886,8 @@ var DEFAULT_SCOPES = [
|
|
|
2706
2886
|
"assets:write",
|
|
2707
2887
|
"balance:read"
|
|
2708
2888
|
];
|
|
2889
|
+
var OPTIONAL_SCOPES = ["products:write"];
|
|
2890
|
+
var DEFAULT_SCOPES = [...REQUIRED_SCOPES, ...OPTIONAL_SCOPES];
|
|
2709
2891
|
async function login(options) {
|
|
2710
2892
|
const config = await loadConfig({
|
|
2711
2893
|
flagProfile: options.profile,
|
|
@@ -2724,12 +2906,19 @@ async function login(options) {
|
|
|
2724
2906
|
);
|
|
2725
2907
|
}
|
|
2726
2908
|
}
|
|
2727
|
-
const
|
|
2909
|
+
const explicitScope = options.scope?.trim();
|
|
2910
|
+
const scope = explicitScope || DEFAULT_SCOPES.join(" ");
|
|
2728
2911
|
const flow = options.runDeviceFlow ?? runDeviceFlow;
|
|
2729
2912
|
const token = await flow({
|
|
2730
2913
|
apiBaseUrl,
|
|
2731
2914
|
clientId: CLIENT_ID,
|
|
2732
2915
|
scope,
|
|
2916
|
+
requiredScope: REQUIRED_SCOPES.join(" "),
|
|
2917
|
+
// ONLY when the list is ours. An explicit --scope is a wholesale override,
|
|
2918
|
+
// and quietly requesting a different set than the caller asked for would
|
|
2919
|
+
// hand them a token whose powers they never chose and cannot see.
|
|
2920
|
+
allowScopeDowngrade: !explicitScope,
|
|
2921
|
+
onScopeDowngrade: options.onScopeDowngrade,
|
|
2733
2922
|
onUserCode: options.onUserCode,
|
|
2734
2923
|
onPoll: options.onPoll,
|
|
2735
2924
|
signal: options.signal,
|
|
@@ -2951,6 +3140,71 @@ async function listNodes(opts) {
|
|
|
2951
3140
|
return data;
|
|
2952
3141
|
}
|
|
2953
3142
|
|
|
3143
|
+
// src/auth/scopes.ts
|
|
3144
|
+
var PRODUCTS_WRITE_SCOPE = "products:write";
|
|
3145
|
+
function assertScope(config, scope, hint) {
|
|
3146
|
+
if (config.scopes === null) return;
|
|
3147
|
+
if (config.scopes.includes(scope)) return;
|
|
3148
|
+
throw new ApiError("AUTH_TOKEN_SCOPE_INSUFFICIENT", hint, { retryable: false, hint });
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/commands/product/create.ts
|
|
3152
|
+
var IDEMPOTENCY_KEY_MAX = 255;
|
|
3153
|
+
async function createProduct(options) {
|
|
3154
|
+
if (options.idempotencyKey !== void 0) {
|
|
3155
|
+
const len = options.idempotencyKey.length;
|
|
3156
|
+
if (len < 1 || len > IDEMPOTENCY_KEY_MAX) {
|
|
3157
|
+
throw new ArgError(`--idempotency-key must be 1..${IDEMPOTENCY_KEY_MAX} characters.`);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
const config = await loadConfig({
|
|
3161
|
+
flagProfile: options.profile,
|
|
3162
|
+
flagApiBaseUrl: options.apiBaseUrl,
|
|
3163
|
+
flagToken: options.token,
|
|
3164
|
+
credentialsPath: options.credentialsPath
|
|
3165
|
+
});
|
|
3166
|
+
if (!config.accessToken) {
|
|
3167
|
+
throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
|
|
3168
|
+
}
|
|
3169
|
+
assertScope(config, PRODUCTS_WRITE_SCOPE, options.scopeHint);
|
|
3170
|
+
const body = buildRequestBody2(options);
|
|
3171
|
+
const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
|
|
3172
|
+
apiBaseUrl,
|
|
3173
|
+
accessToken,
|
|
3174
|
+
signal: options.signal,
|
|
3175
|
+
cloudflareAccess: config.cloudflareAccess
|
|
3176
|
+
}));
|
|
3177
|
+
const client = factory(config.apiBaseUrl, config.accessToken);
|
|
3178
|
+
return client.request({
|
|
3179
|
+
method: "POST",
|
|
3180
|
+
path: "/products",
|
|
3181
|
+
body,
|
|
3182
|
+
idempotencyKey: options.idempotencyKey,
|
|
3183
|
+
responseSchema: ProductSummarySchema,
|
|
3184
|
+
signal: options.signal
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
function buildRequestBody2(options) {
|
|
3188
|
+
if (options.images.length === 0) {
|
|
3189
|
+
throw new ArgError("At least one --image is required.");
|
|
3190
|
+
}
|
|
3191
|
+
if (options.primary !== void 0) {
|
|
3192
|
+
const known = options.images.some((img) => img.assetId === options.primary);
|
|
3193
|
+
if (!known) {
|
|
3194
|
+
throw new ArgError(`--primary ${options.primary} is not among the --image assets provided.`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
const images = options.images.map((img) => {
|
|
3198
|
+
const entry = { assetId: img.assetId };
|
|
3199
|
+
if (img.altText !== void 0) entry.altText = img.altText;
|
|
3200
|
+
if (options.primary === img.assetId) entry.isPrimary = true;
|
|
3201
|
+
return entry;
|
|
3202
|
+
});
|
|
3203
|
+
const raw = { name: options.name, images };
|
|
3204
|
+
if (options.description !== void 0) raw.description = options.description;
|
|
3205
|
+
return parseProductCreateRequest(raw);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
2954
3208
|
// src/commands/product/list.ts
|
|
2955
3209
|
import "zod";
|
|
2956
3210
|
var DEFAULT_LIMIT = 50;
|
|
@@ -2992,6 +3246,23 @@ async function listProducts(options) {
|
|
|
2992
3246
|
};
|
|
2993
3247
|
}
|
|
2994
3248
|
|
|
3249
|
+
// src/commands/product/parse-image-spec.ts
|
|
3250
|
+
function parseImageSpec(spec) {
|
|
3251
|
+
const trimmed = spec.trim();
|
|
3252
|
+
if (trimmed.length === 0) {
|
|
3253
|
+
throw new ArgError("--image requires a value of the form <assetId>[:<altText>].");
|
|
3254
|
+
}
|
|
3255
|
+
const colon = trimmed.indexOf(":");
|
|
3256
|
+
const assetId = (colon === -1 ? trimmed : trimmed.slice(0, colon)).trim();
|
|
3257
|
+
const rawAlt = colon === -1 ? void 0 : trimmed.slice(colon + 1).trim();
|
|
3258
|
+
if (!UUID_RE.test(assetId)) {
|
|
3259
|
+
throw new ArgError(
|
|
3260
|
+
`--image expects an asset id (UUID) before the colon, got "${assetId}". Use the assetId returned by \`clickraft upload\`.`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return rawAlt ? { assetId, altText: rawAlt } : { assetId };
|
|
3264
|
+
}
|
|
3265
|
+
|
|
2995
3266
|
// src/telemetry/detect.ts
|
|
2996
3267
|
var AGENT_SIGNALS = [
|
|
2997
3268
|
{ envVar: "CLAUDE_CODE_SESSION_ID", agent: "claude-code" },
|
|
@@ -3565,6 +3836,21 @@ function renderProductsList(result, out) {
|
|
|
3565
3836
|
out.write("\nMore results available. Use --cursor to fetch the next page.\n");
|
|
3566
3837
|
}
|
|
3567
3838
|
}
|
|
3839
|
+
function renderProductCreateResult(product, out) {
|
|
3840
|
+
const lines = [
|
|
3841
|
+
`Created product "${product.title}"`,
|
|
3842
|
+
` ID: ${product.id}`,
|
|
3843
|
+
` Source: ${product.sourceType}`,
|
|
3844
|
+
` Images: ${product.images.length}`
|
|
3845
|
+
];
|
|
3846
|
+
for (const img of product.images) {
|
|
3847
|
+
lines.push(
|
|
3848
|
+
` ${img.isPrimary ? "*" : "-"} ${img.id}${img.altText ? ` ${img.altText}` : ""}`
|
|
3849
|
+
);
|
|
3850
|
+
}
|
|
3851
|
+
lines.push("", `Use it with: clickraft generate create --product ${product.id} ...`);
|
|
3852
|
+
out.write(lines.join("\n") + "\n");
|
|
3853
|
+
}
|
|
3568
3854
|
function renderTemplatesList(result, out) {
|
|
3569
3855
|
const { templates } = result;
|
|
3570
3856
|
if (templates.length === 0) {
|
|
@@ -3943,6 +4229,18 @@ async function loginCommand(parsed, rc) {
|
|
|
3943
4229
|
},
|
|
3944
4230
|
onPoll: () => {
|
|
3945
4231
|
rc.uiHooks?.onPoll?.();
|
|
4232
|
+
},
|
|
4233
|
+
// Always warn, including in --json mode, and always on stderr: the login
|
|
4234
|
+
// succeeded, so the JSON envelope on stdout stays a clean success, but the
|
|
4235
|
+
// token is quietly less capable than the one that was asked for. Silence
|
|
4236
|
+
// here is how a user ends up debugging a scope error days later.
|
|
4237
|
+
onScopeDowngrade: (info) => {
|
|
4238
|
+
rc.stderr.write(
|
|
4239
|
+
`Warning: the server refused one or more requested scopes${info.description ? ` (${info.description})` : ""}.
|
|
4240
|
+
Retrying with the required scopes only. This token will not be able to use commands that need the refused scopes.
|
|
4241
|
+
Re-run \`clickraft login --force-reauth\` once the server grants them.
|
|
4242
|
+
`
|
|
4243
|
+
);
|
|
3946
4244
|
}
|
|
3947
4245
|
});
|
|
3948
4246
|
} finally {
|
|
@@ -4029,6 +4327,7 @@ function renderRootHelp(version) {
|
|
|
4029
4327
|
" balance Show credit balance and rate limits",
|
|
4030
4328
|
" brand-model list List available brand models",
|
|
4031
4329
|
" product list List products in your organization",
|
|
4330
|
+
" product create Create a product from uploaded assets",
|
|
4032
4331
|
" models list List available AI models",
|
|
4033
4332
|
" nodes list List available workflow node types",
|
|
4034
4333
|
" nodes describe Describe a node type's ports and fields",
|
|
@@ -4120,7 +4419,8 @@ function renderCommandHelp(command) {
|
|
|
4120
4419
|
"Usage: clickraft product <subcommand> [options]",
|
|
4121
4420
|
"",
|
|
4122
4421
|
"Subcommands:",
|
|
4123
|
-
" list
|
|
4422
|
+
" list List products in your organization",
|
|
4423
|
+
" create Create a product from uploaded assets",
|
|
4124
4424
|
""
|
|
4125
4425
|
].join("\n");
|
|
4126
4426
|
case "product list":
|
|
@@ -4141,6 +4441,34 @@ function renderCommandHelp(command) {
|
|
|
4141
4441
|
" --json",
|
|
4142
4442
|
""
|
|
4143
4443
|
].join("\n");
|
|
4444
|
+
case "product create":
|
|
4445
|
+
return [
|
|
4446
|
+
"Usage: clickraft product create --name <name> --image <assetId>[:<altText>] [options]",
|
|
4447
|
+
"",
|
|
4448
|
+
"Create a product from images already uploaded to your Clickraft library.",
|
|
4449
|
+
"Upload them first with `clickraft upload`, which prints the assetId to use here.",
|
|
4450
|
+
"",
|
|
4451
|
+
"Options:",
|
|
4452
|
+
" --name <name> Product name (required, max 200 chars)",
|
|
4453
|
+
" --image <spec> <assetId>[:<altText>] (repeatable, 1-10 images, required)",
|
|
4454
|
+
" --description <text> Product description (max 5000 chars)",
|
|
4455
|
+
" --primary <assetId> Mark this image primary (default: the first --image)",
|
|
4456
|
+
" --idempotency-key <key> Reuse a key to make a retry safe across invocations",
|
|
4457
|
+
" --profile <name>",
|
|
4458
|
+
" --api-base-url <url>",
|
|
4459
|
+
" --token <token>",
|
|
4460
|
+
" --json",
|
|
4461
|
+
"",
|
|
4462
|
+
"Examples:",
|
|
4463
|
+
' clickraft product create --name "Ceramic Mug" \\',
|
|
4464
|
+
" --image 1f7c1e8e-2a4b-4c9d-8e5f-3b2a1c0d9e8f",
|
|
4465
|
+
"",
|
|
4466
|
+
' clickraft product create --name "Ceramic Mug" \\',
|
|
4467
|
+
' --image "1f7c1e8e-2a4b-4c9d-8e5f-3b2a1c0d9e8f:Front view" \\',
|
|
4468
|
+
' --image "2a8d2f9f-3b5c-4d0e-9f6a-4c3b2d1e0f9a:Side view" \\',
|
|
4469
|
+
" --primary 2a8d2f9f-3b5c-4d0e-9f6a-4c3b2d1e0f9a",
|
|
4470
|
+
""
|
|
4471
|
+
].join("\n");
|
|
4144
4472
|
case "models":
|
|
4145
4473
|
return [
|
|
4146
4474
|
"Usage: clickraft models <subcommand> [options]",
|
|
@@ -4304,6 +4632,7 @@ function renderCommandHelp(command) {
|
|
|
4304
4632
|
" --aspect-ratio <a:b> e.g. 16:9",
|
|
4305
4633
|
" --resolution <size> e.g. 1024x768, 720p, 1K",
|
|
4306
4634
|
" --duration-seconds <n> For video/audio models (1-60)",
|
|
4635
|
+
" --quality <tier> e.g. low, medium, high (valid values depend on the model)",
|
|
4307
4636
|
" --workflow-id <uuid> Attach to an existing workflow",
|
|
4308
4637
|
" --node-id <id> Attach to a specific node",
|
|
4309
4638
|
" --no-wait, --async Return immediately with the jobId",
|
|
@@ -4488,6 +4817,7 @@ function matchNewCommand(argv) {
|
|
|
4488
4817
|
if (verb === "product") {
|
|
4489
4818
|
const sub = argv[1];
|
|
4490
4819
|
if (sub === "list") return { kind: "cmd", path: "product.list", verbTokenCount: 2 };
|
|
4820
|
+
if (sub === "create") return { kind: "cmd", path: "product.create", verbTokenCount: 2 };
|
|
4491
4821
|
if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "product" };
|
|
4492
4822
|
return { kind: "unknown-sub", verb: "product", sub };
|
|
4493
4823
|
}
|
|
@@ -4571,6 +4901,8 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
|
|
|
4571
4901
|
return await runTemplateFind(argvAfterVerb, ctx, rc);
|
|
4572
4902
|
case "product.list":
|
|
4573
4903
|
return await runProductList(argvAfterVerb, ctx, rc);
|
|
4904
|
+
case "product.create":
|
|
4905
|
+
return await runProductCreate(argvAfterVerb, ctx, rc);
|
|
4574
4906
|
case "models.list":
|
|
4575
4907
|
return await runModelsList(argvAfterVerb, ctx, rc);
|
|
4576
4908
|
case "nodes.list":
|
|
@@ -4619,6 +4951,7 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4619
4951
|
"aspect-ratio",
|
|
4620
4952
|
"resolution",
|
|
4621
4953
|
"duration-seconds",
|
|
4954
|
+
"quality",
|
|
4622
4955
|
"workflow-id",
|
|
4623
4956
|
"node-id",
|
|
4624
4957
|
"timeout"
|
|
@@ -4642,6 +4975,7 @@ async function runGenerateCreate(argv, ctx, rc) {
|
|
|
4642
4975
|
aspectRatio: parsed.string["aspect-ratio"],
|
|
4643
4976
|
resolution: parsed.string.resolution,
|
|
4644
4977
|
durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
|
|
4978
|
+
quality: parsed.string.quality,
|
|
4645
4979
|
brandModels,
|
|
4646
4980
|
products,
|
|
4647
4981
|
workflowId: parsed.string["workflow-id"],
|
|
@@ -4875,7 +5209,20 @@ function emitTemplateDetail(detail, ctx, rc) {
|
|
|
4875
5209
|
}
|
|
4876
5210
|
renderTemplateDetail(detail, rc.stdout);
|
|
4877
5211
|
}
|
|
4878
|
-
|
|
5212
|
+
function scopeHintFor(scope) {
|
|
5213
|
+
return `Your CLI token doesn't include ${scope}. Run \`clickraft login --force-reauth\` to re-authenticate with the new scope.`;
|
|
5214
|
+
}
|
|
5215
|
+
async function withScopeHint(scope, fn) {
|
|
5216
|
+
try {
|
|
5217
|
+
return await fn();
|
|
5218
|
+
} catch (err) {
|
|
5219
|
+
if (err instanceof ApiError && err.code === "AUTH_TOKEN_SCOPE_INSUFFICIENT") {
|
|
5220
|
+
const hint = scopeHintFor(scope);
|
|
5221
|
+
throw new ApiError(err.code, hint, { ...err.meta, hint }, err.cause);
|
|
5222
|
+
}
|
|
5223
|
+
throw err;
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
4879
5226
|
async function runProductList(argv, ctx, rc) {
|
|
4880
5227
|
const parsed = parseArgs(argv, {
|
|
4881
5228
|
string: [
|
|
@@ -4892,8 +5239,9 @@ async function runProductList(argv, ctx, rc) {
|
|
|
4892
5239
|
if (limit !== void 0 && (limit < 1 || limit > 100)) {
|
|
4893
5240
|
throw new ArgError("--limit must be between 1 and 100.");
|
|
4894
5241
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
5242
|
+
const result = await withScopeHint(
|
|
5243
|
+
"products:read",
|
|
5244
|
+
() => listProducts({
|
|
4897
5245
|
cursor: parsed.string.cursor,
|
|
4898
5246
|
limit,
|
|
4899
5247
|
syncStatus: parsed.string["sync-status"],
|
|
@@ -4903,21 +5251,71 @@ async function runProductList(argv, ctx, rc) {
|
|
|
4903
5251
|
apiBaseUrl: parsed.string["api-base-url"],
|
|
4904
5252
|
token: parsed.string.token,
|
|
4905
5253
|
signal: rc.signal
|
|
4906
|
-
})
|
|
4907
|
-
|
|
4908
|
-
|
|
5254
|
+
})
|
|
5255
|
+
);
|
|
5256
|
+
ctx.nextCursor = result.nextCursor ?? void 0;
|
|
5257
|
+
emitProductList(result, ctx, rc);
|
|
5258
|
+
return 0;
|
|
5259
|
+
}
|
|
5260
|
+
var PRODUCT_CREATE_NOT_ENABLED = "Product creation is not enabled on this deployment yet. The endpoint answers 404 while the feature flag is off.";
|
|
5261
|
+
function remapProductCreateError(err) {
|
|
5262
|
+
if (!(err instanceof ApiError)) return err;
|
|
5263
|
+
if (err.code === "NOT_FOUND") {
|
|
5264
|
+
return new ApiError(
|
|
5265
|
+
err.code,
|
|
5266
|
+
PRODUCT_CREATE_NOT_ENABLED,
|
|
5267
|
+
{ ...err.meta, hint: PRODUCT_CREATE_NOT_ENABLED },
|
|
5268
|
+
err.cause
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
if (err.code === "IDEMPOTENCY_KEY_CONFLICT") {
|
|
5272
|
+
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.";
|
|
5273
|
+
return new ApiError(err.code, err.message, { ...err.meta, hint }, err.cause);
|
|
5274
|
+
}
|
|
5275
|
+
return err;
|
|
5276
|
+
}
|
|
5277
|
+
async function runProductCreate(argv, ctx, rc) {
|
|
5278
|
+
const parsed = parseArgs(argv, {
|
|
5279
|
+
string: [...COMMON_RICH_STRING_FLAGS, "name", "description", "primary", "idempotency-key"],
|
|
5280
|
+
boolean: [...COMMON_RICH_BOOL_FLAGS],
|
|
5281
|
+
array: ["image"]
|
|
5282
|
+
});
|
|
5283
|
+
const name = parsed.string.name;
|
|
5284
|
+
if (!name) throw new ArgError("--name is required.");
|
|
5285
|
+
const rawImages = parsed.array["image"] ?? [];
|
|
5286
|
+
if (rawImages.length === 0) throw new ArgError("At least one --image is required.");
|
|
5287
|
+
const images = rawImages.map(parseImageSpec);
|
|
5288
|
+
try {
|
|
5289
|
+
const product = await withScopeHint(
|
|
5290
|
+
"products:write",
|
|
5291
|
+
() => createProduct({
|
|
5292
|
+
name,
|
|
5293
|
+
description: parsed.string.description,
|
|
5294
|
+
images,
|
|
5295
|
+
primary: parsed.string.primary,
|
|
5296
|
+
idempotencyKey: parsed.string["idempotency-key"],
|
|
5297
|
+
profile: parsed.string.profile,
|
|
5298
|
+
apiBaseUrl: parsed.string["api-base-url"],
|
|
5299
|
+
token: parsed.string.token,
|
|
5300
|
+
signal: rc.signal,
|
|
5301
|
+
scopeHint: scopeHintFor("products:write")
|
|
5302
|
+
})
|
|
5303
|
+
);
|
|
5304
|
+
emitProductCreate(product, ctx, rc);
|
|
4909
5305
|
return 0;
|
|
4910
5306
|
} catch (err) {
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
5307
|
+
throw remapProductCreateError(err);
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
function emitProductCreate(product, ctx, rc) {
|
|
5311
|
+
if (rc.signal?.aborted) {
|
|
5312
|
+
throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
|
|
5313
|
+
}
|
|
5314
|
+
if (rc.jsonMode) {
|
|
5315
|
+
rc.stdout.write(JSON.stringify(wrapSuccess(product, ctx)) + "\n");
|
|
5316
|
+
return;
|
|
4920
5317
|
}
|
|
5318
|
+
renderProductCreateResult(product, rc.stdout);
|
|
4921
5319
|
}
|
|
4922
5320
|
function emitProductList(result, ctx, rc) {
|
|
4923
5321
|
if (rc.signal?.aborted) {
|