@roj-ai/platform-cli 0.1.6 → 0.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;IAChE,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd,GAAG,OAAO,CAAC,IAAI,CAAC,CA4GhB"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;IAChE,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAsHhB"}
package/dist/resource.js CHANGED
@@ -25,24 +25,30 @@ export async function uploadResource(pathOrDir, options) {
25
25
  mimeType = guessMimeType(filename);
26
26
  }
27
27
  try {
28
- // 1. Upload file
29
- console.log(`Uploading file: ${filename}`);
28
+ // 1. Hash file content for dedup
30
29
  const file = Bun.file(filePath);
31
- const formData = new FormData();
32
- formData.append('file', file, filename);
33
- const uploadResponse = await fetch(`${options.url}/api/v1/files/upload`, {
34
- method: 'POST',
35
- headers,
36
- body: formData,
37
- });
38
- if (!uploadResponse.ok) {
39
- const error = await uploadResponse.json().catch(() => ({ error: uploadResponse.statusText }));
40
- console.error('File upload failed:', error.error ?? uploadResponse.statusText);
30
+ const buf = await file.arrayBuffer();
31
+ const contentHash = await sha256Hex(buf);
32
+ // 2. Preflight upload — if hash already known, server skips R2 put.
33
+ let uploadResult = await postFile({ url: options.url, apiKey: options.apiKey, contentHash, filename, mimeType });
34
+ if (uploadResult.status === 409 && uploadResult.body?.error === 'file-required') {
35
+ uploadResult = await postFile({
36
+ url: options.url,
37
+ apiKey: options.apiKey,
38
+ contentHash,
39
+ filename,
40
+ mimeType,
41
+ body: { buf, filename, mimeType },
42
+ });
43
+ }
44
+ if (uploadResult.status >= 400 || !uploadResult.body?.ok || !uploadResult.body.fileId) {
45
+ console.error('File upload failed:', uploadResult.body?.error ?? `HTTP ${uploadResult.status}`);
41
46
  process.exit(1);
42
47
  }
43
- const uploadResult = await uploadResponse.json();
44
- console.log(`File uploaded: ${uploadResult.fileId}`);
45
- // 2. Check if resource exists
48
+ const fileId = uploadResult.body.fileId;
49
+ const dedupNote = uploadResult.body.deduped ? ' (deduped, reused existing R2 object)' : '';
50
+ console.log(`File uploaded: ${fileId}${dedupNote}`);
51
+ // 3. Check if resource exists
46
52
  const getResponse = await fetch(`${options.url}/api/v1/rpc`, {
47
53
  method: 'POST',
48
54
  headers: { ...headers, 'Content-Type': 'application/json' },
@@ -59,17 +65,22 @@ export async function uploadResource(pathOrDir, options) {
59
65
  method: 'resources.addRevision',
60
66
  input: {
61
67
  resourceSlug: options.slug,
62
- fileId: uploadResult.fileId,
68
+ fileId,
63
69
  label: options.label,
64
70
  },
65
71
  }),
66
72
  });
67
73
  const revResult = await revResponse.json();
68
- if (!revResult.ok) {
74
+ if (!revResult.ok || !revResult.value) {
69
75
  console.error('Failed to add revision:', revResult);
70
76
  process.exit(1);
71
77
  }
72
- console.log(`Revision added: ${revResult.value?.revisionId}`);
78
+ if (revResult.value.noop) {
79
+ console.log(`Unchanged: latest revision already points at this file (revisionId=${revResult.value.revisionId})`);
80
+ }
81
+ else {
82
+ console.log(`Revision added: ${revResult.value.revisionId}`);
83
+ }
73
84
  }
74
85
  else {
75
86
  // 3b. Resource doesn't exist → create
@@ -83,7 +94,7 @@ export async function uploadResource(pathOrDir, options) {
83
94
  slug: options.slug,
84
95
  name: options.name,
85
96
  description: options.description,
86
- fileId: uploadResult.fileId,
97
+ fileId,
87
98
  label: options.label,
88
99
  },
89
100
  }),
@@ -103,6 +114,31 @@ export async function uploadResource(pathOrDir, options) {
103
114
  }
104
115
  }
105
116
  }
