@clickraft/cli 0.8.5 → 0.8.7

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,4 +1,4 @@
1
- ## [0.8.5](https://github.com/clickraft/cli/compare/v0.8.4...v0.8.5) (2026-05-31)
1
+ ## [0.8.7](https://github.com/clickraft/cli/compare/v0.8.6...v0.8.7) (2026-05-31)
2
2
 
3
3
  # Changelog
4
4
 
package/dist/cli.js CHANGED
@@ -14,6 +14,8 @@ import { request as undiciRequest } from "undici";
14
14
  import { z as z6 } from "zod";
15
15
  import { createHash, randomUUID } from "crypto";
16
16
  import { z as z5 } from "zod";
17
+ import { createHash as createHash2 } from "crypto";
18
+ import { request as undiciRequest2 } from "undici";
17
19
  var TOKEN_FIELD_PATTERN = /"(access_token|refresh_token|device_code|user_code|code_verifier|client_secret)"\s*:\s*"[^"]*"/gi;
18
20
  var BARE_TOKEN_PATTERN = /\bck_(?:live|dev|test)_[A-Za-z0-9_-]+/gi;
19
21
  function redactTokenLikeFields(input) {
@@ -636,6 +638,181 @@ function errorMessage(err) {
636
638
  if (err instanceof Error) return err.message;
637
639
  return String(err);
638
640
  }
641
+ async function applyWorkflowWithRetry(params) {
642
+ try {
643
+ return await patch(params);
644
+ } catch (err) {
645
+ if (!(err instanceof ApiError)) throw err;
646
+ const isRevConflict = err.code === "CANVAS_REV_MISMATCH" || err.meta.status === 409 && err.meta.details != null;
647
+ if (!isRevConflict || !params.autoRev) throw err;
648
+ const snapshot = await params.client.request({
649
+ method: "GET",
650
+ path: `/workflows/${encodeURIComponent(params.id)}`,
651
+ responseSchema: params.schemas.detail,
652
+ signal: params.signal
653
+ });
654
+ const retryResult = await patch({
655
+ ...params,
656
+ canvasRev: snapshot.canvas_rev,
657
+ // Fresh key — the body changed (different canvas_rev), so reusing the
658
+ // caller's key would trigger IDEMPOTENCY_KEY_CONFLICT.
659
+ idempotencyKey: void 0
660
+ });
661
+ return { ...retryResult, retriedWithRev: snapshot.canvas_rev };
662
+ }
663
+ }
664
+ async function patch(params) {
665
+ const data = await params.client.request({
666
+ method: "PATCH",
667
+ path: `/workflows/${encodeURIComponent(params.id)}`,
668
+ body: {
669
+ canvas_rev: params.canvasRev,
670
+ operations: params.operations
671
+ },
672
+ idempotencyKey: params.idempotencyKey,
673
+ responseSchema: params.schemas.mutate,
674
+ signal: params.signal
675
+ });
676
+ return {
677
+ canvasRev: data.canvas_rev,
678
+ applied: data.applied,
679
+ workflow: data.workflow
680
+ };
681
+ }
682
+ async function uploadAsset(params) {
683
+ const contentType = detectContentType(params.filename, params.content);
684
+ const sha = sha256Hex(params.content);
685
+ const contentHash = `sha256:${sha}`;
686
+ const idempotencyKey = generateUploadIdempotencyKey(params.content, params.scope);
687
+ const response = await params.client.request({
688
+ method: "POST",
689
+ path: "/assets",
690
+ body: {
691
+ filename: params.filename,
692
+ contentType,
693
+ sizeBytes: params.content.length,
694
+ contentHash
695
+ },
696
+ idempotencyKey,
697
+ responseSchema: params.schema,
698
+ signal: params.signal
699
+ });
700
+ if (!response.deduplicated) {
701
+ const first = await tryPut({
702
+ uploadUrl: response.uploadUrl,
703
+ content: params.content,
704
+ contentType,
705
+ signal: params.signal,
706
+ dispatcher: params.putDispatcher
707
+ });
708
+ if (!first.ok) {
709
+ if (first.kind === "fatal") {
710
+ throw new ApiError(
711
+ "STORAGE_UPLOAD_FAILED",
712
+ `Upload to signed URL failed for ${params.filename} (HTTP ${first.status ?? "unknown"}).`,
713
+ { retryable: false }
714
+ );
715
+ }
716
+ const retry = await tryPut({
717
+ uploadUrl: response.uploadUrl,
718
+ content: params.content,
719
+ contentType,
720
+ signal: params.signal,
721
+ dispatcher: params.putDispatcher
722
+ });
723
+ if (!retry.ok) {
724
+ throw new ApiError(
725
+ "STORAGE_UPLOAD_FAILED",
726
+ `Upload to signed URL failed for ${params.filename} after one retry.`,
727
+ { retryable: true }
728
+ );
729
+ }
730
+ }
731
+ }
732
+ return {
733
+ assetId: response.assetId,
734
+ publicUrl: response.publicUrl,
735
+ deduplicated: response.deduplicated,
736
+ bytes: params.content.length,
737
+ contentHash
738
+ };
739
+ }
740
+ async function tryPut(args) {
741
+ try {
742
+ const response = await undiciRequest2(args.uploadUrl, {
743
+ method: "PUT",
744
+ body: args.content,
745
+ headers: {
746
+ "content-type": args.contentType,
747
+ "content-length": String(args.content.length)
748
+ },
749
+ signal: args.signal,
750
+ dispatcher: args.dispatcher
751
+ });
752
+ await response.body.text();
753
+ const status = response.statusCode;
754
+ if (status >= 200 && status < 300) return { ok: true };
755
+ if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
756
+ return { ok: false, kind: "retryable", status };
757
+ } catch {
758
+ return { ok: false, kind: "retryable" };
759
+ }
760
+ }
761
+ function sha256Hex(content) {
762
+ return createHash2("sha256").update(content).digest("hex");
763
+ }
764
+ var MAGIC_NUMBERS = [
765
+ { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
766
+ { contentType: "image/jpeg", prefix: [255, 216, 255] },
767
+ { contentType: "image/webp", prefix: [82, 73, 70, 70] }
768
+ // also matches WAV — disambiguated below
769
+ ];
770
+ var EXTENSION_FALLBACK = {
771
+ png: "image/png",
772
+ jpg: "image/jpeg",
773
+ jpeg: "image/jpeg",
774
+ gif: "image/gif",
775
+ webp: "image/webp",
776
+ mp4: "video/mp4",
777
+ mov: "video/quicktime",
778
+ mp3: "audio/mpeg",
779
+ wav: "audio/wav",
780
+ pdf: "application/pdf",
781
+ txt: "text/plain",
782
+ json: "application/json"
783
+ };
784
+ function detectContentType(filename, content) {
785
+ if (content.length >= 6) {
786
+ const head = content.subarray(0, 6).toString("ascii");
787
+ if (head === "GIF87a" || head === "GIF89a") return "image/gif";
788
+ }
789
+ if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
790
+ return "video/mp4";
791
+ }
792
+ for (const { contentType, prefix } of MAGIC_NUMBERS) {
793
+ if (matchesPrefix(content, prefix)) {
794
+ if (contentType === "image/webp") {
795
+ const formatId = content.subarray(8, 12).toString("ascii");
796
+ if (formatId === "WEBP") return "image/webp";
797
+ if (formatId === "WAVE") return "audio/wav";
798
+ break;
799
+ }
800
+ return contentType;
801
+ }
802
+ }
803
+ const ext = filename.toLowerCase().split(".").pop();
804
+ if (ext && EXTENSION_FALLBACK[ext]) {
805
+ return EXTENSION_FALLBACK[ext];
806
+ }
807
+ return "application/octet-stream";
808
+ }
809
+ function matchesPrefix(content, prefix) {
810
+ if (content.length < prefix.length) return false;
811
+ for (let i = 0; i < prefix.length; i++) {
812
+ if (content[i] !== prefix[i]) return false;
813
+ }
814
+ return true;
815
+ }
639
816
 
640
817
  // src/errors/codes.ts
641
818
  var LOGIN_HINT = "Run `clickraft login` to authenticate.";
@@ -1594,10 +1771,9 @@ var GenerationResultSchema = z9.object({
1594
1771
  }).strict();
1595
1772
 
1596
1773
  // src/commands/upload.ts
1597
- import { createHash as createHash2 } from "crypto";
1598
1774
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
1599
1775
  import { basename } from "path";
1600
- import { request as undiciRequest2 } from "undici";
1776
+ import "undici";
1601
1777
 
1602
1778
  // src/schemas/assets.ts
1603
1779
  import { z as z10 } from "zod";
@@ -1622,60 +1798,16 @@ async function uploadFile(opts) {
1622
1798
  const size = await safeStatSize(opts.filePath);
1623
1799
  validateSize(size);
1624
1800
  const content = await safeReadFile(opts.filePath);
1625
- const contentType = detectContentType(opts.filePath, content);
1626
- const sha = sha256Hex(content);
1627
- const contentHash = `sha256:${sha}`;
1628
- const filename = basename(opts.filePath);
1629
- const idempotencyKey = generateUploadIdempotencyKey(content, scope);
1630
1801
  const client = opts.client ?? await buildClient(opts);
1631
- const response = await postAssetMetadata({
1802
+ return uploadAsset({
1632
1803
  client,
1633
- filename,
1634
- contentType,
1635
- sizeBytes: content.length,
1636
- contentHash,
1637
- idempotencyKey,
1638
- signal: opts.signal
1804
+ content,
1805
+ filename: basename(opts.filePath),
1806
+ scope,
1807
+ signal: opts.signal,
1808
+ putDispatcher: opts.putDispatcher,
1809
+ schema: UploadResponseSchema
1639
1810
  });
1640
- if (!response.deduplicated) {
1641
- const first = await tryPut({
1642
- uploadUrl: response.uploadUrl,
1643
- content,
1644
- contentType,
1645
- signal: opts.signal,
1646
- dispatcher: opts.putDispatcher
1647
- });
1648
- if (!first.ok) {
1649
- if (first.kind === "fatal") {
1650
- throw new ApiError(
1651
- "STORAGE_UPLOAD_FAILED",
1652
- `Upload to signed URL failed for ${filename} (HTTP ${first.status ?? "unknown"}).`,
1653
- { retryable: false }
1654
- );
1655
- }
1656
- const retry = await tryPut({
1657
- uploadUrl: response.uploadUrl,
1658
- content,
1659
- contentType,
1660
- signal: opts.signal,
1661
- dispatcher: opts.putDispatcher
1662
- });
1663
- if (!retry.ok) {
1664
- throw new ApiError(
1665
- "STORAGE_UPLOAD_FAILED",
1666
- `Upload to signed URL failed for ${filename} after one retry.`,
1667
- { retryable: true }
1668
- );
1669
- }
1670
- }
1671
- }
1672
- return {
1673
- assetId: response.assetId,
1674
- publicUrl: response.publicUrl,
1675
- deduplicated: response.deduplicated,
1676
- bytes: content.length,
1677
- contentHash
1678
- };
1679
1811
  }
1680
1812
  async function buildClient(opts) {
1681
1813
  const cfg = await loadConfig({
@@ -1690,42 +1822,6 @@ async function buildClient(opts) {
1690
1822
  cloudflareAccess: cfg.cloudflareAccess
1691
1823
  });
1692
1824
  }
1693
- async function postAssetMetadata(args) {
1694
- return args.client.request({
1695
- method: "POST",
1696
- path: "/assets",
1697
- body: {
1698
- filename: args.filename,
1699
- contentType: args.contentType,
1700
- sizeBytes: args.sizeBytes,
1701
- contentHash: args.contentHash
1702
- },
1703
- idempotencyKey: args.idempotencyKey,
1704
- responseSchema: UploadResponseSchema,
1705
- signal: args.signal
1706
- });
1707
- }
1708
- async function tryPut(args) {
1709
- try {
1710
- const response = await undiciRequest2(args.uploadUrl, {
1711
- method: "PUT",
1712
- body: args.content,
1713
- headers: {
1714
- "content-type": args.contentType,
1715
- "content-length": String(args.content.length)
1716
- },
1717
- signal: args.signal,
1718
- dispatcher: args.dispatcher
1719
- });
1720
- await response.body.text();
1721
- const status = response.statusCode;
1722
- if (status >= 200 && status < 300) return { ok: true };
1723
- if (status >= 400 && status < 500) return { ok: false, kind: "fatal", status };
1724
- return { ok: false, kind: "retryable", status };
1725
- } catch {
1726
- return { ok: false, kind: "retryable" };
1727
- }
1728
- }
1729
1825
  async function safeStatSize(filePath) {
1730
1826
  let stats;
1731
1827
  try {
@@ -1787,61 +1883,6 @@ function validateSize(bytes) {
1787
1883
  );
1788
1884
  }
1789
1885
  }
1790
- function sha256Hex(content) {
1791
- return createHash2("sha256").update(content).digest("hex");
1792
- }
1793
- var MAGIC_NUMBERS = [
1794
- { contentType: "image/png", prefix: [137, 80, 78, 71, 13, 10, 26, 10] },
1795
- { contentType: "image/jpeg", prefix: [255, 216, 255] },
1796
- { contentType: "image/webp", prefix: [82, 73, 70, 70] }
1797
- // also matches WAV — disambiguated below
1798
- ];
1799
- var EXTENSION_FALLBACK = {
1800
- png: "image/png",
1801
- jpg: "image/jpeg",
1802
- jpeg: "image/jpeg",
1803
- gif: "image/gif",
1804
- webp: "image/webp",
1805
- mp4: "video/mp4",
1806
- mov: "video/quicktime",
1807
- mp3: "audio/mpeg",
1808
- wav: "audio/wav",
1809
- pdf: "application/pdf",
1810
- txt: "text/plain",
1811
- json: "application/json"
1812
- };
1813
- function detectContentType(filePath, content) {
1814
- if (content.length >= 6) {
1815
- const head = content.subarray(0, 6).toString("ascii");
1816
- if (head === "GIF87a" || head === "GIF89a") return "image/gif";
1817
- }
1818
- if (content.length >= 12 && content.subarray(4, 8).toString("ascii") === "ftyp") {
1819
- return "video/mp4";
1820
- }
1821
- for (const { contentType, prefix } of MAGIC_NUMBERS) {
1822
- if (matchesPrefix(content, prefix)) {
1823
- if (contentType === "image/webp") {
1824
- const formatId = content.subarray(8, 12).toString("ascii");
1825
- if (formatId === "WEBP") return "image/webp";
1826
- if (formatId === "WAVE") return "audio/wav";
1827
- break;
1828
- }
1829
- return contentType;
1830
- }
1831
- }
1832
- const ext = filePath.toLowerCase().split(".").pop();
1833
- if (ext && EXTENSION_FALLBACK[ext]) {
1834
- return EXTENSION_FALLBACK[ext];
1835
- }
1836
- return "application/octet-stream";
1837
- }
1838
- function matchesPrefix(content, prefix) {
1839
- if (content.length < prefix.length) return false;
1840
- for (let i = 0; i < prefix.length; i++) {
1841
- if (content[i] !== prefix[i]) return false;
1842
- }
1843
- return true;
1844
- }
1845
1886
 
1846
1887
  // src/commands/generate/internal.ts
1847
1888
  async function buildGenerateClient(opts) {
@@ -3087,54 +3128,19 @@ async function buildWorkflowClient(opts) {
3087
3128
  });
3088
3129
  }
3089
3130
 
3090
- // src/commands/workflow/get.ts
3091
- async function getWorkflow(opts) {
3092
- const client = opts.client ?? await buildWorkflowClient(opts);
3093
- return client.request({
3094
- method: "GET",
3095
- path: `/workflows/${encodeURIComponent(opts.id)}`,
3096
- responseSchema: WorkflowDetailSchema,
3097
- signal: opts.signal
3098
- });
3099
- }
3100
-
3101
3131
  // src/commands/workflow/apply.ts
3102
3132
  async function applyWorkflow(opts) {
3103
3133
  const client = opts.client ?? await buildWorkflowClient(opts);
3104
- try {
3105
- return await patchWorkflow(client, opts);
3106
- } catch (err) {
3107
- if (!(err instanceof ApiError)) throw err;
3108
- const isRevConflict = err.code === "CANVAS_REV_MISMATCH" || err.meta.status === 409 && err.meta.details != null;
3109
- if (!isRevConflict || !opts.autoRev) throw err;
3110
- const snapshot = await getWorkflow({ id: opts.id, client, signal: opts.signal });
3111
- const retryResult = await patchWorkflow(client, {
3112
- ...opts,
3113
- canvasRev: snapshot.canvas_rev,
3114
- // Fresh key — the body changed (different canvas_rev), so reusing
3115
- // the caller's key would trigger IDEMPOTENCY_KEY_CONFLICT.
3116
- idempotencyKey: void 0
3117
- });
3118
- return { ...retryResult, retriedWithRev: snapshot.canvas_rev };
3119
- }
3120
- }
3121
- async function patchWorkflow(client, opts) {
3122
- const data = await client.request({
3123
- method: "PATCH",
3124
- path: `/workflows/${encodeURIComponent(opts.id)}`,
3125
- body: {
3126
- canvas_rev: opts.canvasRev,
3127
- operations: opts.operations
3128
- },
3134
+ return applyWorkflowWithRetry({
3135
+ client,
3136
+ id: opts.id,
3137
+ canvasRev: opts.canvasRev,
3138
+ operations: opts.operations,
3129
3139
  idempotencyKey: opts.idempotencyKey,
3130
- responseSchema: WorkflowMutateResponseSchema,
3131
- signal: opts.signal
3140
+ autoRev: opts.autoRev,
3141
+ signal: opts.signal,
3142
+ schemas: { mutate: WorkflowMutateResponseSchema, detail: WorkflowDetailSchema }
3132
3143
  });
3133
- return {
3134
- canvasRev: data.canvas_rev,
3135
- applied: data.applied,
3136
- workflow: data.workflow
3137
- };
3138
3144
  }
3139
3145
 
3140
3146
  // src/commands/workflow/create.ts
@@ -3165,6 +3171,17 @@ async function createWorkflow(opts) {
3165
3171
  });
3166
3172
  }
3167
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
+
3168
3185
  // src/commands/workflow/list.ts
3169
3186
  async function listWorkflows(opts) {
3170
3187
  const client = opts.client ?? await buildWorkflowClient(opts);