@clickraft/cli 0.8.4 → 0.8.6

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/cli.js CHANGED
@@ -10,6 +10,10 @@ import { z } from "zod";
10
10
  import { z as z2 } from "zod";
11
11
  import { z as z3 } from "zod";
12
12
  import { z as z4 } from "zod";
13
+ import { request as undiciRequest } from "undici";
14
+ import { z as z6 } from "zod";
15
+ import { createHash, randomUUID } from "crypto";
16
+ import { z as z5 } from "zod";
13
17
  var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
14
18
  var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
15
19
  function redactTokenLikeFields(input) {
@@ -202,432 +206,6 @@ function composeIterationSignal(userSignal, remainingMs) {
202
206
  const deadlineSignal = AbortSignal.timeout(Math.max(remainingMs, 0));
203
207
  return userSignal ? AbortSignal.any([userSignal, deadlineSignal]) : deadlineSignal;
204
208
  }
205
-
206
- // src/api/client.ts
207
- import { request as undiciRequest } from "undici";
208
- import { z as z6 } from "zod";
209
-
210
- // src/errors/codes.ts
211
- var LOGIN_HINT = "Run `clickraft login` to authenticate.";
212
- var RELOGIN_HINT = "Run `clickraft login` to refresh.";
213
- var EXIT_CODE_MAP = {
214
- // --- 37 Phase 8 Q7 codes ----------------------------------------------------
215
- AUTH_TOKEN_MISSING: {
216
- exitCode: 3,
217
- category: "auth",
218
- retryable: false,
219
- hint: "Set `CLICKRAFT_TOKEN` or run `clickraft login`."
220
- },
221
- AUTH_TOKEN_INVALID: {
222
- exitCode: 3,
223
- category: "auth",
224
- retryable: false,
225
- hint: "The token is malformed or unknown. Run `clickraft login`."
226
- },
227
- AUTH_TOKEN_EXPIRED: {
228
- exitCode: 3,
229
- category: "auth",
230
- retryable: false,
231
- hint: RELOGIN_HINT
232
- },
233
- AUTH_TOKEN_REVOKED: {
234
- exitCode: 3,
235
- category: "auth",
236
- retryable: false,
237
- hint: "Token was revoked. Run `clickraft login` to mint a new one."
238
- },
239
- AUTH_TOKEN_SCOPE_INSUFFICIENT: {
240
- exitCode: 4,
241
- category: "permission",
242
- retryable: false,
243
- hint: "This token does not have the required scope. Re-run `clickraft login` selecting the needed scopes."
244
- },
245
- AUTH_ORG_MISMATCH: {
246
- exitCode: 4,
247
- category: "permission",
248
- retryable: false,
249
- hint: "This token's org no longer exists or you have been removed."
250
- },
251
- AUTH_DEVICE_CODE_PENDING: {
252
- exitCode: 0,
253
- category: "success",
254
- retryable: false,
255
- hint: "Continue polling."
256
- },
257
- AUTH_DEVICE_CODE_EXPIRED: {
258
- exitCode: 3,
259
- category: "auth",
260
- retryable: false,
261
- hint: "Device code expired. Re-run `clickraft login`."
262
- },
263
- AUTH_DEVICE_CODE_SLOW_DOWN: {
264
- exitCode: 0,
265
- category: "success",
266
- retryable: false,
267
- hint: "Increase polling interval (Retry-After)."
268
- },
269
- IDEMPOTENCY_KEY_MISSING: {
270
- exitCode: 2,
271
- category: "usage",
272
- retryable: false,
273
- hint: "Add `X-Idempotency-Key` header (or `--idempotency-key`)."
274
- },
275
- IDEMPOTENCY_KEY_CONFLICT: {
276
- exitCode: 7,
277
- category: "conflict",
278
- retryable: false,
279
- hint: "Same key was used with a different request body. Use a new key."
280
- },
281
- PLAN_FEATURE_LOCKED: {
282
- exitCode: 4,
283
- category: "permission",
284
- retryable: false,
285
- hint: "API access is not enabled on your plan. Visit billing settings."
286
- },
287
- PLAN_QUOTA_EXCEEDED: {
288
- exitCode: 5,
289
- category: "quota",
290
- retryable: false,
291
- hint: "Daily API quota exhausted. Wait, or upgrade your plan."
292
- },
293
- INSUFFICIENT_CREDITS: {
294
- exitCode: 5,
295
- category: "quota",
296
- retryable: false,
297
- hint: "Top up credits to retry."
298
- },
299
- CREDIT_DEDUCTION_FAILED: {
300
- exitCode: 1,
301
- category: "runtime",
302
- retryable: true,
303
- hint: "Could not charge credits. Retry; no charge was made."
304
- },
305
- RATE_LIMITED: {
306
- exitCode: 5,
307
- category: "quota",
308
- retryable: true,
309
- hint: "Slow down. Honor Retry-After."
310
- },
311
- INPUT_INVALID_FORMAT: {
312
- exitCode: 2,
313
- category: "usage",
314
- retryable: false,
315
- hint: "Check input format and try again."
316
- },
317
- INPUT_TOO_LONG: {
318
- exitCode: 2,
319
- category: "usage",
320
- retryable: false,
321
- hint: "Field exceeded max length."
322
- },
323
- WORKFLOW_VALIDATION_FAILED: {
324
- exitCode: 2,
325
- category: "usage",
326
- retryable: false,
327
- hint: "Run `workflow validate` for the full list of errors."
328
- },
329
- WORKFLOW_NODE_TYPE_UNKNOWN: {
330
- exitCode: 2,
331
- category: "usage",
332
- retryable: false,
333
- hint: "Run `clickraft nodes list` for the current set of node types."
334
- },
335
- CANVAS_REV_MISMATCH: {
336
- exitCode: 7,
337
- category: "conflict",
338
- retryable: false,
339
- hint: "The workflow was updated elsewhere. Re-fetch and retry with the current canvas_rev."
340
- },
341
- CANVAS_REV_REQUIRED: {
342
- exitCode: 2,
343
- category: "usage",
344
- retryable: false,
345
- hint: "Include --canvas-rev or use --auto-rev."
346
- },
347
- WORKER_UNREACHABLE: {
348
- exitCode: 6,
349
- category: "network",
350
- retryable: true,
351
- hint: "Collaboration service temporarily unavailable. Retry."
352
- },
353
- IDEMPOTENCY_IN_PROGRESS: {
354
- exitCode: 7,
355
- category: "conflict",
356
- retryable: false,
357
- hint: "A request with this idempotency key is currently in-flight. Retry in a moment."
358
- },
359
- TOKEN_NAME_INVALID: {
360
- exitCode: 2,
361
- category: "usage",
362
- retryable: false,
363
- hint: "Agent token name is malformed."
364
- },
365
- WORKFLOW_NOT_FOUND: {
366
- exitCode: 2,
367
- category: "usage",
368
- retryable: false,
369
- hint: "No workflow with that ID in your org."
370
- },
371
- NOT_FOUND: {
372
- exitCode: 2,
373
- category: "usage",
374
- retryable: false,
375
- hint: "Resource not found."
376
- },
377
- NODE_TYPE_NOT_FOUND: {
378
- exitCode: 2,
379
- category: "usage",
380
- retryable: false,
381
- hint: "Run `clickraft nodes list` for the current set of node types."
382
- },
383
- FEATURE_DISABLED: {
384
- exitCode: 2,
385
- category: "usage",
386
- retryable: false,
387
- hint: "This endpoint is not enabled. Contact support if you believe this is in error."
388
- },
389
- FORBIDDEN: {
390
- exitCode: 4,
391
- category: "permission",
392
- retryable: false,
393
- hint: "Your role does not permit this action."
394
- },
395
- MODEL_NOT_FOUND: {
396
- exitCode: 2,
397
- category: "usage",
398
- retryable: false,
399
- hint: "Unknown `modelSlug`. Run `brand-model list` for the current set."
400
- },
401
- PROVIDER_UNAVAILABLE: {
402
- exitCode: 6,
403
- category: "network",
404
- retryable: true,
405
- hint: "Provider is temporarily unavailable. Retry shortly."
406
- },
407
- STORAGE_UPLOAD_FAILED: {
408
- exitCode: 1,
409
- category: "runtime",
410
- retryable: true,
411
- hint: "Upload failed mid-stream. Retry."
412
- },
413
- STORAGE_QUOTA_EXCEEDED: {
414
- exitCode: 5,
415
- category: "quota",
416
- retryable: false,
417
- hint: "Org storage quota exceeded. Free space or upgrade."
418
- },
419
- GEN_CONTENT_REFUSAL: {
420
- exitCode: 1,
421
- category: "runtime",
422
- retryable: false,
423
- hint: "The model declined the request via safety policy."
424
- },
425
- GEN_OUTPUT_SCHEMA_MISMATCH: {
426
- exitCode: 1,
427
- category: "runtime",
428
- retryable: true,
429
- hint: "Provider returned malformed output. Full refund issued. Retry."
430
- },
431
- GEN_EMPTY_OUTPUT: {
432
- exitCode: 1,
433
- category: "runtime",
434
- retryable: true,
435
- hint: "Provider returned no usable output. Full refund issued. Retry."
436
- },
437
- GEN_ITEM_COUNT_MISMATCH: {
438
- exitCode: 0,
439
- category: "success",
440
- retryable: false,
441
- hint: "Partial output kept; no refund."
442
- },
443
- AGENT_FINGERPRINT_REJECTED: {
444
- exitCode: 4,
445
- category: "permission",
446
- retryable: false,
447
- hint: "This client identification is not permitted. Contact support."
448
- },
449
- INTERNAL_ERROR: {
450
- exitCode: 1,
451
- category: "runtime",
452
- retryable: true,
453
- hint: "Server error. Retry; if it persists, contact support with the request ID."
454
- },
455
- CREDENTIALS_LOCKED: {
456
- exitCode: 1,
457
- category: "runtime",
458
- retryable: true,
459
- hint: "Another CLI invocation holds the credentials lock. Retry shortly."
460
- },
461
- CREDENTIALS_PERMISSIONS_LOOSE: {
462
- exitCode: 0,
463
- category: "success",
464
- retryable: false,
465
- hint: "Credentials file mode is wider than 0600; continuing."
466
- },
467
- // --- 5 additional codes introduced by 1A.1 ---------------------------------
468
- AUTH_ACCESS_DENIED: {
469
- exitCode: 3,
470
- category: "auth",
471
- retryable: false,
472
- hint: "You denied the OAuth grant. Re-run `clickraft login` to retry."
473
- },
474
- AUTH_FLOW_CANCELLED: {
475
- exitCode: 3,
476
- category: "auth",
477
- retryable: false,
478
- hint: "Authentication was cancelled. Re-run `clickraft login`."
479
- },
480
- CREDENTIALS_CORRUPT: {
481
- exitCode: 1,
482
- category: "runtime",
483
- retryable: false,
484
- hint: "Credentials file is unreadable. Remove it and run `clickraft login`."
485
- },
486
- CREDENTIALS_NOT_FOUND: {
487
- exitCode: 3,
488
- category: "auth",
489
- retryable: false,
490
- hint: LOGIN_HINT
491
- },
492
- CREDENTIALS_PROFILE_NOT_FOUND: {
493
- exitCode: 2,
494
- category: "usage",
495
- retryable: false,
496
- hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
497
- },
498
- // --- 8 brand model resolution codes -----------------------------------------
499
- BRAND_MODEL_NOT_FOUND: {
500
- exitCode: 1,
501
- category: "runtime",
502
- retryable: false,
503
- hint: "No brand model with that ID. Run `clickraft brand-model list`."
504
- },
505
- BRAND_MODEL_NOT_A_MODEL: {
506
- exitCode: 1,
507
- category: "runtime",
508
- retryable: false,
509
- hint: "The specified asset is not a brand model."
510
- },
511
- BRAND_MODEL_NOT_ACTIVE: {
512
- exitCode: 1,
513
- category: "runtime",
514
- retryable: false,
515
- hint: "Brand model is not active. Check its status in the dashboard."
516
- },
517
- BRAND_MODEL_NO_REFERENCES: {
518
- exitCode: 1,
519
- category: "runtime",
520
- retryable: false,
521
- hint: "Brand model has no reference images. Upload references first."
522
- },
523
- BRAND_MODEL_POSE_NOT_FOUND: {
524
- exitCode: 1,
525
- category: "runtime",
526
- retryable: false,
527
- hint: "Requested pose not found. Valid: front, 3/4-right, right, left, 3/4-left, back, approved."
528
- },
529
- BRAND_MODEL_DUPLICATE_ID: {
530
- exitCode: 2,
531
- category: "usage",
532
- retryable: false,
533
- hint: "Same brand model ID appears more than once."
534
- },
535
- MODEL_DOES_NOT_SUPPORT_REFERENCES: {
536
- exitCode: 2,
537
- category: "usage",
538
- retryable: false,
539
- hint: "This model does not support brand model references."
540
- },
541
- REFERENCE_LIMIT_EXCEEDED: {
542
- exitCode: 2,
543
- category: "usage",
544
- retryable: false,
545
- hint: "Too many reference images. Check model limits."
546
- },
547
- // --- 8 product reference resolver codes -------------------------------------
548
- PRODUCT_NOT_FOUND: {
549
- exitCode: 1,
550
- category: "runtime",
551
- retryable: false,
552
- hint: "No product with that ID in your org. Run `clickraft product list`."
553
- },
554
- PRODUCT_DUPLICATE_REFERENCE: {
555
- exitCode: 2,
556
- category: "usage",
557
- retryable: false,
558
- hint: "Same (productId, imageId) tuple appears twice in the request."
559
- },
560
- PRODUCT_IMAGE_NOT_FOUND: {
561
- exitCode: 1,
562
- category: "runtime",
563
- retryable: false,
564
- hint: "imageId does not exist in the product images."
565
- },
566
- PRODUCT_IMAGE_MISMATCH: {
567
- exitCode: 2,
568
- category: "usage",
569
- retryable: false,
570
- hint: "imageId belongs to a different product."
571
- },
572
- PRODUCT_SYNC_ERROR: {
573
- exitCode: 1,
574
- category: "runtime",
575
- retryable: false,
576
- hint: "Product sync status is 'error'. Re-sync or use a different product."
577
- },
578
- PRODUCT_DELETED_UPSTREAM: {
579
- exitCode: 1,
580
- category: "runtime",
581
- retryable: false,
582
- hint: "Product was deleted upstream. Remove it or re-sync."
583
- },
584
- PRODUCT_NO_PRIMARY_IMAGE: {
585
- exitCode: 1,
586
- category: "runtime",
587
- retryable: false,
588
- hint: "Product has no primary image. Specify --product <id>:<imageId> explicitly."
589
- },
590
- PRODUCT_SOURCE_MANAGED: {
591
- exitCode: 4,
592
- category: "permission",
593
- retryable: false,
594
- hint: "Product is managed by an external integration."
595
- },
596
- // --- 4 CLI-synthetic codes --------------------------------------------------
597
- NETWORK_ERROR: {
598
- exitCode: 6,
599
- category: "network",
600
- retryable: true,
601
- hint: "Network connection failed. Check connectivity and retry."
602
- },
603
- TIMEOUT: {
604
- exitCode: 6,
605
- category: "network",
606
- retryable: true,
607
- hint: "Request timed out. Retry; consider increasing the timeout."
608
- },
609
- PARSE_ERROR: {
610
- exitCode: 1,
611
- category: "runtime",
612
- retryable: false,
613
- hint: "Server returned an unexpected response. Contact support with the request ID."
614
- },
615
- SCHEMA_MISMATCH: {
616
- exitCode: 1,
617
- category: "runtime",
618
- retryable: false,
619
- hint: "Server response did not match the expected schema. Update the CLI."
620
- }
621
- };
622
- function getExitCode(code) {
623
- return EXIT_CODE_MAP[code]?.exitCode ?? 1;
624
- }
625
- function toEnvelopeCode(code) {
626
- return code.startsWith("E_") ? code : `E_${code}`;
627
- }
628
- var SIGINT_EXIT_CODE = 130;
629
-
630
- // src/errors/schema-mismatch.ts
631
209
  var MAX_ISSUES = 5;
632
210
  function formatSchemaIssues(error) {
633
211
  const issues = error.issues;
@@ -651,9 +229,6 @@ function formatPath(path) {
651
229
  if (path.length === 0) return "(root)";
652
230
  return path.map(String).join(".");
653
231
  }
654
-
655
- // src/api/idempotency.ts
656
- import { createHash, randomUUID } from "crypto";
657
232
  function generateIdempotencyKey() {
658
233
  return randomUUID();
659
234
  }
@@ -661,8 +236,6 @@ function generateUploadIdempotencyKey(content, scope) {
661
236
  const hash = createHash("sha256").update(content).digest("hex");
662
237
  return `upload-${scope}-${hash}`;
663
238
  }
664
-
665
- // src/api/retry.ts
666
239
  var DEFAULT_RETRY_OPTIONS = {
667
240
  maxAttempts: 3,
668
241
  baseDelayMs: 200,
@@ -676,7 +249,7 @@ function computeBackoffMs(attempt, baseDelayMs, maxDelayMs, jitterFraction) {
676
249
  return Math.max(0, Math.round(exp + jitter));
677
250
  }
678
251
  function decideRetry(params) {
679
- const { attempt, options, serverRetryAfterMs, isNetworkError, isRetryable } = params;
252
+ const { attempt, options, serverRetryAfterMs, isNetworkError, isRetryable, onWarning: onWarning2 } = params;
680
253
  if (options.signal?.aborted) {
681
254
  return { shouldRetry: false, delayMs: 0, reason: "aborted" };
682
255
  }
@@ -689,7 +262,7 @@ function decideRetry(params) {
689
262
  if (typeof serverRetryAfterMs === "number" && serverRetryAfterMs >= 0) {
690
263
  let delayMs2 = serverRetryAfterMs;
691
264
  if (delayMs2 > ABSOLUTE_MAX_RETRY_AFTER_MS) {
692
- console.warn(`Server Retry-After exceeded ${ABSOLUTE_MAX_RETRY_AFTER_MS}ms; clipping.`);
265
+ onWarning2?.(`Server Retry-After exceeded ${ABSOLUTE_MAX_RETRY_AFTER_MS}ms; clipping.`);
693
266
  delayMs2 = ABSOLUTE_MAX_RETRY_AFTER_MS;
694
267
  }
695
268
  return { shouldRetry: true, delayMs: delayMs2, reason: "rate_limited" };
@@ -729,9 +302,6 @@ function signalToAbortError(signal) {
729
302
  err.name = "AbortError";
730
303
  return err;
731
304
  }
732
-
733
- // src/api/types.ts
734
- import { z as z5 } from "zod";
735
305
  var ServerSuccessSchema = (payload) => z5.object({ data: payload }).strict();
736
306
  var ServerErrorBodySchema = z5.object({
737
307
  error: z5.object({
@@ -742,8 +312,6 @@ var ServerErrorBodySchema = z5.object({
742
312
  details: z5.unknown().optional()
743
313
  }).strict()
744
314
  }).strict();
745
-
746
- // src/api/client.ts
747
315
  var REDACT_BODY_LIMIT = 1024;
748
316
  var REDACT_INPUT_CAP = 100 * 1024;
749
317
  var DEFAULT_USER_AGENT = "clickraft-cli";
@@ -800,7 +368,8 @@ var ApiClient = class {
800
368
  options: { ...retry, signal },
801
369
  serverRetryAfterMs: err.meta.retryAfterMs,
802
370
  isNetworkError,
803
- isRetryable
371
+ isRetryable,
372
+ onWarning: this.options.onWarning
804
373
  });
805
374
  if (!decision.shouldRetry) throw err;
806
375
  try {
@@ -888,178 +457,654 @@ var ApiClient = class {
888
457
  bodyText,
889
458
  statusCode,
890
459
  requestId,
891
- retryAfterHeader: pickHeader(responseHeaders, "retry-after")
460
+ retryAfterHeader: pickHeader(responseHeaders, "retry-after"),
461
+ onWarning: this.options.onWarning,
462
+ isRetryableServerCode: this.options.isRetryableServerCode
892
463
  });
893
464
  }
894
465
  };
895
- function parseSuccessBody(args) {
896
- let parsed;
897
- try {
898
- parsed = JSON.parse(args.bodyText);
899
- } catch (err) {
900
- throw new ApiError(
901
- "PARSE_ERROR",
902
- "Server returned a non-JSON response.",
903
- {
904
- status: args.statusCode,
905
- requestId: args.requestId,
906
- retryable: false,
907
- redactedBody: redactAndTruncate(args.bodyText)
908
- },
909
- err
910
- );
911
- }
912
- const envelope = ServerSuccessSchema(args.responseSchema ?? z6.unknown()).safeParse(parsed);
913
- if (!envelope.success) {
914
- const issues = formatSchemaIssues(envelope.error);
915
- throw new ApiError(
916
- "SCHEMA_MISMATCH",
917
- `Server response did not match the expected schema. Issues: ${issues}`,
918
- {
919
- status: args.statusCode,
920
- requestId: args.requestId,
921
- retryable: false,
922
- redactedBody: redactAndTruncate(args.bodyText)
923
- }
924
- );
925
- }
926
- return envelope.data.data;
927
- }
928
- function parseErrorBody(args) {
929
- const redactedBody = redactAndTruncate(args.bodyText);
930
- let parsed;
931
- try {
932
- parsed = JSON.parse(args.bodyText);
933
- } catch {
934
- return new ApiError(
935
- "PARSE_ERROR",
936
- `Server returned a ${args.statusCode} response with a non-JSON body.`,
937
- {
938
- status: args.statusCode,
939
- requestId: args.requestId,
940
- retryable: args.statusCode >= 500,
941
- redactedBody
942
- }
943
- );
944
- }
945
- const envelope = ServerErrorBodySchema.safeParse(parsed);
946
- if (!envelope.success) {
947
- return new ApiError(
948
- "PARSE_ERROR",
949
- `Server returned ${args.statusCode} but body did not match the error envelope.`,
950
- {
951
- status: args.statusCode,
952
- requestId: args.requestId,
953
- retryable: args.statusCode >= 500,
954
- redactedBody
955
- }
956
- );
957
- }
958
- return apiErrorFromServerBody({
959
- body: envelope.data,
960
- statusCode: args.statusCode,
961
- requestId: args.requestId,
962
- retryAfterHeader: args.retryAfterHeader,
963
- redactedBody
964
- });
965
- }
966
- function apiErrorFromServerBody(args) {
967
- const { error } = args.body;
968
- let code;
969
- let safeMessage;
970
- if (KNOWN_SERVER_CODES.has(error.code)) {
971
- code = error.code;
972
- safeMessage = redactTokenLikeFields(error.message);
973
- } else {
974
- const redactedRawCode = redactTokenLikeFields(error.code);
975
- console.warn(`Unknown server error code: ${redactedRawCode}. Treating as INTERNAL_ERROR.`);
976
- code = "INTERNAL_ERROR";
977
- safeMessage = `Unknown server error code: ${redactedRawCode}.`;
978
- }
979
- const mapping = EXIT_CODE_MAP[code];
980
- const retryable = mapping?.retryable ?? (args.statusCode >= 500 || args.statusCode === 429);
981
- const retryAfterMs = typeof error.retryAfter === "number" ? error.retryAfter * 1e3 : parseRetryAfterHeader(args.retryAfterHeader);
982
- return new ApiError(code, safeMessage, {
983
- status: args.statusCode,
984
- requestId: error.correlationId ?? args.requestId,
985
- retryable,
986
- retryAfterMs,
987
- redactedBody: args.redactedBody,
988
- details: error.details
989
- });
990
- }
991
- function parseRetryAfterHeader(headerValue) {
992
- if (!headerValue) return void 0;
993
- const trimmed = headerValue.trim();
994
- if (/^\d+$/.test(trimmed)) {
995
- return Number.parseInt(trimmed, 10) * 1e3;
996
- }
997
- if (/[A-Za-z]/.test(trimmed)) {
998
- const parsed = Date.parse(trimmed);
999
- if (!Number.isNaN(parsed)) {
1000
- return Math.max(0, parsed - Date.now());
1001
- }
1002
- }
1003
- return void 0;
1004
- }
1005
- function buildUrl(baseUrl, path, query) {
1006
- const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1007
- const relative = path.startsWith("/") ? path.slice(1) : path;
1008
- const url = new URL(relative, base);
1009
- if (query) {
1010
- for (const [key, value] of Object.entries(query)) {
1011
- if (value === void 0) continue;
1012
- if (Array.isArray(value)) {
1013
- for (const item of value) url.searchParams.append(key, item);
1014
- continue;
1015
- }
1016
- url.searchParams.append(key, String(value));
1017
- }
1018
- }
1019
- return url.toString();
1020
- }
1021
- function serializeBody(opts) {
1022
- if (opts.body === void 0) return void 0;
1023
- if (opts.method === "GET") return void 0;
1024
- return JSON.stringify(opts.body);
1025
- }
1026
- function composeSignals(signals) {
1027
- const present = signals.filter((s) => s !== void 0);
1028
- if (present.length === 0) return new AbortController().signal;
1029
- if (present.length === 1) return present[0];
1030
- const any = AbortSignal.any;
1031
- if (typeof any === "function") return any(present);
1032
- const ctrl = new AbortController();
1033
- for (const s of present) {
1034
- if (s.aborted) {
1035
- ctrl.abort(s.reason);
1036
- break;
1037
- }
1038
- s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
1039
- }
1040
- return ctrl.signal;
1041
- }
1042
- function pickHeader(headers, name) {
1043
- const value = headers[name] ?? headers[name.toLowerCase()];
1044
- if (Array.isArray(value)) return value[0];
1045
- return value;
1046
- }
1047
- function redactAndTruncate(body) {
1048
- const input = body.length > REDACT_INPUT_CAP ? body.slice(0, REDACT_INPUT_CAP) : body;
1049
- const redacted = redactTokenLikeFields(input);
1050
- return redacted.length > REDACT_BODY_LIMIT ? `${redacted.slice(0, REDACT_BODY_LIMIT)}\u2026` : redacted;
1051
- }
1052
- function isAbortError(err) {
1053
- if (!(err instanceof Error)) return false;
1054
- return err.name === "AbortError" || /aborted/i.test(err.message);
466
+ function createClient(options) {
467
+ return new ApiClient(options);
468
+ }
469
+ function parseSuccessBody(args) {
470
+ let parsed;
471
+ try {
472
+ parsed = JSON.parse(args.bodyText);
473
+ } catch (err) {
474
+ throw new ApiError(
475
+ "PARSE_ERROR",
476
+ "Server returned a non-JSON response.",
477
+ {
478
+ status: args.statusCode,
479
+ requestId: args.requestId,
480
+ retryable: false,
481
+ redactedBody: redactAndTruncate(args.bodyText)
482
+ },
483
+ err
484
+ );
485
+ }
486
+ const envelope = ServerSuccessSchema(args.responseSchema ?? z6.unknown()).safeParse(parsed);
487
+ if (!envelope.success) {
488
+ const issues = formatSchemaIssues(envelope.error);
489
+ throw new ApiError(
490
+ "SCHEMA_MISMATCH",
491
+ `Server response did not match the expected schema. Issues: ${issues}`,
492
+ {
493
+ status: args.statusCode,
494
+ requestId: args.requestId,
495
+ retryable: false,
496
+ redactedBody: redactAndTruncate(args.bodyText)
497
+ }
498
+ );
499
+ }
500
+ return envelope.data.data;
501
+ }
502
+ function parseErrorBody(args) {
503
+ const redactedBody = redactAndTruncate(args.bodyText);
504
+ let parsed;
505
+ try {
506
+ parsed = JSON.parse(args.bodyText);
507
+ } catch {
508
+ return new ApiError(
509
+ "PARSE_ERROR",
510
+ `Server returned a ${args.statusCode} response with a non-JSON body.`,
511
+ {
512
+ status: args.statusCode,
513
+ requestId: args.requestId,
514
+ retryable: args.statusCode >= 500,
515
+ redactedBody
516
+ }
517
+ );
518
+ }
519
+ const envelope = ServerErrorBodySchema.safeParse(parsed);
520
+ if (!envelope.success) {
521
+ return new ApiError(
522
+ "PARSE_ERROR",
523
+ `Server returned ${args.statusCode} but body did not match the error envelope.`,
524
+ {
525
+ status: args.statusCode,
526
+ requestId: args.requestId,
527
+ retryable: args.statusCode >= 500,
528
+ redactedBody
529
+ }
530
+ );
531
+ }
532
+ return apiErrorFromServerBody({
533
+ body: envelope.data,
534
+ statusCode: args.statusCode,
535
+ requestId: args.requestId,
536
+ retryAfterHeader: args.retryAfterHeader,
537
+ redactedBody,
538
+ onWarning: args.onWarning,
539
+ isRetryableServerCode: args.isRetryableServerCode
540
+ });
541
+ }
542
+ function apiErrorFromServerBody(args) {
543
+ const { error } = args.body;
544
+ let code;
545
+ let safeMessage;
546
+ if (KNOWN_SERVER_CODES.has(error.code)) {
547
+ code = error.code;
548
+ safeMessage = redactTokenLikeFields(error.message);
549
+ } else {
550
+ const redactedRawCode = redactTokenLikeFields(error.code);
551
+ args.onWarning?.(`Unknown server error code: ${redactedRawCode}. Treating as INTERNAL_ERROR.`);
552
+ code = "INTERNAL_ERROR";
553
+ safeMessage = `Unknown server error code: ${redactedRawCode}.`;
554
+ }
555
+ const retryable = args.isRetryableServerCode?.(code) ?? (args.statusCode >= 500 || args.statusCode === 429);
556
+ const retryAfterMs = typeof error.retryAfter === "number" ? error.retryAfter * 1e3 : parseRetryAfterHeader(args.retryAfterHeader);
557
+ return new ApiError(code, safeMessage, {
558
+ status: args.statusCode,
559
+ requestId: error.correlationId ?? args.requestId,
560
+ retryable,
561
+ retryAfterMs,
562
+ redactedBody: args.redactedBody,
563
+ details: error.details
564
+ });
565
+ }
566
+ function parseRetryAfterHeader(headerValue) {
567
+ if (!headerValue) return void 0;
568
+ const trimmed = headerValue.trim();
569
+ if (/^\d+$/.test(trimmed)) {
570
+ return Number.parseInt(trimmed, 10) * 1e3;
571
+ }
572
+ if (/[A-Za-z]/.test(trimmed)) {
573
+ const parsed = Date.parse(trimmed);
574
+ if (!Number.isNaN(parsed)) {
575
+ return Math.max(0, parsed - Date.now());
576
+ }
577
+ }
578
+ return void 0;
579
+ }
580
+ function buildUrl(baseUrl, path, query) {
581
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
582
+ const relative = path.startsWith("/") ? path.slice(1) : path;
583
+ const url = new URL(relative, base);
584
+ if (query) {
585
+ for (const [key, value] of Object.entries(query)) {
586
+ if (value === void 0) continue;
587
+ if (Array.isArray(value)) {
588
+ for (const item of value) url.searchParams.append(key, item);
589
+ continue;
590
+ }
591
+ url.searchParams.append(key, String(value));
592
+ }
593
+ }
594
+ return url.toString();
595
+ }
596
+ function serializeBody(opts) {
597
+ if (opts.body === void 0) return void 0;
598
+ if (opts.method === "GET") return void 0;
599
+ return JSON.stringify(opts.body);
600
+ }
601
+ function composeSignals(signals) {
602
+ const present = signals.filter((s) => s !== void 0);
603
+ if (present.length === 0) return new AbortController().signal;
604
+ if (present.length === 1) return present[0];
605
+ const any = AbortSignal.any;
606
+ if (typeof any === "function") return any(present);
607
+ const ctrl = new AbortController();
608
+ for (const s of present) {
609
+ if (s.aborted) {
610
+ ctrl.abort(s.reason);
611
+ break;
612
+ }
613
+ s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
614
+ }
615
+ return ctrl.signal;
616
+ }
617
+ function pickHeader(headers, name) {
618
+ const value = headers[name] ?? headers[name.toLowerCase()];
619
+ if (Array.isArray(value)) return value[0];
620
+ return value;
621
+ }
622
+ function redactAndTruncate(body) {
623
+ const input = body.length > REDACT_INPUT_CAP ? body.slice(0, REDACT_INPUT_CAP) : body;
624
+ const redacted = redactTokenLikeFields(input);
625
+ return redacted.length > REDACT_BODY_LIMIT ? `${redacted.slice(0, REDACT_BODY_LIMIT)}\u2026` : redacted;
626
+ }
627
+ function isAbortError(err) {
628
+ if (!(err instanceof Error)) return false;
629
+ return err.name === "AbortError" || /aborted/i.test(err.message);
630
+ }
631
+ function toAbortApiError(signal, cause) {
632
+ const reasonMessage = signal.reason instanceof Error ? signal.reason.message : "Request was aborted.";
633
+ return new ApiError("TIMEOUT", reasonMessage, { retryable: true }, cause);
634
+ }
635
+ function errorMessage(err) {
636
+ if (err instanceof Error) return err.message;
637
+ return String(err);
638
+ }
639
+ async function applyWorkflowWithRetry(params) {
640
+ try {
641
+ return await patch(params);
642
+ } catch (err) {
643
+ if (!(err instanceof ApiError)) throw err;
644
+ const isRevConflict = err.code === "CANVAS_REV_MISMATCH" || err.meta.status === 409 && err.meta.details != null;
645
+ if (!isRevConflict || !params.autoRev) throw err;
646
+ const snapshot = await params.client.request({
647
+ method: "GET",
648
+ path: `/workflows/${encodeURIComponent(params.id)}`,
649
+ responseSchema: params.schemas.detail,
650
+ signal: params.signal
651
+ });
652
+ const retryResult = await patch({
653
+ ...params,
654
+ canvasRev: snapshot.canvas_rev,
655
+ // Fresh key — the body changed (different canvas_rev), so reusing the
656
+ // caller's key would trigger IDEMPOTENCY_KEY_CONFLICT.
657
+ idempotencyKey: void 0
658
+ });
659
+ return { ...retryResult, retriedWithRev: snapshot.canvas_rev };
660
+ }
661
+ }
662
+ async function patch(params) {
663
+ const data = await params.client.request({
664
+ method: "PATCH",
665
+ path: `/workflows/${encodeURIComponent(params.id)}`,
666
+ body: {
667
+ canvas_rev: params.canvasRev,
668
+ operations: params.operations
669
+ },
670
+ idempotencyKey: params.idempotencyKey,
671
+ responseSchema: params.schemas.mutate,
672
+ signal: params.signal
673
+ });
674
+ return {
675
+ canvasRev: data.canvas_rev,
676
+ applied: data.applied,
677
+ workflow: data.workflow
678
+ };
679
+ }
680
+
681
+ // src/errors/codes.ts
682
+ var LOGIN_HINT = "Run `clickraft login` to authenticate.";
683
+ var RELOGIN_HINT = "Run `clickraft login` to refresh.";
684
+ var EXIT_CODE_MAP = {
685
+ // --- 37 Phase 8 Q7 codes ----------------------------------------------------
686
+ AUTH_TOKEN_MISSING: {
687
+ exitCode: 3,
688
+ category: "auth",
689
+ retryable: false,
690
+ hint: "Set `CLICKRAFT_TOKEN` or run `clickraft login`."
691
+ },
692
+ AUTH_TOKEN_INVALID: {
693
+ exitCode: 3,
694
+ category: "auth",
695
+ retryable: false,
696
+ hint: "The token is malformed or unknown. Run `clickraft login`."
697
+ },
698
+ AUTH_TOKEN_EXPIRED: {
699
+ exitCode: 3,
700
+ category: "auth",
701
+ retryable: false,
702
+ hint: RELOGIN_HINT
703
+ },
704
+ AUTH_TOKEN_REVOKED: {
705
+ exitCode: 3,
706
+ category: "auth",
707
+ retryable: false,
708
+ hint: "Token was revoked. Run `clickraft login` to mint a new one."
709
+ },
710
+ AUTH_TOKEN_SCOPE_INSUFFICIENT: {
711
+ exitCode: 4,
712
+ category: "permission",
713
+ retryable: false,
714
+ hint: "This token does not have the required scope. Re-run `clickraft login` selecting the needed scopes."
715
+ },
716
+ AUTH_ORG_MISMATCH: {
717
+ exitCode: 4,
718
+ category: "permission",
719
+ retryable: false,
720
+ hint: "This token's org no longer exists or you have been removed."
721
+ },
722
+ AUTH_DEVICE_CODE_PENDING: {
723
+ exitCode: 0,
724
+ category: "success",
725
+ retryable: false,
726
+ hint: "Continue polling."
727
+ },
728
+ AUTH_DEVICE_CODE_EXPIRED: {
729
+ exitCode: 3,
730
+ category: "auth",
731
+ retryable: false,
732
+ hint: "Device code expired. Re-run `clickraft login`."
733
+ },
734
+ AUTH_DEVICE_CODE_SLOW_DOWN: {
735
+ exitCode: 0,
736
+ category: "success",
737
+ retryable: false,
738
+ hint: "Increase polling interval (Retry-After)."
739
+ },
740
+ IDEMPOTENCY_KEY_MISSING: {
741
+ exitCode: 2,
742
+ category: "usage",
743
+ retryable: false,
744
+ hint: "Add `X-Idempotency-Key` header (or `--idempotency-key`)."
745
+ },
746
+ IDEMPOTENCY_KEY_CONFLICT: {
747
+ exitCode: 7,
748
+ category: "conflict",
749
+ retryable: false,
750
+ hint: "Same key was used with a different request body. Use a new key."
751
+ },
752
+ PLAN_FEATURE_LOCKED: {
753
+ exitCode: 4,
754
+ category: "permission",
755
+ retryable: false,
756
+ hint: "API access is not enabled on your plan. Visit billing settings."
757
+ },
758
+ PLAN_QUOTA_EXCEEDED: {
759
+ exitCode: 5,
760
+ category: "quota",
761
+ retryable: false,
762
+ hint: "Daily API quota exhausted. Wait, or upgrade your plan."
763
+ },
764
+ INSUFFICIENT_CREDITS: {
765
+ exitCode: 5,
766
+ category: "quota",
767
+ retryable: false,
768
+ hint: "Top up credits to retry."
769
+ },
770
+ CREDIT_DEDUCTION_FAILED: {
771
+ exitCode: 1,
772
+ category: "runtime",
773
+ retryable: true,
774
+ hint: "Could not charge credits. Retry; no charge was made."
775
+ },
776
+ RATE_LIMITED: {
777
+ exitCode: 5,
778
+ category: "quota",
779
+ retryable: true,
780
+ hint: "Slow down. Honor Retry-After."
781
+ },
782
+ INPUT_INVALID_FORMAT: {
783
+ exitCode: 2,
784
+ category: "usage",
785
+ retryable: false,
786
+ hint: "Check input format and try again."
787
+ },
788
+ INPUT_TOO_LONG: {
789
+ exitCode: 2,
790
+ category: "usage",
791
+ retryable: false,
792
+ hint: "Field exceeded max length."
793
+ },
794
+ WORKFLOW_VALIDATION_FAILED: {
795
+ exitCode: 2,
796
+ category: "usage",
797
+ retryable: false,
798
+ hint: "Run `workflow validate` for the full list of errors."
799
+ },
800
+ WORKFLOW_NODE_TYPE_UNKNOWN: {
801
+ exitCode: 2,
802
+ category: "usage",
803
+ retryable: false,
804
+ hint: "Run `clickraft nodes list` for the current set of node types."
805
+ },
806
+ CANVAS_REV_MISMATCH: {
807
+ exitCode: 7,
808
+ category: "conflict",
809
+ retryable: false,
810
+ hint: "The workflow was updated elsewhere. Re-fetch and retry with the current canvas_rev."
811
+ },
812
+ CANVAS_REV_REQUIRED: {
813
+ exitCode: 2,
814
+ category: "usage",
815
+ retryable: false,
816
+ hint: "Include --canvas-rev or use --auto-rev."
817
+ },
818
+ WORKER_UNREACHABLE: {
819
+ exitCode: 6,
820
+ category: "network",
821
+ retryable: true,
822
+ hint: "Collaboration service temporarily unavailable. Retry."
823
+ },
824
+ IDEMPOTENCY_IN_PROGRESS: {
825
+ exitCode: 7,
826
+ category: "conflict",
827
+ retryable: false,
828
+ hint: "A request with this idempotency key is currently in-flight. Retry in a moment."
829
+ },
830
+ TOKEN_NAME_INVALID: {
831
+ exitCode: 2,
832
+ category: "usage",
833
+ retryable: false,
834
+ hint: "Agent token name is malformed."
835
+ },
836
+ WORKFLOW_NOT_FOUND: {
837
+ exitCode: 2,
838
+ category: "usage",
839
+ retryable: false,
840
+ hint: "No workflow with that ID in your org."
841
+ },
842
+ NOT_FOUND: {
843
+ exitCode: 2,
844
+ category: "usage",
845
+ retryable: false,
846
+ hint: "Resource not found."
847
+ },
848
+ NODE_TYPE_NOT_FOUND: {
849
+ exitCode: 2,
850
+ category: "usage",
851
+ retryable: false,
852
+ hint: "Run `clickraft nodes list` for the current set of node types."
853
+ },
854
+ FEATURE_DISABLED: {
855
+ exitCode: 2,
856
+ category: "usage",
857
+ retryable: false,
858
+ hint: "This endpoint is not enabled. Contact support if you believe this is in error."
859
+ },
860
+ FORBIDDEN: {
861
+ exitCode: 4,
862
+ category: "permission",
863
+ retryable: false,
864
+ hint: "Your role does not permit this action."
865
+ },
866
+ MODEL_NOT_FOUND: {
867
+ exitCode: 2,
868
+ category: "usage",
869
+ retryable: false,
870
+ hint: "Unknown `modelSlug`. Run `brand-model list` for the current set."
871
+ },
872
+ PROVIDER_UNAVAILABLE: {
873
+ exitCode: 6,
874
+ category: "network",
875
+ retryable: true,
876
+ hint: "Provider is temporarily unavailable. Retry shortly."
877
+ },
878
+ STORAGE_UPLOAD_FAILED: {
879
+ exitCode: 1,
880
+ category: "runtime",
881
+ retryable: true,
882
+ hint: "Upload failed mid-stream. Retry."
883
+ },
884
+ STORAGE_QUOTA_EXCEEDED: {
885
+ exitCode: 5,
886
+ category: "quota",
887
+ retryable: false,
888
+ hint: "Org storage quota exceeded. Free space or upgrade."
889
+ },
890
+ GEN_CONTENT_REFUSAL: {
891
+ exitCode: 1,
892
+ category: "runtime",
893
+ retryable: false,
894
+ hint: "The model declined the request via safety policy."
895
+ },
896
+ GEN_OUTPUT_SCHEMA_MISMATCH: {
897
+ exitCode: 1,
898
+ category: "runtime",
899
+ retryable: true,
900
+ hint: "Provider returned malformed output. Full refund issued. Retry."
901
+ },
902
+ GEN_EMPTY_OUTPUT: {
903
+ exitCode: 1,
904
+ category: "runtime",
905
+ retryable: true,
906
+ hint: "Provider returned no usable output. Full refund issued. Retry."
907
+ },
908
+ GEN_ITEM_COUNT_MISMATCH: {
909
+ exitCode: 0,
910
+ category: "success",
911
+ retryable: false,
912
+ hint: "Partial output kept; no refund."
913
+ },
914
+ AGENT_FINGERPRINT_REJECTED: {
915
+ exitCode: 4,
916
+ category: "permission",
917
+ retryable: false,
918
+ hint: "This client identification is not permitted. Contact support."
919
+ },
920
+ INTERNAL_ERROR: {
921
+ exitCode: 1,
922
+ category: "runtime",
923
+ retryable: true,
924
+ hint: "Server error. Retry; if it persists, contact support with the request ID."
925
+ },
926
+ CREDENTIALS_LOCKED: {
927
+ exitCode: 1,
928
+ category: "runtime",
929
+ retryable: true,
930
+ hint: "Another CLI invocation holds the credentials lock. Retry shortly."
931
+ },
932
+ CREDENTIALS_PERMISSIONS_LOOSE: {
933
+ exitCode: 0,
934
+ category: "success",
935
+ retryable: false,
936
+ hint: "Credentials file mode is wider than 0600; continuing."
937
+ },
938
+ // --- 5 additional codes introduced by 1A.1 ---------------------------------
939
+ AUTH_ACCESS_DENIED: {
940
+ exitCode: 3,
941
+ category: "auth",
942
+ retryable: false,
943
+ hint: "You denied the OAuth grant. Re-run `clickraft login` to retry."
944
+ },
945
+ AUTH_FLOW_CANCELLED: {
946
+ exitCode: 3,
947
+ category: "auth",
948
+ retryable: false,
949
+ hint: "Authentication was cancelled. Re-run `clickraft login`."
950
+ },
951
+ CREDENTIALS_CORRUPT: {
952
+ exitCode: 1,
953
+ category: "runtime",
954
+ retryable: false,
955
+ hint: "Credentials file is unreadable. Remove it and run `clickraft login`."
956
+ },
957
+ CREDENTIALS_NOT_FOUND: {
958
+ exitCode: 3,
959
+ category: "auth",
960
+ retryable: false,
961
+ hint: LOGIN_HINT
962
+ },
963
+ CREDENTIALS_PROFILE_NOT_FOUND: {
964
+ exitCode: 2,
965
+ category: "usage",
966
+ retryable: false,
967
+ hint: "No such profile. Use `clickraft auth profiles` to list available profiles."
968
+ },
969
+ // --- 8 brand model resolution codes -----------------------------------------
970
+ BRAND_MODEL_NOT_FOUND: {
971
+ exitCode: 1,
972
+ category: "runtime",
973
+ retryable: false,
974
+ hint: "No brand model with that ID. Run `clickraft brand-model list`."
975
+ },
976
+ BRAND_MODEL_NOT_A_MODEL: {
977
+ exitCode: 1,
978
+ category: "runtime",
979
+ retryable: false,
980
+ hint: "The specified asset is not a brand model."
981
+ },
982
+ BRAND_MODEL_NOT_ACTIVE: {
983
+ exitCode: 1,
984
+ category: "runtime",
985
+ retryable: false,
986
+ hint: "Brand model is not active. Check its status in the dashboard."
987
+ },
988
+ BRAND_MODEL_NO_REFERENCES: {
989
+ exitCode: 1,
990
+ category: "runtime",
991
+ retryable: false,
992
+ hint: "Brand model has no reference images. Upload references first."
993
+ },
994
+ BRAND_MODEL_POSE_NOT_FOUND: {
995
+ exitCode: 1,
996
+ category: "runtime",
997
+ retryable: false,
998
+ hint: "Requested pose not found. Valid: front, 3/4-right, right, left, 3/4-left, back, approved."
999
+ },
1000
+ BRAND_MODEL_DUPLICATE_ID: {
1001
+ exitCode: 2,
1002
+ category: "usage",
1003
+ retryable: false,
1004
+ hint: "Same brand model ID appears more than once."
1005
+ },
1006
+ MODEL_DOES_NOT_SUPPORT_REFERENCES: {
1007
+ exitCode: 2,
1008
+ category: "usage",
1009
+ retryable: false,
1010
+ hint: "This model does not support brand model references."
1011
+ },
1012
+ REFERENCE_LIMIT_EXCEEDED: {
1013
+ exitCode: 2,
1014
+ category: "usage",
1015
+ retryable: false,
1016
+ hint: "Too many reference images. Check model limits."
1017
+ },
1018
+ // --- 8 product reference resolver codes -------------------------------------
1019
+ PRODUCT_NOT_FOUND: {
1020
+ exitCode: 1,
1021
+ category: "runtime",
1022
+ retryable: false,
1023
+ hint: "No product with that ID in your org. Run `clickraft product list`."
1024
+ },
1025
+ PRODUCT_DUPLICATE_REFERENCE: {
1026
+ exitCode: 2,
1027
+ category: "usage",
1028
+ retryable: false,
1029
+ hint: "Same (productId, imageId) tuple appears twice in the request."
1030
+ },
1031
+ PRODUCT_IMAGE_NOT_FOUND: {
1032
+ exitCode: 1,
1033
+ category: "runtime",
1034
+ retryable: false,
1035
+ hint: "imageId does not exist in the product images."
1036
+ },
1037
+ PRODUCT_IMAGE_MISMATCH: {
1038
+ exitCode: 2,
1039
+ category: "usage",
1040
+ retryable: false,
1041
+ hint: "imageId belongs to a different product."
1042
+ },
1043
+ PRODUCT_SYNC_ERROR: {
1044
+ exitCode: 1,
1045
+ category: "runtime",
1046
+ retryable: false,
1047
+ hint: "Product sync status is 'error'. Re-sync or use a different product."
1048
+ },
1049
+ PRODUCT_DELETED_UPSTREAM: {
1050
+ exitCode: 1,
1051
+ category: "runtime",
1052
+ retryable: false,
1053
+ hint: "Product was deleted upstream. Remove it or re-sync."
1054
+ },
1055
+ PRODUCT_NO_PRIMARY_IMAGE: {
1056
+ exitCode: 1,
1057
+ category: "runtime",
1058
+ retryable: false,
1059
+ hint: "Product has no primary image. Specify --product <id>:<imageId> explicitly."
1060
+ },
1061
+ PRODUCT_SOURCE_MANAGED: {
1062
+ exitCode: 4,
1063
+ category: "permission",
1064
+ retryable: false,
1065
+ hint: "Product is managed by an external integration."
1066
+ },
1067
+ // --- 4 CLI-synthetic codes --------------------------------------------------
1068
+ NETWORK_ERROR: {
1069
+ exitCode: 6,
1070
+ category: "network",
1071
+ retryable: true,
1072
+ hint: "Network connection failed. Check connectivity and retry."
1073
+ },
1074
+ TIMEOUT: {
1075
+ exitCode: 6,
1076
+ category: "network",
1077
+ retryable: true,
1078
+ hint: "Request timed out. Retry; consider increasing the timeout."
1079
+ },
1080
+ PARSE_ERROR: {
1081
+ exitCode: 1,
1082
+ category: "runtime",
1083
+ retryable: false,
1084
+ hint: "Server returned an unexpected response. Contact support with the request ID."
1085
+ },
1086
+ SCHEMA_MISMATCH: {
1087
+ exitCode: 1,
1088
+ category: "runtime",
1089
+ retryable: false,
1090
+ hint: "Server response did not match the expected schema. Update the CLI."
1091
+ }
1092
+ };
1093
+ function getExitCode(code) {
1094
+ return EXIT_CODE_MAP[code]?.exitCode ?? 1;
1055
1095
  }
1056
- function toAbortApiError(signal, cause) {
1057
- const reasonMessage = signal.reason instanceof Error ? signal.reason.message : "Request was aborted.";
1058
- return new ApiError("TIMEOUT", reasonMessage, { retryable: true }, cause);
1096
+ function toEnvelopeCode(code) {
1097
+ return code.startsWith("E_") ? code : `E_${code}`;
1059
1098
  }
1060
- function errorMessage(err) {
1061
- if (err instanceof Error) return err.message;
1062
- return String(err);
1099
+ var SIGINT_EXIT_CODE = 130;
1100
+
1101
+ // src/api/client.ts
1102
+ var isRetryableServerCode = (code) => EXIT_CODE_MAP[code].retryable;
1103
+ var onWarning = (msg) => {
1104
+ console.warn(msg);
1105
+ };
1106
+ function createApiClient(opts) {
1107
+ return createClient({ ...opts, onWarning, isRetryableServerCode });
1063
1108
  }
1064
1109
 
1065
1110
  // src/auth/errors.ts
@@ -1488,7 +1533,7 @@ async function getBalance(options) {
1488
1533
  if (!config.accessToken) {
1489
1534
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
1490
1535
  }
1491
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
1536
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
1492
1537
  apiBaseUrl,
1493
1538
  accessToken,
1494
1539
  signal: options.signal,
@@ -1514,7 +1559,7 @@ async function listBrandModels(options) {
1514
1559
  if (!config.accessToken) {
1515
1560
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
1516
1561
  }
1517
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
1562
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
1518
1563
  apiBaseUrl,
1519
1564
  accessToken,
1520
1565
  signal: options.signal,
@@ -1679,7 +1724,7 @@ async function buildClient(opts) {
1679
1724
  flagApiBaseUrl: opts.apiBaseUrl,
1680
1725
  flagToken: opts.token
1681
1726
  });
1682
- return new ApiClient({
1727
+ return createApiClient({
1683
1728
  apiBaseUrl: cfg.apiBaseUrl,
1684
1729
  accessToken: cfg.accessToken,
1685
1730
  signal: opts.signal,
@@ -1846,7 +1891,7 @@ async function buildGenerateClient(opts) {
1846
1891
  flagApiBaseUrl: opts.apiBaseUrl,
1847
1892
  flagToken: opts.token
1848
1893
  });
1849
- return new ApiClient({
1894
+ return createApiClient({
1850
1895
  apiBaseUrl: cfg.apiBaseUrl,
1851
1896
  accessToken: cfg.accessToken,
1852
1897
  signal: opts.signal,
@@ -2618,7 +2663,7 @@ async function logout(options) {
2618
2663
  let serverRevoked = false;
2619
2664
  let revokeFailureMessage;
2620
2665
  if (options.revoke) {
2621
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2666
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
2622
2667
  apiBaseUrl,
2623
2668
  accessToken,
2624
2669
  signal: options.signal,
@@ -2703,7 +2748,7 @@ async function buildModelClient(opts) {
2703
2748
  flagApiBaseUrl: opts.apiBaseUrl,
2704
2749
  flagToken: opts.token
2705
2750
  });
2706
- return new ApiClient({
2751
+ return createApiClient({
2707
2752
  apiBaseUrl: cfg.apiBaseUrl,
2708
2753
  accessToken: cfg.accessToken,
2709
2754
  signal: opts.signal,
@@ -2756,7 +2801,7 @@ async function buildNodesClient(opts) {
2756
2801
  flagApiBaseUrl: opts.apiBaseUrl,
2757
2802
  flagToken: opts.token
2758
2803
  });
2759
- return new ApiClient({
2804
+ return createApiClient({
2760
2805
  apiBaseUrl: cfg.apiBaseUrl,
2761
2806
  accessToken: cfg.accessToken,
2762
2807
  signal: opts.signal,
@@ -2800,7 +2845,7 @@ async function listProducts(options) {
2800
2845
  if (!config.accessToken) {
2801
2846
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2802
2847
  }
2803
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2848
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
2804
2849
  apiBaseUrl,
2805
2850
  accessToken,
2806
2851
  signal: options.signal,
@@ -2909,7 +2954,7 @@ async function buildTemplateClient(opts) {
2909
2954
  flagApiBaseUrl: opts.apiBaseUrl,
2910
2955
  flagToken: opts.token
2911
2956
  });
2912
- return new ApiClient({
2957
+ return createApiClient({
2913
2958
  apiBaseUrl: cfg.apiBaseUrl,
2914
2959
  accessToken: cfg.accessToken,
2915
2960
  signal: opts.signal,
@@ -3003,7 +3048,7 @@ async function listTokens(options) {
3003
3048
  if (!config.accessToken) {
3004
3049
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
3005
3050
  }
3006
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
3051
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => createApiClient({
3007
3052
  apiBaseUrl,
3008
3053
  accessToken,
3009
3054
  signal: options.signal,
@@ -3075,7 +3120,7 @@ async function buildWorkflowClient(opts) {
3075
3120
  flagApiBaseUrl: opts.apiBaseUrl,
3076
3121
  flagToken: opts.token
3077
3122
  });
3078
- return new ApiClient({
3123
+ return createApiClient({
3079
3124
  apiBaseUrl: cfg.apiBaseUrl,
3080
3125
  accessToken: cfg.accessToken,
3081
3126
  signal: opts.signal,
@@ -3083,54 +3128,19 @@ async function buildWorkflowClient(opts) {
3083
3128
  });
3084
3129
  }
3085
3130
 
3086
- // src/commands/workflow/get.ts
3087
- async function getWorkflow(opts) {
3088
- const client = opts.client ?? await buildWorkflowClient(opts);
3089
- return client.request({
3090
- method: "GET",
3091
- path: `/workflows/${encodeURIComponent(opts.id)}`,
3092
- responseSchema: WorkflowDetailSchema,
3093
- signal: opts.signal
3094
- });
3095
- }
3096
-
3097
3131
  // src/commands/workflow/apply.ts
3098
3132
  async function applyWorkflow(opts) {
3099
3133
  const client = opts.client ?? await buildWorkflowClient(opts);
3100
- try {
3101
- return await patchWorkflow(client, opts);
3102
- } catch (err) {
3103
- if (!(err instanceof ApiError)) throw err;
3104
- const isRevConflict = err.code === "CANVAS_REV_MISMATCH" || err.meta.status === 409 && err.meta.details != null;
3105
- if (!isRevConflict || !opts.autoRev) throw err;
3106
- const snapshot = await getWorkflow({ id: opts.id, client, signal: opts.signal });
3107
- const retryResult = await patchWorkflow(client, {
3108
- ...opts,
3109
- canvasRev: snapshot.canvas_rev,
3110
- // Fresh key — the body changed (different canvas_rev), so reusing
3111
- // the caller's key would trigger IDEMPOTENCY_KEY_CONFLICT.
3112
- idempotencyKey: void 0
3113
- });
3114
- return { ...retryResult, retriedWithRev: snapshot.canvas_rev };
3115
- }
3116
- }
3117
- async function patchWorkflow(client, opts) {
3118
- const data = await client.request({
3119
- method: "PATCH",
3120
- path: `/workflows/${encodeURIComponent(opts.id)}`,
3121
- body: {
3122
- canvas_rev: opts.canvasRev,
3123
- operations: opts.operations
3124
- },
3134
+ return applyWorkflowWithRetry({
3135
+ client,
3136
+ id: opts.id,
3137
+ canvasRev: opts.canvasRev,
3138
+ operations: opts.operations,
3125
3139
  idempotencyKey: opts.idempotencyKey,
3126
- responseSchema: WorkflowMutateResponseSchema,
3127
- signal: opts.signal
3140
+ autoRev: opts.autoRev,
3141
+ signal: opts.signal,
3142
+ schemas: { mutate: WorkflowMutateResponseSchema, detail: WorkflowDetailSchema }
3128
3143
  });
3129
- return {
3130
- canvasRev: data.canvas_rev,
3131
- applied: data.applied,
3132
- workflow: data.workflow
3133
- };
3134
3144
  }
3135
3145
 
3136
3146
  // src/commands/workflow/create.ts
@@ -3161,6 +3171,17 @@ async function createWorkflow(opts) {
3161
3171
  });
3162
3172
  }
3163
3173
 
3174
+ // src/commands/workflow/get.ts
3175
+ async function getWorkflow(opts) {
3176
+ const client = opts.client ?? await buildWorkflowClient(opts);
3177
+ return client.request({
3178
+ method: "GET",
3179
+ path: `/workflows/${encodeURIComponent(opts.id)}`,
3180
+ responseSchema: WorkflowDetailSchema,
3181
+ signal: opts.signal
3182
+ });
3183
+ }
3184
+
3164
3185
  // src/commands/workflow/list.ts
3165
3186
  async function listWorkflows(opts) {
3166
3187
  const client = opts.client ?? await buildWorkflowClient(opts);