117
+ async function postFile(args) {
118
+ const formData = new FormData();
119
+ formData.append('contentHash', args.contentHash);
120
+ formData.append('filename', args.filename);
121
+ formData.append('mimeType', args.mimeType);
122
+ if (args.body) {
123
+ formData.append('file', new Blob([args.body.buf], { type: args.body.mimeType }), args.body.filename);
124
+ }
125
+ const response = await fetch(`${args.url}/api/v1/files/upload`, {
126
+ method: 'POST',
127
+ headers: { Authorization: `Bearer ${args.apiKey}` },
128
+ body: formData,
129
+ });
130
+ const body = await response.json().catch(() => null);
131
+ return { status: response.status, body };
132
+ }
133
+ async function sha256Hex(buf) {
134
+ const digest = await crypto.subtle.digest('SHA-256', buf);
135
+ const bytes = new Uint8Array(digest);
136
+ let hex = '';
137
+ for (let i = 0; i < bytes.length; i++) {
138
+ hex += bytes[i].toString(16).padStart(2, '0');
139
+ }
140
+ return hex;
141
+ }
106
142
  function guessMimeType(filename) {
107
143
  const ext = filename.split('.').pop()?.toLowerCase();
108
144
  switch (ext) {
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,wBAAsB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE;IACzD,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BhB"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,wBAAsB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE;IACzD,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsChB"}
package/dist/upload.js CHANGED
@@ -4,23 +4,60 @@ export async function upload(bundlePath, options) {
4
4
  console.error(`Bundle not found: ${bundlePath}`);
5
5
  process.exit(1);
6
6
  }
7
+ const filename = bundlePath.split('/').pop();
8
+ const buf = await file.arrayBuffer();
9
+ const contentHash = await sha256Hex(buf);
10
+ // 1. Preflight: ask server if it already has this content for the org.
11
+ let result = await postBundle({ url: options.url, apiKey: options.apiKey, name: options.name, version: options.version, contentHash });
12
+ // 2. Server reports the bundle bytes are missing — retry with body.
13
+ if (result.status === 409 && result.body?.error === 'bundle-required') {
14
+ result = await postBundle({
15
+ url: options.url,
16
+ apiKey: options.apiKey,
17
+ name: options.name,
18
+ version: options.version,
19
+ contentHash,
20
+ body: { buf, filename, mimeType: file.type || 'application/javascript' },
21
+ });
22
+ }
23
+ if (result.status >= 400 || !result.body?.ok) {
24
+ console.error('Upload failed:', result.body?.error ?? `HTTP ${result.status}`);
25
+ process.exit(1);
26
+ }
27
+ const status = result.body.noop
28
+ ? `unchanged (latest revision already at ${shortHash(contentHash)})`
29
+ : result.body.deduped
30
+ ? `new revision pointing at existing bundle (${shortHash(contentHash)})`
31
+ : `uploaded new bundle (${shortHash(contentHash)})`;
32
+ console.log(`${status}: slug=${result.body.bundleSlug} revisionId=${result.body.revisionId}`);
33
+ }
34
+ async function postBundle(args) {
7
35
  const formData = new FormData();
8
- formData.append('bundle', file, bundlePath.split('/').pop());
9
- formData.append('name', options.name);
10
- if (options.version) {
11
- formData.append('version', options.version);
36
+ formData.append('name', args.name);
37
+ formData.append('contentHash', args.contentHash);
38
+ if (args.version)
39
+ formData.append('version', args.version);
40
+ if (args.body) {
41
+ formData.append('bundle', new Blob([args.body.buf], { type: args.body.mimeType }), args.body.filename);
12
42
  }
13
- const response = await fetch(`${options.url}/api/v1/bundles/upload`, {
43
+ const response = await fetch(`${args.url}/api/v1/bundles/upload`, {
14
44
  method: 'POST',
15
- headers: { Authorization: `Bearer ${options.apiKey}` },
45
+ headers: { Authorization: `Bearer ${args.apiKey}` },
16
46
  body: formData,
17
47
  });
18
- if (!response.ok) {
19
- const error = await response.json().catch(() => ({ error: response.statusText }));
20
- console.error('Upload failed:', error.error ?? response.statusText);
21
- process.exit(1);
48
+ const body = await response.json().catch(() => null);
49
+ return { status: response.status, body };
50
+ }
51
+ async function sha256Hex(buf) {
52
+ const digest = await crypto.subtle.digest('SHA-256', buf);
53
+ const bytes = new Uint8Array(digest);
54
+ let hex = '';
55
+ for (let i = 0; i < bytes.length; i++) {
56
+ hex += bytes[i].toString(16).padStart(2, '0');
22
57
  }
23
- const result = await response.json();
24
- console.log(`Uploaded: slug=${result.bundleSlug} revisionId=${result.revisionId}`);
58
+ return hex;
59
+ }
60
+ function shortHash(hex) {
61
+ return hex.slice(0, 12);
25
62
  }
26
63
  //# sourceMappingURL=upload.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,UAAkB,EAAE,OAKhD;IACA,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACjC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;IAC/B,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAA;IAC7D,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5C,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,wBAAwB,EAAE;QACpE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE,EAAE;QACtD,IAAI,EAAE,QAAQ;KACd,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;QACjF,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAG,KAA4B,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAA;QAC3F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA+E,CAAA;IACjH,OAAO,CAAC,GAAG,CAAC,kBAAkB,MAAM,CAAC,UAAU,eAAe,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA;AACnF,CAAC"}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roj-ai/platform-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "roj": "./dist/main.js"
@@ -22,7 +22,7 @@
22
22
  "url": "https://github.com/contember/roj/issues"
23
23
  },
24
24
  "dependencies": {
25
- "@roj-ai/sandbox-runtime": "^0.1.6"
25
+ "@roj-ai/sandbox-runtime": "^0.1.7"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/bun": "^1.3.3"
package/src/resource.ts CHANGED
@@ -35,28 +35,34 @@ export async function uploadResource(pathOrDir: string, options: {
35
35
  }
36
36
 
37
37
  try {
38
- // 1. Upload file
39
- console.log(`Uploading file: ${filename}`)
38
+ // 1. Hash file content for dedup
40
39
  const file = Bun.file(filePath)
41
- const formData = new FormData()
42
- formData.append('file', file, filename)
40
+ const buf = await file.arrayBuffer()
41
+ const contentHash = await sha256Hex(buf)
43
42
 
44
- const uploadResponse = await fetch(`${options.url}/api/v1/files/upload`, {
45
- method: 'POST',
46
- headers,
47
- body: formData,
48
- })
43
+ // 2. Preflight upload — if hash already known, server skips R2 put.
44
+ let uploadResult = await postFile({ url: options.url, apiKey: options.apiKey, contentHash, filename, mimeType })
45
+ if (uploadResult.status === 409 && uploadResult.body?.error === 'file-required') {
46
+ uploadResult = await postFile({
47
+ url: options.url,
48
+ apiKey: options.apiKey,
49
+ contentHash,
50
+ filename,
51
+ mimeType,
52
+ body: { buf, filename, mimeType },
53
+ })
54
+ }
49
55
 
50
- if (!uploadResponse.ok) {
51
- const error = await uploadResponse.json().catch(() => ({ error: uploadResponse.statusText }))
52
- console.error('File upload failed:', (error as { error?: string }).error ?? uploadResponse.statusText)
56
+ if (uploadResult.status >= 400 || !uploadResult.body?.ok || !uploadResult.body.fileId) {
57
+ console.error('File upload failed:', uploadResult.body?.error ?? `HTTP ${uploadResult.status}`)
53
58
  process.exit(1)
54
59
  }
55
60
 
56
- const uploadResult = await uploadResponse.json() as { fileId: string }
57
- console.log(`File uploaded: ${uploadResult.fileId}`)
61
+ const fileId = uploadResult.body.fileId
62
+ const dedupNote = uploadResult.body.deduped ? ' (deduped, reused existing R2 object)' : ''
63
+ console.log(`File uploaded: ${fileId}${dedupNote}`)
58
64
 
59
- // 2. Check if resource exists
65
+ // 3. Check if resource exists
60
66
  const getResponse = await fetch(`${options.url}/api/v1/rpc`, {
61
67
  method: 'POST',
62
68
  headers: { ...headers, 'Content-Type': 'application/json' },
@@ -75,18 +81,22 @@ export async function uploadResource(pathOrDir: string, options: {
75
81
  method: 'resources.addRevision',
76
82
  input: {
77
83
  resourceSlug: options.slug,
78
- fileId: uploadResult.fileId,
84
+ fileId,
79
85
  label: options.label,
80
86
  },
81
87
  }),
82
88
  })
83
89
 
84
- const revResult = await revResponse.json() as { ok: boolean; value?: { revisionId: string } }
85
- if (!revResult.ok) {
90
+ const revResult = await revResponse.json() as { ok: boolean; value?: { revisionId: string; noop?: boolean } }
91
+ if (!revResult.ok || !revResult.value) {
86
92
  console.error('Failed to add revision:', revResult)
87
93
  process.exit(1)
88
94
  }
89
- console.log(`Revision added: ${revResult.value?.revisionId}`)
95
+ if (revResult.value.noop) {
96
+ console.log(`Unchanged: latest revision already points at this file (revisionId=${revResult.value.revisionId})`)
97
+ } else {
98
+ console.log(`Revision added: ${revResult.value.revisionId}`)
99
+ }
90
100
  } else {
91
101
  // 3b. Resource doesn't exist → create
92
102
  console.log(`Creating resource "${options.slug}"...`)
@@ -99,7 +109,7 @@ export async function uploadResource(pathOrDir: string, options: {
99
109
  slug: options.slug,
100
110
  name: options.name,
101
111
  description: options.description,
102
- fileId: uploadResult.fileId,
112
+ fileId,
103
113
  label: options.label,
104
114
  },
105
115
  }),
@@ -120,6 +130,58 @@ export async function uploadResource(pathOrDir: string, options: {
120
130
  }
121
131
  }
122
132
 
133
+ interface PostFileArgs {
134
+ url: string
135
+ apiKey: string
136
+ contentHash: string
137
+ filename: string
138
+ mimeType: string
139
+ body?: { buf: ArrayBuffer; filename: string; mimeType: string }
140
+ }
141
+
142
+ interface PostFileResult {
143
+ status: number
144
+ body: {
145
+ ok?: boolean
146
+ error?: string
147
+ fileId?: string
148
+ filename?: string
149
+ mimeType?: string
150
+ size?: number
151
+ r2Key?: string
152
+ deduped?: boolean
153
+ } | null
154
+ }
155
+
156
+ async function postFile(args: PostFileArgs): Promise<PostFileResult> {
157
+ const formData = new FormData()
158
+ formData.append('contentHash', args.contentHash)
159
+ formData.append('filename', args.filename)
160
+ formData.append('mimeType', args.mimeType)
161
+ if (args.body) {
162
+ formData.append('file', new Blob([args.body.buf], { type: args.body.mimeType }), args.body.filename)
163
+ }
164
+
165
+ const response = await fetch(`${args.url}/api/v1/files/upload`, {
166
+ method: 'POST',
167
+ headers: { Authorization: `Bearer ${args.apiKey}` },
168
+ body: formData,
169
+ })
170
+
171
+ const body = await response.json().catch(() => null) as PostFileResult['body']
172
+ return { status: response.status, body }
173
+ }
174
+
175
+ async function sha256Hex(buf: ArrayBuffer): Promise<string> {
176
+ const digest = await crypto.subtle.digest('SHA-256', buf)
177
+ const bytes = new Uint8Array(digest)
178
+ let hex = ''
179
+ for (let i = 0; i < bytes.length; i++) {
180
+ hex += bytes[i].toString(16).padStart(2, '0')
181
+ }
182
+ return hex
183
+ }
184
+
123
185
  function guessMimeType(filename: string): string {
124
186
  const ext = filename.split('.').pop()?.toLowerCase()
125
187
  switch (ext) {
package/src/upload.ts CHANGED
@@ -10,25 +10,90 @@ export async function upload(bundlePath: string, options: {
10
10
  process.exit(1)
11
11
  }
12
12
 
13
+ const filename = bundlePath.split('/').pop()!
14
+ const buf = await file.arrayBuffer()
15
+ const contentHash = await sha256Hex(buf)
16
+
17
+ // 1. Preflight: ask server if it already has this content for the org.
18
+ let result = await postBundle({ url: options.url, apiKey: options.apiKey, name: options.name, version: options.version, contentHash })
19
+
20
+ // 2. Server reports the bundle bytes are missing — retry with body.
21
+ if (result.status === 409 && result.body?.error === 'bundle-required') {
22
+ result = await postBundle({
23
+ url: options.url,
24
+ apiKey: options.apiKey,
25
+ name: options.name,
26
+ version: options.version,
27
+ contentHash,
28
+ body: { buf, filename, mimeType: file.type || 'application/javascript' },
29
+ })
30
+ }
31
+
32
+ if (result.status >= 400 || !result.body?.ok) {
33
+ console.error('Upload failed:', result.body?.error ?? `HTTP ${result.status}`)
34
+ process.exit(1)
35
+ }
36
+
37
+ const status = result.body.noop
38
+ ? `unchanged (latest revision already at ${shortHash(contentHash)})`
39
+ : result.body.deduped
40
+ ? `new revision pointing at existing bundle (${shortHash(contentHash)})`
41
+ : `uploaded new bundle (${shortHash(contentHash)})`
42
+
43
+ console.log(`${status}: slug=${result.body.bundleSlug} revisionId=${result.body.revisionId}`)
44
+ }
45
+
46
+ interface PostBundleArgs {
47
+ url: string
48
+ apiKey: string
49
+ name: string
50
+ version?: string
51
+ contentHash: string
52
+ body?: { buf: ArrayBuffer; filename: string; mimeType: string }
53
+ }
54
+
55
+ interface PostBundleResult {
56
+ status: number
57
+ body: {
58
+ ok?: boolean
59
+ error?: string
60
+ bundleSlug?: string
61
+ revisionId?: string
62
+ r2Key?: string
63
+ deduped?: boolean
64
+ noop?: boolean
65
+ } | null
66
+ }
67
+
68
+ async function postBundle(args: PostBundleArgs): Promise<PostBundleResult> {
13
69
  const formData = new FormData()
14
- formData.append('bundle', file, bundlePath.split('/').pop()!)
15
- formData.append('name', options.name)
16
- if (options.version) {
17
- formData.append('version', options.version)
70
+ formData.append('name', args.name)
71
+ formData.append('contentHash', args.contentHash)
72
+ if (args.version) formData.append('version', args.version)
73
+ if (args.body) {
74
+ formData.append('bundle', new Blob([args.body.buf], { type: args.body.mimeType }), args.body.filename)
18
75
  }
19
76
 
20
- const response = await fetch(`${options.url}/api/v1/bundles/upload`, {
77
+ const response = await fetch(`${args.url}/api/v1/bundles/upload`, {
21
78
  method: 'POST',
22
- headers: { Authorization: `Bearer ${options.apiKey}` },
79
+ headers: { Authorization: `Bearer ${args.apiKey}` },
23
80
  body: formData,
24
81
  })
25
82
 
26
- if (!response.ok) {
27
- const error = await response.json().catch(() => ({ error: response.statusText }))
28
- console.error('Upload failed:', (error as { error?: string }).error ?? response.statusText)
29
- process.exit(1)
83
+ const body = await response.json().catch(() => null) as PostBundleResult['body']
84
+ return { status: response.status, body }
85
+ }
86
+
87
+ async function sha256Hex(buf: ArrayBuffer): Promise<string> {
88
+ const digest = await crypto.subtle.digest('SHA-256', buf)
89
+ const bytes = new Uint8Array(digest)
90
+ let hex = ''
91
+ for (let i = 0; i < bytes.length; i++) {
92
+ hex += bytes[i].toString(16).padStart(2, '0')
30
93
  }
94
+ return hex
95
+ }
31
96
 
32
- const result = await response.json() as { ok: boolean; bundleSlug?: string; revisionId?: string; r2Key?: string }
33
- console.log(`Uploaded: slug=${result.bundleSlug} revisionId=${result.revisionId}`)
97
+ function shortHash(hex: string): string {
98
+ return hex.slice(0, 12)
34
99
  }