@clickraft/cli 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -285,6 +285,55 @@ var EXIT_CODE_MAP = {
285
285
  retryable: false,
286
286
  hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
287
287
  },
288
+ // --- 8 brand model resolution codes -----------------------------------------
289
+ BRAND_MODEL_NOT_FOUND: {
290
+ exitCode: 1,
291
+ category: "runtime",
292
+ retryable: false,
293
+ hint: "No brand model with that ID. Run `clickraft brand-model list`."
294
+ },
295
+ BRAND_MODEL_NOT_A_MODEL: {
296
+ exitCode: 1,
297
+ category: "runtime",
298
+ retryable: false,
299
+ hint: "The specified asset is not a brand model."
300
+ },
301
+ BRAND_MODEL_NOT_ACTIVE: {
302
+ exitCode: 1,
303
+ category: "runtime",
304
+ retryable: false,
305
+ hint: "Brand model is not active. Check its status in the dashboard."
306
+ },
307
+ BRAND_MODEL_NO_REFERENCES: {
308
+ exitCode: 1,
309
+ category: "runtime",
310
+ retryable: false,
311
+ hint: "Brand model has no reference images. Upload references first."
312
+ },
313
+ BRAND_MODEL_POSE_NOT_FOUND: {
314
+ exitCode: 1,
315
+ category: "runtime",
316
+ retryable: false,
317
+ hint: "Requested pose not found. Valid: front, 3/4-right, right, left, 3/4-left, back, approved."
318
+ },
319
+ BRAND_MODEL_DUPLICATE_ID: {
320
+ exitCode: 2,
321
+ category: "usage",
322
+ retryable: false,
323
+ hint: "Same brand model ID appears more than once."
324
+ },
325
+ MODEL_DOES_NOT_SUPPORT_REFERENCES: {
326
+ exitCode: 2,
327
+ category: "usage",
328
+ retryable: false,
329
+ hint: "This model does not support brand model references."
330
+ },
331
+ REFERENCE_LIMIT_EXCEEDED: {
332
+ exitCode: 2,
333
+ category: "usage",
334
+ retryable: false,
335
+ hint: "Too many reference images. Check model limits."
336
+ },
288
337
  // --- 4 CLI-synthetic codes --------------------------------------------------
289
338
  NETWORK_ERROR: {
290
339
  exitCode: 6,
@@ -697,7 +746,8 @@ function apiErrorFromServerBody(args) {
697
746
  requestId: error.correlationId ?? args.requestId,
698
747
  retryable,
699
748
  retryAfterMs,
700
- redactedBody: args.redactedBody
749
+ redactedBody: args.redactedBody,
750
+ details: error.details
701
751
  });
702
752
  }
703
753
  function parseRetryAfterHeader(headerValue) {
@@ -721,6 +771,10 @@ function buildUrl(baseUrl, path, query) {
721
771
  if (query) {
722
772
  for (const [key, value] of Object.entries(query)) {
723
773
  if (value === void 0) continue;
774
+ if (Array.isArray(value)) {
775
+ for (const item of value) url.searchParams.append(key, item);
776
+ continue;
777
+ }
724
778
  url.searchParams.append(key, String(value));
725
779
  }
726
780
  }
@@ -822,7 +876,8 @@ function errorToEnvelopePayload(err) {
822
876
  message: err.message,
823
877
  retryable: err.meta.retryable,
824
878
  retryAfterMs: err.meta.retryAfterMs,
825
- requestId: err.meta.requestId
879
+ requestId: err.meta.requestId,
880
+ details: err.meta.details
826
881
  });
827
882
  }
