@clickraft/cli 0.5.0 → 0.6.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 CHANGED
@@ -1,8 +1,20 @@
1
- ## [0.5.0](https://github.com/clickraft/cli/compare/v0.4.0...v0.5.0) (2026-05-24)
1
+ ## [0.6.0](https://github.com/clickraft/cli/compare/v0.5.0...v0.6.0) (2026-05-25)
2
2
 
3
3
  ### Features
4
4
 
5
- * **models:** add models list command consuming agents/v1/models ([83761d1](https://github.com/clickraft/cli/commit/83761d11d136758e23c40cf80cd87c8f08dbaad6))
5
+ * **errors:** add PRODUCT_* server error codes ([b60a018](https://github.com/clickraft/cli/commit/b60a0184f57d52d8a423631adeea69d47e8f28fc))
6
+ * **products:** add --product flag to generate create ([41a8948](https://github.com/clickraft/cli/commit/41a8948ee79c63abffe891411c0a36c67059d2d5))
7
+ * **products:** add product list command with cursor pagination ([ca4c343](https://github.com/clickraft/cli/commit/ca4c3433e5e3aa7c53ccdd5ab26bac3775d37608))
8
+ * **products:** show product and primary image IDs in list render ([2dc1d65](https://github.com/clickraft/cli/commit/2dc1d653ada365ceb0f1fa68697df71e24045633))
9
+ * **products:** wire product commands into CLI dispatch ([c458355](https://github.com/clickraft/cli/commit/c45835599fe546f8a19ad6b08eb35ee772299d49))
10
+ * **scopes:** request products:read in OAuth login ([ad67816](https://github.com/clickraft/cli/commit/ad67816e359de8198f2c5423de9f6ab9671f7418))
11
+
12
+ ### Bug Fixes
13
+
14
+ * **products:** align scope-insufficient hint in error message and Try line ([6f6cb1e](https://github.com/clickraft/cli/commit/6f6cb1e935e9e65b7039f4423acdb0569772892d))
15
+ * **products:** honor meta.hint in JSON error envelope ([f39b516](https://github.com/clickraft/cli/commit/f39b51668b29d5d78166933d56bd4f3954039ebe))
16
+ * **products:** tolerate forward-compat additive fields in list response ([cfe7bf2](https://github.com/clickraft/cli/commit/cfe7bf25dc18710583408dea9dc1e975449ce13a))
17
+ * **products:** validate --limit range and correct login help scope count ([d1341d9](https://github.com/clickraft/cli/commit/d1341d929f80f87407ec89c9abff5c214cd3b52a))
6
18
 
7
19
  # Changelog
8
20
 
package/dist/cli.js CHANGED
@@ -334,6 +334,55 @@ var EXIT_CODE_MAP = {
334
334
  retryable: false,
335
335
  hint: "Too many reference images. Check model limits."
336
336
  },
337
+ // --- 8 product reference resolver codes -------------------------------------
338
+ PRODUCT_NOT_FOUND: {
339
+ exitCode: 1,
340
+ category: "runtime",
341
+ retryable: false,
342
+ hint: "No product with that ID in your org. Run `clickraft product list`."
343
+ },
344
+ PRODUCT_DUPLICATE_REFERENCE: {
345
+ exitCode: 2,
346
+ category: "usage",
347
+ retryable: false,
348
+ hint: "Same (productId, imageId) tuple appears twice in the request."
349
+ },
350
+ PRODUCT_IMAGE_NOT_FOUND: {
351
+ exitCode: 1,
352
+ category: "runtime",
353
+ retryable: false,
354
+ hint: "imageId does not exist in the product images."
355
+ },
356
+ PRODUCT_IMAGE_MISMATCH: {
357
+ exitCode: 2,
358
+ category: "usage",
359
+ retryable: false,
360
+ hint: "imageId belongs to a different product."
361
+ },
362
+ PRODUCT_SYNC_ERROR: {
363
+ exitCode: 1,
364
+ category: "runtime",
365
+ retryable: false,
366
+ hint: "Product sync status is 'error'. Re-sync or use a different product."
367
+ },
368
+ PRODUCT_DELETED_UPSTREAM: {
369
+ exitCode: 1,
370
+ category: "runtime",
371
+ retryable: false,
372
+ hint: "Product was deleted upstream. Remove it or re-sync."
373
+ },
374
+ PRODUCT_NO_PRIMARY_IMAGE: {
375
+ exitCode: 1,
376
+ category: "runtime",
377
+ retryable: false,
378
+ hint: "Product has no primary image. Specify --product <id>:<imageId> explicitly."
379
+ },
380
+ PRODUCT_SOURCE_MANAGED: {
381
+ exitCode: 4,
382
+ category: "permission",
383
+ retryable: false,
384
+ hint: "Product is managed by an external integration."
385
+ },
337
386
  // --- 4 CLI-synthetic codes --------------------------------------------------
338
387
  NETWORK_ERROR: {
339
388
  exitCode: 6,
@@ -877,7 +926,8 @@ function errorToEnvelopePayload(err) {
877
926
  retryable: err.meta.retryable,
878
927
  retryAfterMs: err.meta.retryAfterMs,
879
928
  requestId: err.meta.requestId,
880
- details: err.meta.details
929
+ details: err.meta.details,
930
+ hint: err.meta.hint
881
931
  });
882
932
  }
883
933
  if (err instanceof AuthError || err instanceof CredentialsError) {
@@ -905,7 +955,8 @@ function buildPayload(args) {
905
955
  };
906
956
  if (typeof args.retryAfterMs === "number") payload.retry_after_ms = args.retryAfterMs;
907
957
  if (args.requestId) payload.request_id = args.requestId;
908
- if (mapping?.hint) payload.hint = mapping.hint;
958
+ const resolvedHint = args.hint ?? mapping?.hint;
959
+ if (resolvedHint) payload.hint = resolvedHint;
909
960
  if (args.details != null) payload.details = args.details;
910
961
  return payload;
911
962
  }
@@ -1322,6 +1373,10 @@ async function listBrandModels(options) {
1322
1373
 
1323
1374
  // src/schemas/jobs.ts
1324
1375
  import { z as z6 } from "zod";
1376
+ var ProductReferenceSchema = z6.object({
1377
+ id: z6.string().uuid(),
1378
+ imageId: z6.string().uuid().optional()
1379
+ }).strict();
1325
1380
  var GenerateCreateRequestSchema = z6.object({
1326
1381
  modelSlug: z6.string().min(1).max(100),
1327
1382
  input: z6.object({
@@ -1336,7 +1391,8 @@ var GenerateCreateRequestSchema = z6.object({
1336
1391
  options: z6.object({
1337
1392
  workflowId: z6.string().uuid().optional(),
1338
1393
  nodeId: z6.string().optional()
1339
- }).strict().optional()
1394
+ }).strict().optional(),
1395
+ products: z6.array(ProductReferenceSchema).min(1).max(10).optional()
1340
1396
  }).strict();
1341
1397
  var GenerateCreateResponseSchema = z6.object({
1342
1398
  jobId: z6.string().uuid(),
@@ -1807,6 +1863,13 @@ function buildRequestBody(args) {
1807
1863
  if (args.nodeId !== void 0) options.nodeId = args.nodeId;
1808
1864
  body.options = options;
1809
1865
  }
1866
+ if (args.products !== void 0 && args.products.length > 0) {
1867
+ body.products = args.products.map((spec) => {
1868
+ const entry = { id: spec.id };
1869
+ if (spec.imageId) entry.imageId = spec.imageId;
1870
+ return entry;
1871
+ });
1872
+ }
1810
1873
  const parsed = GenerateCreateRequestSchema.safeParse(body);
1811
1874
  if (!parsed.success) {
1812
1875
  throw new ApiError(
@@ -1979,6 +2042,44 @@ function validateBrandModelSpecs(specs) {
1979
2042
  }
1980
2043
  }
1981
2044
 
2045
+ // src/commands/generate/parse-product-spec.ts
2046
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2047
+ function parseProductSpec(raw) {
2048
+ if (!raw) throw new ArgError("--product value cannot be empty.");
2049
+ const colonIdx = raw.indexOf(":");
2050
+ const productId = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
2051
+ const imageIdRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
2052
+ if (!UUID_RE2.test(productId)) {
2053
+ throw new ArgError(
2054
+ `Invalid product ID: '${productId}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
2055
+ );
2056
+ }
2057
+ if (imageIdRaw !== void 0) {
2058
+ if (!imageIdRaw) {
2059
+ throw new ArgError(
2060
+ `Empty image ID for product ${productId.toLowerCase()}. Omit the colon or provide a valid UUID.`
2061
+ );
2062
+ }
2063
+ if (!UUID_RE2.test(imageIdRaw)) {
2064
+ throw new ArgError(
2065
+ `Invalid image ID: '${imageIdRaw}' for product ${productId.toLowerCase()}. Expected a UUID.`
2066
+ );
2067
+ }
2068
+ return { id: productId.toLowerCase(), imageId: imageIdRaw.toLowerCase() };
2069
+ }
2070
+ return { id: productId.toLowerCase() };
2071
+ }
2072
+ function validateProductSpecs(specs) {
2073
+ const seen = /* @__PURE__ */ new Set();
2074
+ for (const spec of specs) {
2075
+ const key = spec.imageId ? `${spec.id}:${spec.imageId}` : spec.id;
2076
+ if (seen.has(key)) {
2077
+ throw new ArgError(`Duplicate product reference: ${key}. Each --product must be unique.`);
2078
+ }
2079
+ seen.add(key);
2080
+ }
2081
+ }
2082
+
1982
2083
  // src/auth/device-flow.ts
1983
2084
  import { setTimeout as delay } from "timers/promises";
1984
2085
  import { request } from "undici";
@@ -2315,6 +2416,7 @@ var DEFAULT_SCOPES = [
2315
2416
  "workflows:write",
2316
2417
  "workflows:read",
2317
2418
  "brand-models:read",
2419
+ "products:read",
2318
2420
  "assets:write",
2319
2421
  "balance:read"
2320
2422
  ];
@@ -2512,6 +2614,68 @@ async function listModels(opts) {
2512
2614
  return data;
2513
2615
  }
2514
2616
 
2617
+ // src/commands/product/list.ts
2618
+ import { z as z9 } from "zod";
2619
+ var ProductImageSummarySchema = z9.object({
2620
+ id: z9.string().uuid(),
2621
+ url: z9.string(),
2622
+ altText: z9.string().nullable(),
2623
+ isPrimary: z9.boolean(),
2624
+ sortOrder: z9.number().int()
2625
+ });
2626
+ var ProductSummarySchema = z9.object({
2627
+ id: z9.string().uuid(),
2628
+ title: z9.string(),
2629
+ sourceType: z9.string(),
2630
+ syncStatus: z9.enum(["synced", "pending", "stale", "error", "deleted_upstream"]).nullable(),
2631
+ vendor: z9.string().nullable(),
2632
+ productType: z9.string().nullable(),
2633
+ tags: z9.array(z9.string()),
2634
+ images: z9.array(ProductImageSummarySchema)
2635
+ });
2636
+ var ProductsListResponseSchema = z9.object({
2637
+ items: z9.array(ProductSummarySchema),
2638
+ nextCursor: z9.string().nullable()
2639
+ });
2640
+ var DEFAULT_LIMIT = 50;
2641
+ async function listProducts(options) {
2642
+ const config = await loadConfig({
2643
+ flagProfile: options.profile,
2644
+ flagApiBaseUrl: options.apiBaseUrl,
2645
+ flagToken: options.token,
2646
+ credentialsPath: options.credentialsPath
2647
+ });
2648
+ if (!config.accessToken) {
2649
+ throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2650
+ }
2651
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2652
+ apiBaseUrl,
2653
+ accessToken,
2654
+ signal: options.signal,
2655
+ cloudflareAccess: config.cloudflareAccess
2656
+ }));
2657
+ const client = factory(config.apiBaseUrl, config.accessToken);
2658
+ const query = {
2659
+ limit: options.limit ?? DEFAULT_LIMIT
2660
+ };
2661
+ if (options.cursor) query.cursor = options.cursor;
2662
+ if (options.syncStatus) query.syncStatus = options.syncStatus;
2663
+ if (options.sourceType) query.sourceType = options.sourceType;
2664
+ if (options.search) query.search = options.search;
2665
+ const data = await client.request({
2666
+ method: "GET",
2667
+ path: "/products",
2668
+ query,
2669
+ responseSchema: ProductsListResponseSchema,
2670
+ signal: options.signal
2671
+ });
2672
+ return {
2673
+ products: data.items,
2674
+ nextCursor: data.nextCursor,
2675
+ hasMore: data.nextCursor !== null
2676
+ };
2677
+ }
2678
+
2515
2679
  // src/telemetry/detect.ts
2516
2680
  var AGENT_SIGNALS = [
2517
2681
  { envVar: "CLAUDE_CODE_SESSION_ID", agent: "claude-code" },
@@ -2602,33 +2766,33 @@ async function buildTemplateClient(opts) {
2602
2766
  }
2603
2767
 
2604
2768
  // src/schemas/templates.ts
2605
- import { z as z9 } from "zod";
2606
- var DeclaredParamSchema = z9.object({
2607
- name: z9.string(),
2608
- type: z9.string(),
2609
- required: z9.boolean(),
2610
- defaultValue: z9.string().nullable().optional(),
2611
- enumValues: z9.array(z9.string()).nullable().optional(),
2612
- description: z9.string().nullable().optional()
2769
+ import { z as z10 } from "zod";
2770
+ var DeclaredParamSchema = z10.object({
2771
+ name: z10.string(),
2772
+ type: z10.string(),
2773
+ required: z10.boolean(),
2774
+ defaultValue: z10.string().nullable().optional(),
2775
+ enumValues: z10.array(z10.string()).nullable().optional(),
2776
+ description: z10.string().nullable().optional()
2613
2777
  }).strict();
2614
- var TemplateSummarySchema = z9.object({
2615
- id: z9.string().uuid(),
2616
- source: z9.enum(["own", "system"]),
2617
- name: z9.string(),
2618
- description: z9.string().nullable(),
2619
- category: z9.string().nullable(),
2620
- thumbnailUrl: z9.string().nullable(),
2621
- tags: z9.array(z9.string()),
2622
- createdAt: z9.string()
2778
+ var TemplateSummarySchema = z10.object({
2779
+ id: z10.string().uuid(),
2780
+ source: z10.enum(["own", "system"]),
2781
+ name: z10.string(),
2782
+ description: z10.string().nullable(),
2783
+ category: z10.string().nullable(),
2784
+ thumbnailUrl: z10.string().nullable(),
2785
+ tags: z10.array(z10.string()),
2786
+ createdAt: z10.string()
2623
2787
  }).strict();
2624
2788
  var TemplateDetailSchema = TemplateSummarySchema.extend({
2625
- declaredParams: z9.array(DeclaredParamSchema),
2626
- timesUsed: z9.number()
2789
+ declaredParams: z10.array(DeclaredParamSchema),
2790
+ timesUsed: z10.number()
2627
2791
  }).strict();
2628
- var TemplatesListResponseSchema = z9.object({
2629
- templates: z9.array(TemplateSummarySchema),
2630
- cursor: z9.string().nullable(),
2631
- hasMore: z9.boolean()
2792
+ var TemplatesListResponseSchema = z10.object({
2793
+ templates: z10.array(TemplateSummarySchema),
2794
+ cursor: z10.string().nullable(),
2795
+ hasMore: z10.boolean()
2632
2796
  }).strict();
2633
2797
 
2634
2798
  // src/commands/template/list.ts
@@ -2678,20 +2842,20 @@ async function getTemplate(opts) {
2678
2842
  }
2679
2843
 
2680
2844
  // src/commands/tokens/list.ts
2681
- import { z as z10 } from "zod";
2682
- var TokenRowSchema = z10.object({
2683
- id: z10.string().uuid(),
2684
- organizationId: z10.string().uuid(),
2685
- userId: z10.string().uuid(),
2686
- clientId: z10.string().nullable(),
2687
- tokenPrefix: z10.string(),
2688
- name: z10.string().nullable(),
2689
- scopes: z10.array(z10.string()),
2690
- lastUsedAt: z10.string().nullable(),
2691
- expiresAt: z10.string(),
2692
- createdAt: z10.string()
2845
+ import { z as z11 } from "zod";
2846
+ var TokenRowSchema = z11.object({
2847
+ id: z11.string().uuid(),
2848
+ organizationId: z11.string().uuid(),
2849
+ userId: z11.string().uuid(),
2850
+ clientId: z11.string().nullable(),
2851
+ tokenPrefix: z11.string(),
2852
+ name: z11.string().nullable(),
2853
+ scopes: z11.array(z11.string()),
2854
+ lastUsedAt: z11.string().nullable(),
2855
+ expiresAt: z11.string(),
2856
+ createdAt: z11.string()
2693
2857
  }).strict();
2694
- var TokensListResponseSchema = z10.array(TokenRowSchema);
2858
+ var TokensListResponseSchema = z11.array(TokenRowSchema);
2695
2859
  async function listTokens(options) {
2696
2860
  const config = await loadConfig({
2697
2861
  flagProfile: options.profile,
@@ -2744,7 +2908,7 @@ function errorToView(err) {
2744
2908
  return {
2745
2909
  code: toEnvelopeCode(err.code),
2746
2910
  message: err.message,
2747
- hint: EXIT_CODE_MAP[err.code]?.hint ?? FALLBACK_HINT,
2911
+ hint: err.meta.hint ?? EXIT_CODE_MAP[err.code]?.hint ?? FALLBACK_HINT,
2748
2912
  requestId: err.meta.requestId,
2749
2913
  details: formatDetails(err.meta.details)
2750
2914
  };
@@ -3004,6 +3168,39 @@ function renderBrandModelsList(models, out) {
3004
3168
  out.write(padRow(headers) + "\n");
3005
3169
  for (const cells of data) out.write(padRow(cells) + "\n");
3006
3170
  }
3171
+ function renderProductsList(result, out) {
3172
+ const { products } = result;
3173
+ if (products.length === 0) {
3174
+ out.write("No products.\n");
3175
+ return;
3176
+ }
3177
+ const MAX_TAGS = 3;
3178
+ const headers = ["ID", "TITLE", "SOURCE", "SYNC", "IMAGES", "PRIMARY IMAGE", "VENDOR", "TAGS"];
3179
+ const data = products.map((p) => {
3180
+ const shown = p.tags.slice(0, MAX_TAGS).join(", ");
3181
+ const extra = p.tags.length > MAX_TAGS ? ` +${p.tags.length - MAX_TAGS} more` : "";
3182
+ const primary = p.images.find((img) => img.isPrimary) ?? p.images.find((img) => img.sortOrder === 0);
3183
+ return [
3184
+ p.id,
3185
+ p.title,
3186
+ p.sourceType,
3187
+ p.syncStatus ?? "\u2014",
3188
+ String(p.images.length),
3189
+ primary?.id ?? "\u2014",
3190
+ p.vendor ?? "\u2014",
3191
+ p.tags.length > 0 ? shown + extra : "\u2014"
3192
+ ];
3193
+ });
3194
+ const widths = headers.map(
3195
+ (header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
3196
+ );
3197
+ const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
3198
+ out.write(padRow(headers) + "\n");
3199
+ for (const cells of data) out.write(padRow(cells) + "\n");
3200
+ if (result.hasMore) {
3201
+ out.write("\nMore results available. Use --cursor to fetch the next page.\n");
3202
+ }
3203
+ }
3007
3204
  function renderTemplatesList(result, out) {
3008
3205
  const { templates } = result;
3009
3206
  if (templates.length === 0) {
@@ -3351,6 +3548,7 @@ function renderRootHelp(version) {
3351
3548
  " tokens list List the caller's API tokens",
3352
3549
  " balance Show credit balance and rate limits",
3353
3550
  " brand-model list List available brand models",
3551
+ " product list List products in your organization",
3354
3552
  " models list List available AI models",
3355
3553
  " telemetry inspect Show current telemetry state and payload shape",
3356
3554
  " template list List available templates",
@@ -3379,7 +3577,7 @@ function renderCommandHelp(command) {
3379
3577
  "Options:",
3380
3578
  ' --profile <name> Write to a specific profile (default: "default")',
3381
3579
  " --api-base-url <url> Override the API base URL",
3382
- " --scope <s> Space-separated scope list (default: all 8)",
3580
+ ` --scope <s> Space-separated scope list (default: all ${DEFAULT_SCOPES.length})`,
3383
3581
  " --force-reauth Re-authenticate even if a valid token exists",
3384
3582
  " --no-open-browser Do not auto-open the verification URL",
3385
3583
  " --json Emit JSON envelope instead of human output",
@@ -3431,6 +3629,32 @@ function renderCommandHelp(command) {
3431
3629
  " --json Emit JSON envelope instead of human output",
3432
3630
  ""
3433
3631
  ].join("\n");
3632
+ case "product":
3633
+ return [
3634
+ "Usage: clickraft product <subcommand> [options]",
3635
+ "",
3636
+ "Subcommands:",
3637
+ " list List products in your organization",
3638
+ ""
3639
+ ].join("\n");
3640
+ case "product list":
3641
+ return [
3642
+ "Usage: clickraft product list [options]",
3643
+ "",
3644
+ "List products in your organization. Supports cursor-based pagination.",
3645
+ "",
3646
+ "Options:",
3647
+ " --cursor <token> Pagination cursor from a previous response",
3648
+ " --limit <n> Max results per page (1-100, default: 50)",
3649
+ " --sync-status <s> Filter by sync status (synced, pending, stale, error, deleted_upstream)",
3650
+ " --source-type <s> Filter by source type (manual, csv, shopify, etsy, etc.)",
3651
+ " --search <q> Free-text search query",
3652
+ " --profile <name>",
3653
+ " --api-base-url <url>",
3654
+ " --token <token>",
3655
+ " --json",
3656
+ ""
3657
+ ].join("\n");
3434
3658
  case "models":
3435
3659
  return [
3436
3660
  "Usage: clickraft models <subcommand> [options]",
@@ -3550,6 +3774,9 @@ function renderCommandHelp(command) {
3550
3774
  " --brand-model <spec> Brand Model reference (repeatable, max 3)",
3551
3775
  " Format: <uuid> or <uuid>:<pose>",
3552
3776
  " Poses: front, 3/4-right, right, left, 3/4-left, back, approved",
3777
+ " --product <spec> Product reference (repeatable)",
3778
+ " Format: <uuid> or <uuid>:<imageId>",
3779
+ " Server enforces reference cap per model",
3553
3780
  " --reference-image <v> URL or local path (repeatable, max 8)",
3554
3781
  " --aspect-ratio <a:b> e.g. 16:9",
3555
3782
  " --resolution <WxH> e.g. 1024x768",
@@ -3658,6 +3885,12 @@ function matchNewCommand(argv) {
3658
3885
  if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "template" };
3659
3886
  return { kind: "unknown-sub", verb: "template", sub };
3660
3887
  }
3888
+ if (verb === "product") {
3889
+ const sub = argv[1];
3890
+ if (sub === "list") return { kind: "cmd", path: "product.list", verbTokenCount: 2 };
3891
+ if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "product" };
3892
+ return { kind: "unknown-sub", verb: "product", sub };
3893
+ }
3661
3894
  if (verb === "models") {
3662
3895
  const sub = argv[1];
3663
3896
  if (sub === "list") return { kind: "cmd", path: "models.list", verbTokenCount: 2 };
@@ -3720,6 +3953,8 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
3720
3953
  return await runTemplateGet(argvAfterVerb, ctx, rc);
3721
3954
  case "template.find":
3722
3955
  return await runTemplateFind(argvAfterVerb, ctx, rc);
3956
+ case "product.list":
3957
+ return await runProductList(argvAfterVerb, ctx, rc);
3723
3958
  case "models.list":
3724
3959
  return await runModelsList(argvAfterVerb, ctx, rc);
3725
3960
  }
@@ -3760,13 +3995,16 @@ async function runGenerateCreate(argv, ctx, rc) {
3760
3995
  "timeout"
3761
3996
  ],
3762
3997
  boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
3763
- array: ["reference-image", "brand-model"]
3998
+ array: ["reference-image", "brand-model", "product"]
3764
3999
  });
3765
4000
  const modelSlug = parsed.string["model-slug"];
3766
4001
  if (!modelSlug) throw new ArgError("--model-slug is required.");
3767
4002
  const rawBrandModels = parsed.array["brand-model"] ?? [];
3768
4003
  const brandModels = rawBrandModels.length > 0 ? rawBrandModels.map(parseBrandModelSpec) : void 0;
3769
4004
  if (brandModels) validateBrandModelSpecs(brandModels);
4005
+ const rawProducts = parsed.array["product"] ?? [];
4006
+ const products = rawProducts.length > 0 ? rawProducts.map(parseProductSpec) : void 0;
4007
+ if (products) validateProductSpecs(products);
3770
4008
  const result = await generateCreate({
3771
4009
  prompt: parsed.string.prompt ?? parsed.positional[0],
3772
4010
  modelSlug,
@@ -3775,6 +4013,7 @@ async function runGenerateCreate(argv, ctx, rc) {
3775
4013
  resolution: parsed.string.resolution,
3776
4014
  durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
3777
4015
  brandModels,
4016
+ products,
3778
4017
  workflowId: parsed.string["workflow-id"],
3779
4018
  nodeId: parsed.string["node-id"],
3780
4019
  wait: computeWaitFlag(parsed),
@@ -3999,6 +4238,60 @@ function emitTemplateDetail(detail, ctx, rc) {
3999
4238
  }
4000
4239
  renderTemplateDetail(detail, rc.stdout);
4001
4240
  }
4241
+ var PRODUCTS_SCOPE_HINT = "Your CLI token doesn't include products:read. Run `clickraft login` to re-authenticate with the new scope.";
4242
+ async function runProductList(argv, ctx, rc) {
4243
+ const parsed = parseArgs(argv, {
4244
+ string: [
4245
+ ...COMMON_RICH_STRING_FLAGS,
4246
+ "cursor",
4247
+ "limit",
4248
+ "sync-status",
4249
+ "source-type",
4250
+ "search"
4251
+ ],
4252
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
4253
+ });
4254
+ const limit = optionalInt(parsed.string.limit, "limit");
4255
+ if (limit !== void 0 && (limit < 1 || limit > 100)) {
4256
+ throw new ArgError("--limit must be between 1 and 100.");
4257
+ }
4258
+ try {
4259
+ const result = await listProducts({
4260
+ cursor: parsed.string.cursor,
4261
+ limit,
4262
+ syncStatus: parsed.string["sync-status"],
4263
+ sourceType: parsed.string["source-type"],
4264
+ search: parsed.string.search,
4265
+ profile: parsed.string.profile,
4266
+ apiBaseUrl: parsed.string["api-base-url"],
4267
+ token: parsed.string.token,
4268
+ signal: rc.signal
4269
+ });
4270
+ ctx.nextCursor = result.nextCursor ?? void 0;
4271
+ emitProductList(result, ctx, rc);
4272
+ return 0;
4273
+ } catch (err) {
4274
+ if (err instanceof ApiError && err.code === "AUTH_TOKEN_SCOPE_INSUFFICIENT") {
4275
+ throw new ApiError(
4276
+ err.code,
4277
+ PRODUCTS_SCOPE_HINT,
4278
+ { ...err.meta, hint: PRODUCTS_SCOPE_HINT },
4279
+ err.cause
4280
+ );
4281
+ }
4282
+ throw err;
4283
+ }
4284
+ }
4285
+ function emitProductList(result, ctx, rc) {
4286
+ if (rc.signal?.aborted) {
4287
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
4288
+ }
4289
+ if (rc.jsonMode) {
4290
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
4291
+ return;
4292
+ }
4293
+ renderProductsList(result, rc.stdout);
4294
+ }
4002
4295
  async function runModelsList(argv, ctx, rc) {
4003
4296
  const parsed = parseArgs(argv, {
4004
4297
  string: [...COMMON_RICH_STRING_FLAGS, "category", "tier", "type"],