From c431cd5a655ff6e5e7da62f4391f9277769cec9b Mon Sep 17 00:00:00 2001 From: andreaanez Date: Tue, 17 Feb 2026 18:03:33 -0800 Subject: [PATCH 1/3] fix: correct API field names and resolve is-fullwidth-code-point ESM conflict in images upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs fixed in `sf images upload`: 1. `partResponse.data.upload_url` → `.url`: the v2/images/{id}/parts endpoint returns `url`, not `upload_url` as the schema declared. 2. `sha256_hash` → `sha256`: the v2/images/{id}/complete endpoint expects the field named `sha256`, not `sha256_hash` as the schema declared. Schema updated to match both. 3. isFullwidthCodePoint runtime crash: bun's symlink layout caused Node.js (v24) to resolve `is-fullwidth-code-point` to v5.1.0 (ESM-only) from within `cli-progress`'s nested `string-width@4.2.3` (CJS). When CJS require()s an ESM module in Node 24 it gets the module namespace object, not the default export, so `isFullwidthCodePoint(code)` threw "not a function". Fixed by pinning `is-fullwidth-code-point` to 3.0.0 via package.json overrides and a clean bun install. Also improves the complete-upload error message to use `completeResponse.error` (already parsed by openapi-fetch) instead of re-reading the consumed response body. Co-Authored-By: Claude Sonnet 4.6 --- bun.lock | 10 +++------- package.json | 5 ++++- src/lib/images/upload.ts | 9 +++------ src/schema.ts | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index fd69efe..4246f95 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "sf-cli", @@ -63,6 +62,9 @@ }, }, }, + "overrides": { + "is-fullwidth-code-point": "3.0.0", + }, "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="], @@ -854,8 +856,6 @@ "yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="], - "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@yao-pkg/pkg-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -904,8 +904,6 @@ "prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "stream-meter/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -932,8 +930,6 @@ "cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/package.json b/package.json index 753111d..1b065ed 100644 --- a/package.json +++ b/package.json @@ -75,5 +75,8 @@ "peerDependencies": { "typescript": "^5.6.2" }, - "version": "0.30.0" + "version": "0.30.0", + "overrides": { + "is-fullwidth-code-point": "3.0.0" + } } diff --git a/src/lib/images/upload.ts b/src/lib/images/upload.ts index 2a1c563..a9d2d30 100644 --- a/src/lib/images/upload.ts +++ b/src/lib/images/upload.ts @@ -261,7 +261,7 @@ const upload = new Command("upload") ); } - const url = partResponse.data.upload_url; + const url = partResponse.data.url; // Read chunk from disk with progress tracking const payload = await readChunk( @@ -355,15 +355,12 @@ const upload = new Command("upload") // Complete upload via v2 API const completeResponse = await client.POST("/v2/images/{id}/complete", { params: { path: { id: imageId } }, - body: { sha256_hash: sha256Hash }, + body: { sha256: sha256Hash }, }); if (!completeResponse.response.ok || !completeResponse.data) { - const errorText = await completeResponse.response - .text() - .catch(() => ""); throw new Error( - `Failed to complete upload: ${completeResponse.response.status} ${completeResponse.response.statusText}${errorText ? ` - ${errorText}` : ""}`, + `Failed to complete upload: ${completeResponse.response.status} ${completeResponse.response.statusText}${completeResponse.error ? ` - ${JSON.stringify(completeResponse.error)}` : ""}`, ); } diff --git a/src/schema.ts b/src/schema.ts index 2a93bd0..7fc8c0e 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2129,7 +2129,7 @@ export interface components { */ vmorch_CapacityId: string; vmorch_CompleteUploadRequest: { - sha256_hash: string; + sha256: string; }; vmorch_CompleteUploadResponse: { /** @@ -2409,7 +2409,7 @@ export interface components { part_id: number; }; vmorch_UploadPartResponse: { - upload_url: string; + url: string; expires_at: components["schemas"]["vmorch_UnixEpoch"]; }; /** @description if the script is valid utf8 then the response may be in either string, or byte form and the client must handle both */ From 0e626f5db9975714eb659420e45c1223cee41247 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 17 Feb 2026 19:06:41 -0800 Subject: [PATCH 2/3] revert: undo schema.ts field name changes The v2 API field names (sha256, url) differ from the generated schema (sha256_hash, upload_url). Rather than modifying the generated schema, the next commit switches sf images upload to raw fetch with hardcoded types so it is not coupled to schema.ts. Co-Authored-By: Claude Opus 4.6 --- src/schema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index 7fc8c0e..2a93bd0 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2129,7 +2129,7 @@ export interface components { */ vmorch_CapacityId: string; vmorch_CompleteUploadRequest: { - sha256: string; + sha256_hash: string; }; vmorch_CompleteUploadResponse: { /** @@ -2409,7 +2409,7 @@ export interface components { part_id: number; }; vmorch_UploadPartResponse: { - url: string; + upload_url: string; expires_at: components["schemas"]["vmorch_UnixEpoch"]; }; /** @description if the script is valid utf8 then the response may be in either string, or byte form and the client must handle both */ From 04ef1de82e762b7a71bbdbb6d695ffeef5da75c4 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 17 Feb 2026 19:13:58 -0800 Subject: [PATCH 3/3] style: use raw fetch for v2/images upload to decouple from schema types Co-Authored-By: Claude Opus 4.6 --- src/lib/images/upload.ts | 75 +++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/src/lib/images/upload.ts b/src/lib/images/upload.ts index a9d2d30..deb2a41 100644 --- a/src/lib/images/upload.ts +++ b/src/lib/images/upload.ts @@ -10,7 +10,7 @@ import chalk from "chalk"; import cliProgress from "cli-progress"; import cliSpinners from "cli-spinners"; import ora, { type Ora } from "ora"; -import { apiClient } from "../../apiClient.ts"; +import { getAuthToken, loadConfig } from "../../helpers/config.ts"; import { logAndQuit } from "../../helpers/errors.ts"; async function readChunk( @@ -75,23 +75,31 @@ const upload = new Command("upload") let progressBar: cliProgress.SingleBar | undefined; try { - const client = await apiClient(); + const config = await loadConfig(); + const token = await getAuthToken(); + const apiHeaders = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }; preparingSpinner = ora(`Preparing upload for ${name}...`).start(); // Create image via v2 API - const startResponse = await client.POST("/v2/images", { - body: { name }, + const startResponse = await fetch(`${config.api_url}/v2/images`, { + method: "POST", + headers: apiHeaders, + body: JSON.stringify({ name }), }); - if (!startResponse.response.ok || !startResponse.data) { - const errorText = await startResponse.response.text().catch(() => ""); + if (!startResponse.ok) { throw new Error( - `Failed to start upload: ${startResponse.response.status} ${startResponse.response.statusText}${errorText ? ` - ${errorText}` : ""}`, + `Failed to start upload: ${startResponse.status} ${startResponse.statusText}`, ); } - const imageId = startResponse.data.id; + const startData: { object: "image"; id: string; upload_status: string } = + await startResponse.json(); + const imageId = startData.id; preparingSpinner.succeed( `Started upload for image ${chalk.cyan(name)} (${chalk.blackBright( @@ -231,16 +239,17 @@ const upload = new Command("upload") } // Get presigned URL via v2 API - const partResponse = await client.POST("/v2/images/{id}/parts", { - params: { path: { id: imageId } }, - body: { part_id: part }, - }); + const partResponse = await fetch( + `${config.api_url}/v2/images/${imageId}/parts`, + { + method: "POST", + headers: apiHeaders, + body: JSON.stringify({ part_id: part }), + }, + ); - if (!partResponse.response.ok || !partResponse.data) { - const status = partResponse.response.status; - const errorText = await partResponse.response - .text() - .catch(() => ""); + if (!partResponse.ok) { + const status = partResponse.status; if ( status >= 400 && @@ -250,18 +259,20 @@ const upload = new Command("upload") ) { bail( new Error( - `Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`, + `Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`, ), ); return; } throw new Error( - `Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`, + `Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`, ); } - const url = partResponse.data.url; + const partData: { url: string; expires_at: string } = + await partResponse.json(); + const url = partData.url; // Read chunk from disk with progress tracking const payload = await readChunk( @@ -353,21 +364,31 @@ const upload = new Command("upload") const sha256Hash = hash.digest("hex"); // Complete upload via v2 API - const completeResponse = await client.POST("/v2/images/{id}/complete", { - params: { path: { id: imageId } }, - body: { sha256: sha256Hash }, - }); + const completeResponse = await fetch( + `${config.api_url}/v2/images/${imageId}/complete`, + { + method: "POST", + headers: apiHeaders, + body: JSON.stringify({ sha256: sha256Hash }), + }, + ); - if (!completeResponse.response.ok || !completeResponse.data) { + if (!completeResponse.ok) { throw new Error( - `Failed to complete upload: ${completeResponse.response.status} ${completeResponse.response.statusText}${completeResponse.error ? ` - ${JSON.stringify(completeResponse.error)}` : ""}`, + `Failed to complete upload: ${completeResponse.status} ${completeResponse.statusText}`, ); } + const completeData: { + object: "image"; + upload_status: string; + id: string; + } = await completeResponse.json(); + finalizingSpinner.succeed("Image uploaded and verified"); console.log(chalk.gray("\nNext steps:")); - console.log(` sf images get ${chalk.cyan(completeResponse.data.id)}`); + console.log(` sf images get ${chalk.cyan(completeData.id)}`); } catch (err) { if (spinnerTimer) { clearInterval(spinnerTimer);