828
883
  if (err instanceof AuthError || err instanceof CredentialsError) {
@@ -851,6 +906,7 @@ function buildPayload(args) {
851
906
  if (typeof args.retryAfterMs === "number") payload.retry_after_ms = args.retryAfterMs;
852
907
  if (args.requestId) payload.request_id = args.requestId;
853
908
  if (mapping?.hint) payload.hint = mapping.hint;
909
+ if (args.details != null) payload.details = args.details;
854
910
  return payload;
855
911
  }
856
912
 
@@ -1732,7 +1788,15 @@ function buildRequestBody(args) {
1732
1788
  if (args.aspectRatio !== void 0) input.aspectRatio = args.aspectRatio;
1733
1789
  if (args.resolution !== void 0) input.resolution = args.resolution;
1734
1790
  if (args.durationSeconds !== void 0) input.durationSeconds = args.durationSeconds;
1735
- if (args.brandModel !== void 0) input.providerParams = { brandModelId: args.brandModel };
1791
+ if (args.brandModels !== void 0 && args.brandModels.length > 0) {
1792
+ input.providerParams = {
1793
+ brandModels: args.brandModels.map((spec) => {
1794
+ const entry = { id: spec.id };
1795
+ if (spec.imageType) entry.imageType = spec.imageType;
1796
+ return entry;
1797
+ })
1798
+ };
1799
+ }
1736
1800
  const body = {
1737
1801
  modelSlug: args.modelSlug,
1738
1802
  input
@@ -1769,6 +1833,152 @@ function projectCreateToResult(response, modelSlug) {
1769
1833
  };
1770
1834
  }
1771
1835
 
1836
+ // src/cli/args.ts
1837
+ var ArgError = class extends Error {
1838
+ constructor(message) {
1839
+ super(message);
1840
+ this.name = "ArgError";
1841
+ }
1842
+ };
1843
+ function parseArgs(argv, spec) {
1844
+ const result = {
1845
+ positional: [],
1846
+ string: {},
1847
+ boolean: {},
1848
+ array: {}
1849
+ };
1850
+ const stringFlags = new Set(spec.string ?? []);
1851
+ const booleanFlags = new Set(spec.boolean ?? []);
1852
+ const arrayFlags = new Set(spec.array ?? []);
1853
+ const aliases = spec.aliases ?? {};
1854
+ let i = 0;
1855
+ let stopFlags = false;
1856
+ while (i < argv.length) {
1857
+ const raw = argv[i];
1858
+ if (raw === void 0) {
1859
+ i += 1;
1860
+ continue;
1861
+ }
1862
+ if (stopFlags) {
1863
+ result.positional.push(raw);
1864
+ i += 1;
1865
+ continue;
1866
+ }
1867
+ if (raw === "--") {
1868
+ stopFlags = true;
1869
+ i += 1;
1870
+ continue;
1871
+ }
1872
+ if (!raw.startsWith("--")) {
1873
+ result.positional.push(raw);
1874
+ i += 1;
1875
+ continue;
1876
+ }
1877
+ const eqIdx = raw.indexOf("=");
1878
+ const flagToken = eqIdx >= 0 ? raw.slice(2, eqIdx) : raw.slice(2);
1879
+ const inlineValue = eqIdx >= 0 ? raw.slice(eqIdx + 1) : void 0;
1880
+ const canonical = resolveCanonical(flagToken, { booleanFlags, aliases });
1881
+ if (booleanFlags.has(canonical)) {
1882
+ if (inlineValue !== void 0) {
1883
+ result.boolean[canonical] = parseBool(inlineValue);
1884
+ } else if (flagToken.startsWith("no-") && booleanFlags.has(flagToken.slice(3))) {
1885
+ result.boolean[flagToken.slice(3)] = false;
1886
+ } else {
1887
+ result.boolean[canonical] = true;
1888
+ }
1889
+ i += 1;
1890
+ continue;
1891
+ }
1892
+ if (stringFlags.has(canonical) || arrayFlags.has(canonical)) {
1893
+ let value;
1894
+ if (inlineValue !== void 0) {
1895
+ value = inlineValue;
1896
+ } else {
1897
+ const next = argv[i + 1];
1898
+ if (next === void 0 || next.startsWith("--")) {
1899
+ throw new ArgError(`Flag --${canonical} requires a value.`);
1900
+ }
1901
+ value = next;
1902
+ i += 1;
1903
+ }
1904
+ if (arrayFlags.has(canonical)) {
1905
+ const existing = result.array[canonical] ?? [];
1906
+ existing.push(value);
1907
+ result.array[canonical] = existing;
1908
+ } else {
1909
+ result.string[canonical] = value;
1910
+ }
1911
+ i += 1;
1912
+ continue;
1913
+ }
1914
+ throw new ArgError(`Unknown flag --${flagToken}`);
1915
+ }
1916
+ return result;
1917
+ }
1918
+ function resolveCanonical(flagToken, ctx) {
1919
+ if (ctx.aliases[flagToken]) return ctx.aliases[flagToken];
1920
+ if (flagToken.startsWith("no-") && ctx.booleanFlags.has(flagToken.slice(3))) {
1921
+ return flagToken.slice(3);
1922
+ }
1923
+ return flagToken;
1924
+ }
1925
+ function parseBool(value) {
1926
+ if (value === "true" || value === "1") return true;
1927
+ if (value === "false" || value === "0") return false;
1928
+ throw new ArgError(`Expected boolean value (true/false), got "${value}".`);
1929
+ }
1930
+
1931
+ // src/commands/generate/parse-brand-model-spec.ts
1932
+ var VALID_POSES = [
1933
+ "front",
1934
+ "3/4-right",
1935
+ "right",
1936
+ "left",
1937
+ "3/4-left",
1938
+ "back",
1939
+ "approved"
1940
+ ];
1941
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1942
+ function parseBrandModelSpec(raw) {
1943
+ if (!raw) throw new ArgError("--brand-model value cannot be empty.");
1944
+ const colonIdx = raw.indexOf(":");
1945
+ const uuid = colonIdx >= 0 ? raw.slice(0, colonIdx) : raw;
1946
+ const poseRaw = colonIdx >= 0 ? raw.slice(colonIdx + 1) : void 0;
1947
+ if (!UUID_RE.test(uuid)) {
1948
+ throw new ArgError(
1949
+ `Invalid brand model ID: '${uuid}'. Expected a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`
1950
+ );
1951
+ }
1952
+ if (poseRaw !== void 0) {
1953
+ if (!poseRaw) {
1954
+ throw new ArgError(
1955
+ `Empty pose for brand model ${uuid}. Valid poses: ${VALID_POSES.join(", ")}.`
1956
+ );
1957
+ }
1958
+ if (!VALID_POSES.includes(poseRaw)) {
1959
+ throw new ArgError(
1960
+ `Unknown pose '${poseRaw}' for brand model ${uuid}. Valid poses: ${VALID_POSES.join(", ")}.`
1961
+ );
1962
+ }
1963
+ return { id: uuid.toLowerCase(), imageType: poseRaw };
1964
+ }
1965
+ return { id: uuid.toLowerCase() };
1966
+ }
1967
+ function validateBrandModelSpecs(specs) {
1968
+ if (specs.length > 3) {
1969
+ throw new ArgError(`Too many --brand-model flags (${specs.length}). Maximum is 3.`);
1970
+ }
1971
+ const seen = /* @__PURE__ */ new Set();
1972
+ for (const spec of specs) {
1973
+ if (seen.has(spec.id)) {
1974
+ throw new ArgError(
1975
+ `Duplicate brand model ID: ${spec.id}. Each --brand-model must reference a different model.`
1976
+ );
1977
+ }
1978
+ seen.add(spec.id);
1979
+ }
1980
+ }
1981
+
1772
1982
  // src/auth/device-flow.ts
1773
1983
  import { setTimeout as delay } from "timers/promises";
1774
1984
  import { request } from "undici";
@@ -2305,21 +2515,112 @@ function inspectTelemetry(options = {}) {
2305
2515
  };
2306
2516
  }
2307
2517
 
2308
- // src/commands/tokens/list.ts
2518
+ // src/commands/template/internal.ts
2519
+ async function buildTemplateClient(opts) {
2520
+ const cfg = await loadConfig({
2521
+ flagProfile: opts.profile,
2522
+ flagApiBaseUrl: opts.apiBaseUrl,
2523
+ flagToken: opts.token
2524
+ });
2525
+ return new ApiClient({
2526
+ apiBaseUrl: cfg.apiBaseUrl,
2527
+ accessToken: cfg.accessToken,
2528
+ signal: opts.signal,
2529
+ cloudflareAccess: cfg.cloudflareAccess
2530
+ });
2531
+ }
2532
+
2533
+ // src/schemas/templates.ts
2309
2534
  import { z as z8 } from "zod";
2310
- var TokenRowSchema = z8.object({
2535
+ var DeclaredParamSchema = z8.object({
2536
+ name: z8.string(),
2537
+ type: z8.string(),
2538
+ required: z8.boolean(),
2539
+ defaultValue: z8.string().nullable().optional(),
2540
+ enumValues: z8.array(z8.string()).nullable().optional(),
2541
+ description: z8.string().nullable().optional()
2542
+ }).strict();
2543
+ var TemplateSummarySchema = z8.object({
2311
2544
  id: z8.string().uuid(),
2312
- organizationId: z8.string().uuid(),
2313
- userId: z8.string().uuid(),
2314
- clientId: z8.string().nullable(),
2315
- tokenPrefix: z8.string(),
2316
- name: z8.string().nullable(),
2317
- scopes: z8.array(z8.string()),
2318
- lastUsedAt: z8.string().nullable(),
2319
- expiresAt: z8.string(),
2545
+ source: z8.enum(["own", "system"]),
2546
+ name: z8.string(),
2547
+ description: z8.string().nullable(),
2548
+ category: z8.string().nullable(),
2549
+ thumbnailUrl: z8.string().nullable(),
2550
+ tags: z8.array(z8.string()),
2320
2551
  createdAt: z8.string()
2321
2552
  }).strict();
2322
- var TokensListResponseSchema = z8.array(TokenRowSchema);
2553
+ var TemplateDetailSchema = TemplateSummarySchema.extend({
2554
+ declaredParams: z8.array(DeclaredParamSchema),
2555
+ timesUsed: z8.number()
2556
+ }).strict();
2557
+ var TemplatesListResponseSchema = z8.object({
2558
+ templates: z8.array(TemplateSummarySchema),
2559
+ cursor: z8.string().nullable(),
2560
+ hasMore: z8.boolean()
2561
+ }).strict();
2562
+
2563
+ // src/commands/template/list.ts
2564
+ async function listTemplates(opts) {
2565
+ const client = opts.client ?? await buildTemplateClient(opts);
2566
+ const data = await client.request({
2567
+ method: "GET",
2568
+ path: "/templates",
2569
+ query: {
2570
+ q: opts.q,
2571
+ tags: opts.tags,
2572
+ scope: opts.scope,
2573
+ cursor: opts.cursor,
2574
+ limit: opts.limit
2575
+ },
2576
+ responseSchema: TemplatesListResponseSchema,
2577
+ signal: opts.signal
2578
+ });
2579
+ return data;
2580
+ }
2581
+
2582
+ // src/commands/template/find.ts
2583
+ async function findTemplates(opts) {
2584
+ return listTemplates({
2585
+ q: opts.query,
2586
+ tags: opts.tags,
2587
+ scope: opts.scope,
2588
+ limit: opts.limit,
2589
+ cursor: opts.cursor,
2590
+ profile: opts.profile,
2591
+ apiBaseUrl: opts.apiBaseUrl,
2592
+ token: opts.token,
2593
+ signal: opts.signal,
2594
+ client: opts.client
2595
+ });
2596
+ }
2597
+
2598
+ // src/commands/template/get.ts
2599
+ async function getTemplate(opts) {
2600
+ const client = opts.client ?? await buildTemplateClient(opts);
2601
+ return client.request({
2602
+ method: "GET",
2603
+ path: `/templates/${encodeURIComponent(opts.id)}`,
2604
+ responseSchema: TemplateDetailSchema,
2605
+ signal: opts.signal
2606
+ });
2607
+ }
2608
+
2609
+ // src/commands/tokens/list.ts
2610
+ import { z as z9 } from "zod";
2611
+ var TokenRowSchema = z9.object({
2612
+ id: z9.string().uuid(),
2613
+ organizationId: z9.string().uuid(),
2614
+ userId: z9.string().uuid(),
2615
+ clientId: z9.string().nullable(),
2616
+ tokenPrefix: z9.string(),
2617
+ name: z9.string().nullable(),
2618
+ scopes: z9.array(z9.string()),
2619
+ lastUsedAt: z9.string().nullable(),
2620
+ expiresAt: z9.string(),
2621
+ createdAt: z9.string()
2622
+ }).strict();
2623
+ var TokensListResponseSchema = z9.array(TokenRowSchema);
2323
2624
  async function listTokens(options) {
2324
2625
  const config = await loadConfig({
2325
2626
  flagProfile: options.profile,
@@ -2348,23 +2649,33 @@ async function listTokens(options) {
2348
2649
  var FALLBACK_HINT = "See `clickraft --help` or contact support.";
2349
2650
  function formatHumanError(err) {
2350
2651
  const view = errorToView(err);
2351
- return [
2652
+ const lines = [
2352
2653
  `Error: ${view.message}`,
2353
2654
  `Code: ${view.code}`,
2354
2655
  `Try: ${view.hint}`,
2355
2656
  `Request ID: ${view.requestId ?? "n/a"}`
2356
- ].join("\n");
2657
+ ];
2658
+ if (view.details) lines.push(`Details: ${view.details}`);
2659
+ return lines.join("\n");
2357
2660
  }
2358
2661
  function formatJsonError(err, ctx) {
2359
2662
  return JSON.stringify(wrapError(err, ctx));
2360
2663
  }
2664
+ function formatDetails(details) {
2665
+ if (details == null) return void 0;
2666
+ if (typeof details !== "object") return String(details);
2667
+ const entries = Object.entries(details);
2668
+ if (entries.length === 0) return void 0;
2669
+ return entries.map(([k, v]) => `${k}=${v}`).join(", ");
2670
+ }
2361
2671
  function errorToView(err) {
2362
2672
  if (err instanceof ApiError) {
2363
2673
  return {
2364
2674
  code: toEnvelopeCode(err.code),
2365
2675
  message: err.message,
2366
2676
  hint: EXIT_CODE_MAP[err.code]?.hint ?? FALLBACK_HINT,
2367
- requestId: err.meta.requestId
2677
+ requestId: err.meta.requestId,
2678
+ details: formatDetails(err.meta.details)
2368
2679
  };
2369
2680
  }
2370
2681
  if (err instanceof AuthError || err instanceof CredentialsError) {
@@ -2382,101 +2693,6 @@ function errorToView(err) {
2382
2693
  };
2383
2694
  }
2384
2695
 
2385
- // src/cli/args.ts
2386
- var ArgError = class extends Error {
2387
- constructor(message) {
2388
- super(message);
2389
- this.name = "ArgError";
2390
- }
2391
- };
2392
- function parseArgs(argv, spec) {
2393
- const result = {
2394
- positional: [],
2395
- string: {},
2396
- boolean: {},
2397
- array: {}
2398
- };
2399
- const stringFlags = new Set(spec.string ?? []);
2400
- const booleanFlags = new Set(spec.boolean ?? []);
2401
- const arrayFlags = new Set(spec.array ?? []);
2402
- const aliases = spec.aliases ?? {};
2403
- let i = 0;
2404
- let stopFlags = false;
2405
- while (i < argv.length) {
2406
- const raw = argv[i];
2407
- if (raw === void 0) {
2408
- i += 1;
2409
- continue;
2410
- }
2411
- if (stopFlags) {
2412
- result.positional.push(raw);
2413
- i += 1;
2414
- continue;
2415
- }
2416
- if (raw === "--") {
2417
- stopFlags = true;
2418
- i += 1;
2419
- continue;
2420
- }
2421
- if (!raw.startsWith("--")) {
2422
- result.positional.push(raw);
2423
- i += 1;
2424
- continue;
2425
- }
2426
- const eqIdx = raw.indexOf("=");
2427
- const flagToken = eqIdx >= 0 ? raw.slice(2, eqIdx) : raw.slice(2);
2428
- const inlineValue = eqIdx >= 0 ? raw.slice(eqIdx + 1) : void 0;
2429
- const canonical = resolveCanonical(flagToken, { booleanFlags, aliases });
2430
- if (booleanFlags.has(canonical)) {
2431
- if (inlineValue !== void 0) {
2432
- result.boolean[canonical] = parseBool(inlineValue);
2433
- } else if (flagToken.startsWith("no-") && booleanFlags.has(flagToken.slice(3))) {
2434
- result.boolean[flagToken.slice(3)] = false;
2435
- } else {
2436
- result.boolean[canonical] = true;
2437
- }
2438
- i += 1;
2439
- continue;
2440
- }
2441
- if (stringFlags.has(canonical) || arrayFlags.has(canonical)) {
2442
- let value;
2443
- if (inlineValue !== void 0) {
2444
- value = inlineValue;
2445
- } else {
2446
- const next = argv[i + 1];
2447
- if (next === void 0 || next.startsWith("--")) {
2448
- throw new ArgError(`Flag --${canonical} requires a value.`);
2449
- }
2450
- value = next;
2451
- i += 1;
2452
- }
2453
- if (arrayFlags.has(canonical)) {
2454
- const existing = result.array[canonical] ?? [];
2455
- existing.push(value);
2456
- result.array[canonical] = existing;
2457
- } else {
2458
- result.string[canonical] = value;
2459
- }
2460
- i += 1;
2461
- continue;
2462
- }
2463
- throw new ArgError(`Unknown flag --${flagToken}`);
2464
- }
2465
- return result;
2466
- }
2467
- function resolveCanonical(flagToken, ctx) {
2468
- if (ctx.aliases[flagToken]) return ctx.aliases[flagToken];
2469
- if (flagToken.startsWith("no-") && ctx.booleanFlags.has(flagToken.slice(3))) {
2470
- return flagToken.slice(3);
2471
- }
2472
- return flagToken;
2473
- }
2474
- function parseBool(value) {
2475
- if (value === "true" || value === "1") return true;
2476
- if (value === "false" || value === "0") return false;
2477
- throw new ArgError(`Expected boolean value (true/false), got "${value}".`);
2478
- }
2479
-
2480
2696
  // src/cli/parse-args.ts
2481
2697
  var SHORT_ALIASES = {
2482
2698
  h: "help",
@@ -2717,6 +2933,61 @@ function renderBrandModelsList(models, out) {
2717
2933
  out.write(padRow(headers) + "\n");
2718
2934
  for (const cells of data) out.write(padRow(cells) + "\n");
2719
2935
  }
2936
+ function renderTemplatesList(result, out) {
2937
+ const { templates } = result;
2938
+ if (templates.length === 0) {
2939
+ out.write("No templates.\n");
2940
+ return;
2941
+ }
2942
+ const MAX_TAGS = 3;
2943
+ const headers = ["SOURCE", "NAME", "CATEGORY", "TAGS", "CREATED"];
2944
+ const data = templates.map((t) => {
2945
+ const shown = t.tags.slice(0, MAX_TAGS).join(", ");
2946
+ const extra = t.tags.length > MAX_TAGS ? ` +${t.tags.length - MAX_TAGS} more` : "";
2947
+ return [
2948
+ t.source,
2949
+ t.name,
2950
+ t.category ?? "\u2014",
2951
+ t.tags.length > 0 ? shown + extra : "\u2014",
2952
+ t.createdAt.slice(0, 10)
2953
+ ];
2954
+ });
2955
+ const widths = headers.map(
2956
+ (header, i) => Math.max(header.length, ...data.map((cells) => cells[i].length))
2957
+ );
2958
+ const padRow = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
2959
+ out.write(padRow(headers) + "\n");
2960
+ for (const cells of data) out.write(padRow(cells) + "\n");
2961
+ if (result.hasMore) {
2962
+ out.write("\nMore results available. Use --cursor to fetch the next page.\n");
2963
+ }
2964
+ }
2965
+ function renderTemplateDetail(detail, out) {
2966
+ const pad = 17;
2967
+ const lines = [
2968
+ detail.name,
2969
+ `${"ID:".padEnd(pad)}${detail.id}`,
2970
+ `${"Source:".padEnd(pad)}${detail.source}`,
2971
+ `${"Category:".padEnd(pad)}${detail.category ?? "\u2014"}`,
2972
+ `${"Tags:".padEnd(pad)}${detail.tags.length > 0 ? detail.tags.join(", ") : "\u2014"}`,
2973
+ `${"Created:".padEnd(pad)}${detail.createdAt.slice(0, 10)}`,
2974
+ `${"Times used:".padEnd(pad)}${detail.timesUsed}`
2975
+ ];
2976
+ if (detail.description) {
2977
+ lines.push("", detail.description);
2978
+ }
2979
+ lines.push("", "Declared parameters:");
2980
+ if (detail.declaredParams.length === 0) {
2981
+ lines.push(" (none)");
2982
+ } else {
2983
+ for (const p of detail.declaredParams) {
2984
+ const req = p.required ? "required" : "optional";
2985
+ const desc = p.description ? `: ${p.description}` : "";
2986
+ lines.push(` - ${p.name} (${p.type}, ${req})${desc}`);
2987
+ }
2988
+ }
2989
+ out.write(lines.join("\n") + "\n");
2990
+ }
2720
2991
  function formatExpiresAt(iso) {
2721
2992
  const ms = Date.parse(iso);
2722
2993
  if (Number.isNaN(ms)) return iso;
@@ -2944,6 +3215,9 @@ function renderRootHelp(version) {
2944
3215
  " balance Show credit balance and rate limits",
2945
3216
  " brand-model list List available brand models",
2946
3217
  " telemetry inspect Show current telemetry state and payload shape",
3218
+ " template list List available templates",
3219
+ " template get Fetch a template by ID",
3220
+ " template find Search templates by keyword",
2947
3221
  " generate create Submit a generation job and wait for completion",
2948
3222
  " generate wait Wait for an existing job to reach a terminal state",
2949
3223
  " generate get Fetch the current state of a job",
@@ -3027,6 +3301,62 @@ function renderCommandHelp(command) {
3027
3301
  " --json Emit JSON envelope instead of human output",
3028
3302
  ""
3029
3303
  ].join("\n");
3304
+ case "template":
3305
+ return [
3306
+ "Usage: clickraft template <subcommand> [options]",
3307
+ "",
3308
+ "Subcommands:",
3309
+ " list List available templates",
3310
+ " get Fetch a template by ID",
3311
+ " find Search templates by keyword",
3312
+ ""
3313
+ ].join("\n");
3314
+ case "template list":
3315
+ return [
3316
+ "Usage: clickraft template list [options]",
3317
+ "",
3318
+ "Options:",
3319
+ " --scope <scope> Filter by scope: own, system, or all (default: all)",
3320
+ " --q <query> Free-text search query",
3321
+ " --tags <tag> Filter by tag (repeatable)",
3322
+ " --cursor <token> Pagination cursor from a previous response",
3323
+ " --limit <n> Max results per page (1-50, default: 20)",
3324
+ " --profile <name>",
3325
+ " --api-base-url <url>",
3326
+ " --token <token>",
3327
+ " --json",
3328
+ ""
3329
+ ].join("\n");
3330
+ case "template get":
3331
+ return [
3332
+ "Usage: clickraft template get <id> [options]",
3333
+ "",
3334
+ "Fetches a single template by its ID, including declared parameters.",
3335
+ "",
3336
+ "Options:",
3337
+ " --profile <name>",
3338
+ " --api-base-url <url>",
3339
+ " --token <token>",
3340
+ " --json",
3341
+ ""
3342
+ ].join("\n");
3343
+ case "template find":
3344
+ return [
3345
+ "Usage: clickraft template find <query> [options]",
3346
+ "",
3347
+ "Searches templates by keyword. Shorthand for `template list --q <query>`.",
3348
+ "",
3349
+ "Options:",
3350
+ " --tags <tag> Filter by tag (repeatable)",
3351
+ " --scope <scope> Filter by scope: own, system, or all (default: all)",
3352
+ " --limit <n> Max results per page (1-50, default: 20)",
3353
+ " --cursor <token> Pagination cursor from a previous response",
3354
+ " --profile <name>",
3355
+ " --api-base-url <url>",
3356
+ " --token <token>",
3357
+ " --json",
3358
+ ""
3359
+ ].join("\n");
3030
3360
  case "generate":
3031
3361
  return [
3032
3362
  "Usage: clickraft generate <subcommand> [options]",
@@ -3047,7 +3377,9 @@ function renderCommandHelp(command) {
3047
3377
  "Options:",
3048
3378
  " --prompt <text> Text prompt (or pass as the positional argument)",
3049
3379
  " --model-slug <slug> Required model identifier",
3050
- " --brand-model <id> Optional Brand Model reference",
3380
+ " --brand-model <spec> Brand Model reference (repeatable, max 3)",
3381
+ " Format: <uuid> or <uuid>:<pose>",
3382
+ " Poses: front, 3/4-right, right, left, 3/4-left, back, approved",
3051
3383
  " --reference-image <v> URL or local path (repeatable, max 8)",
3052
3384
  " --aspect-ratio <a:b> e.g. 16:9",
3053
3385
  " --resolution <WxH> e.g. 1024x768",
@@ -3148,6 +3480,14 @@ function matchNewCommand(argv) {
3148
3480
  if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "jobs" };
3149
3481
  return { kind: "unknown-sub", verb: "jobs", sub };
3150
3482
  }
3483
+ if (verb === "template") {
3484
+ const sub = argv[1];
3485
+ if (sub === "list") return { kind: "cmd", path: "template.list", verbTokenCount: 2 };
3486
+ if (sub === "get") return { kind: "cmd", path: "template.get", verbTokenCount: 2 };
3487
+ if (sub === "find") return { kind: "cmd", path: "template.find", verbTokenCount: 2 };
3488
+ if (sub === void 0 || sub.startsWith("-")) return { kind: "sub-help", verb: "template" };
3489
+ return { kind: "unknown-sub", verb: "template", sub };
3490
+ }
3151
3491
  return null;
3152
3492
  }
3153
3493
  async function runNewCommand(newCmd, options, stdout, stderr) {
@@ -3198,6 +3538,12 @@ async function runNewCommand(newCmd, options, stdout, stderr) {
3198
3538
  return await runJobsWait(argvAfterVerb, ctx, rc);
3199
3539
  case "upload":
3200
3540
  return await runUploadCmd(argvAfterVerb, ctx, rc);
3541
+ case "template.list":
3542
+ return await runTemplateList(argvAfterVerb, ctx, rc);
3543
+ case "template.get":
3544
+ return await runTemplateGet(argvAfterVerb, ctx, rc);
3545
+ case "template.find":
3546
+ return await runTemplateFind(argvAfterVerb, ctx, rc);
3201
3547
  }
3202
3548
  } catch (err) {
3203
3549
  if (isAbortedError(err, options.signal)) {
@@ -3228,7 +3574,6 @@ async function runGenerateCreate(argv, ctx, rc) {
3228
3574
  ...COMMON_RICH_STRING_FLAGS,
3229
3575
  "prompt",
3230
3576
  "model-slug",
3231
- "brand-model",
3232
3577
  "aspect-ratio",
3233
3578
  "resolution",
3234
3579
  "duration-seconds",
@@ -3237,10 +3582,13 @@ async function runGenerateCreate(argv, ctx, rc) {
3237
3582
  "timeout"
3238
3583
  ],
3239
3584
  boolean: [...COMMON_RICH_BOOL_FLAGS, "wait", "async"],
3240
- array: ["reference-image"]
3585
+ array: ["reference-image", "brand-model"]
3241
3586
  });
3242
3587
  const modelSlug = parsed.string["model-slug"];
3243
3588
  if (!modelSlug) throw new ArgError("--model-slug is required.");
3589
+ const rawBrandModels = parsed.array["brand-model"] ?? [];
3590
+ const brandModels = rawBrandModels.length > 0 ? rawBrandModels.map(parseBrandModelSpec) : void 0;
3591
+ if (brandModels) validateBrandModelSpecs(brandModels);
3244
3592
  const result = await generateCreate({
3245
3593
  prompt: parsed.string.prompt ?? parsed.positional[0],
3246
3594
  modelSlug,
@@ -3248,7 +3596,7 @@ async function runGenerateCreate(argv, ctx, rc) {
3248
3596
  aspectRatio: parsed.string["aspect-ratio"],
3249
3597
  resolution: parsed.string.resolution,
3250
3598
  durationSeconds: optionalInt(parsed.string["duration-seconds"], "duration-seconds"),
3251
- brandModel: parsed.string["brand-model"],
3599
+ brandModels,
3252
3600
  workflowId: parsed.string["workflow-id"],
3253
3601
  nodeId: parsed.string["node-id"],
3254
3602
  wait: computeWaitFlag(parsed),
@@ -3392,6 +3740,87 @@ function exitCodeForResult(result) {
3392
3740
  }
3393
3741
  return 0;
3394
3742
  }
3743
+ async function runTemplateList(argv, ctx, rc) {
3744
+ const parsed = parseArgs(argv, {
3745
+ string: [...COMMON_RICH_STRING_FLAGS, "scope", "q", "cursor", "limit"],
3746
+ boolean: [...COMMON_RICH_BOOL_FLAGS],
3747
+ array: ["tags"]
3748
+ });
3749
+ const result = await listTemplates({
3750
+ q: parsed.string.q,
3751
+ tags: parsed.array.tags,
3752
+ scope: parsed.string.scope,
3753
+ cursor: parsed.string.cursor,
3754
+ limit: optionalInt(parsed.string.limit, "limit"),
3755
+ profile: parsed.string.profile,
3756
+ apiBaseUrl: parsed.string["api-base-url"],
3757
+ token: parsed.string.token,
3758
+ signal: rc.signal
3759
+ });
3760
+ ctx.nextCursor = result.cursor ?? void 0;
3761
+ emitTemplateList(result, ctx, rc);
3762
+ return 0;
3763
+ }
3764
+ async function runTemplateGet(argv, ctx, rc) {
3765
+ const parsed = parseArgs(argv, {
3766
+ string: [...COMMON_RICH_STRING_FLAGS],
3767
+ boolean: [...COMMON_RICH_BOOL_FLAGS]
3768
+ });
3769
+ const id = parsed.positional[0];
3770
+ if (!id) throw new ArgError("Missing required <id> argument.");
3771
+ const detail = await getTemplate({
3772
+ id,
3773
+ profile: parsed.string.profile,
3774
+ apiBaseUrl: parsed.string["api-base-url"],
3775
+ token: parsed.string.token,
3776
+ signal: rc.signal
3777
+ });
3778
+ emitTemplateDetail(detail, ctx, rc);
3779
+ return 0;
3780
+ }
3781
+ async function runTemplateFind(argv, ctx, rc) {
3782
+ const parsed = parseArgs(argv, {
3783
+ string: [...COMMON_RICH_STRING_FLAGS, "scope", "cursor", "limit"],
3784
+ boolean: [...COMMON_RICH_BOOL_FLAGS],
3785
+ array: ["tags"]
3786
+ });
3787
+ const query = parsed.positional[0];
3788
+ if (!query) throw new ArgError("Missing required <query> argument.");
3789
+ const result = await findTemplates({
3790
+ query,
3791
+ tags: parsed.array.tags,
3792
+ scope: parsed.string.scope,
3793
+ cursor: parsed.string.cursor,
3794
+ limit: optionalInt(parsed.string.limit, "limit"),
3795
+ profile: parsed.string.profile,
3796
+ apiBaseUrl: parsed.string["api-base-url"],
3797
+ token: parsed.string.token,
3798
+ signal: rc.signal
3799
+ });
3800
+ ctx.nextCursor = result.cursor ?? void 0;
3801
+ emitTemplateList(result, ctx, rc);
3802
+ return 0;
3803
+ }
3804
+ function emitTemplateList(result, ctx, rc) {
3805
+ if (rc.signal?.aborted) {
3806
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
3807
+ }
3808
+ if (rc.jsonMode) {
3809
+ rc.stdout.write(JSON.stringify(wrapSuccess(result, ctx)) + "\n");
3810
+ return;
3811
+ }
3812
+ renderTemplatesList(result, rc.stdout);
3813
+ }
3814
+ function emitTemplateDetail(detail, ctx, rc) {
3815
+ if (rc.signal?.aborted) {
3816
+ throw new AuthError("AUTH_FLOW_CANCELLED", "Cancelled.");
3817
+ }
3818
+ if (rc.jsonMode) {
3819
+ rc.stdout.write(JSON.stringify(wrapSuccess(detail, ctx)) + "\n");
3820
+ return;
3821
+ }
3822
+ renderTemplateDetail(detail, rc.stdout);
3823
+ }
3395
3824
 
3396
3825
  // src/index.ts
3397
3826
  var __filename2 = fileURLToPath(import.meta.url);