From d0b679a9d39e31a00853e53d2c262e1bb4632577 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Thu, 21 May 2026 22:09:29 +0000 Subject: [PATCH 01/10] feat: add sf migrate command and Rust CLI migration banner Show a cyan migration banner when the upgrade banner isn't being shown to nudge users toward the new Rust-based sf CLI, and add a `sf migrate` command that runs the new install script. Generated with [Indent](https://indent.com) Co-Authored-By: Indent --- src/checkVersion.ts | 23 +++++++--- src/index.ts | 14 +++++- src/lib/migrate.ts | 109 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 src/lib/migrate.ts diff --git a/src/checkVersion.ts b/src/checkVersion.ts index 64b68ecc..173169ea 100644 --- a/src/checkVersion.ts +++ b/src/checkVersion.ts @@ -93,30 +93,35 @@ async function checkProductionCLIVersion() { } } -export async function checkVersion() { +/** + * Returns true if an upgrade banner was shown (or an auto-upgrade was + * performed), false otherwise. Callers can use this to decide whether to + * show a different banner instead. + */ +export async function checkVersion(): Promise { // Disable auto-upgrade if env var is set if (process.env.SF_CLI_DISABLE_AUTO_UPGRADE) { - return; + return false; } // Skip version check if running upgrade command const args = process.argv.slice(2); - if (args[0] === "upgrade") return; + if (args[0] === "upgrade") return false; const version = pkg.version; const latestVersion = await checkProductionCLIVersion(); - if (!latestVersion) return; + if (!latestVersion) return false; - if (version === latestVersion) return; + if (version === latestVersion) return false; // Don't upgrade from stable to prerelease const currentIsStable = !semver.prerelease(version); const latestIsPrerelease = semver.prerelease(latestVersion); - if (currentIsStable && latestIsPrerelease) return; + if (currentIsStable && latestIsPrerelease) return false; const isOutdated = semver.lt(version, latestVersion); - if (!isOutdated) return; + if (!isOutdated) return false; // Only auto-upgrade for patch changes and when not going to a prerelease const isPatchUpdate = semver.diff(version, latestVersion) === "patch"; @@ -149,6 +154,7 @@ export async function checkVersion() { } catch { // Silent error, just run the command the user wanted to run } + return true; } else if (!latestIsPrerelease) { // Only show update message for non-prerelease versions const message = ` @@ -166,5 +172,8 @@ Run 'sf upgrade' to update to the latest version borderStyle: "round", }), ); + return true; } + + return false; } diff --git a/src/index.ts b/src/index.ts index 9c73532a..5a381c4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { registerDev } from "./lib/dev.ts"; import { registerImages } from "./lib/images/index.ts"; import { registerLogin } from "./lib/login.ts"; import { registerMe } from "./lib/me.ts"; +import { registerMigrate, showMigrateBanner } from "./lib/migrate.ts"; import { registerNodes } from "./lib/nodes/index.ts"; import { analytics, IS_TRACKING_DISABLED } from "./lib/posthog.ts"; import { registerScale } from "./lib/scale/index.tsx"; @@ -34,7 +35,17 @@ async function main() { const program = new Command(); if (!process.argv.includes("--json")) { - await Promise.all([checkVersion(), getAppBanner()]); + const [shownUpgradeBanner] = await Promise.all([ + checkVersion(), + getAppBanner(), + ]); + // If the user is already on the latest version of the legacy CLI, nudge + // them toward the new Rust CLI instead of showing nothing. We avoid + // double-stacking with the upgrade banner since users on outdated builds + // need to upgrade before migrating. + if (!shownUpgradeBanner && !process.argv.slice(2).includes("migrate")) { + showMigrateBanner(); + } } program @@ -63,6 +74,7 @@ async function main() { registerBalance(program); registerTokens(program); registerUpgrade(program); + registerMigrate(program); await registerScale(program); registerMe(program); await registerVM(program); diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts new file mode 100644 index 00000000..1a6440de --- /dev/null +++ b/src/lib/migrate.ts @@ -0,0 +1,109 @@ +import { spawn } from "node:child_process"; +import * as console from "node:console"; +import process from "node:process"; +import type { Command } from "@commander-js/extra-typings"; +import boxen from "boxen"; +import chalk from "chalk"; +import ora from "ora"; + +const NEW_CLI_INSTALL_URL = "https://cli.sfcompute.com"; +const MIGRATION_GUIDE_URL = + "https://docs.sfcompute.com/preview/guides/migrating-from-nodes"; + +export function showMigrateBanner() { + const message = `A new sf is here. + +We're moving to a Rust CLI with new commands like +'sf availability', 'sf capacities', and 'sf orders'. + +Run 'sf migrate' to install it. Your current sf will +be moved to 'sf-old' so you can keep using it. + +Docs: ${MIGRATION_GUIDE_URL}`; + + console.log( + boxen(chalk.cyan(message), { + padding: 1, + borderColor: "cyan", + borderStyle: "round", + }), + ); +} + +async function fetchInstallScript(): Promise { + const spinner = ora("Downloading install script").start(); + try { + const response = await fetch(NEW_CLI_INSTALL_URL); + if (!response.ok) { + spinner.fail("Failed to download install script."); + return null; + } + const script = await response.text(); + spinner.succeed(); + return script; + } catch (err) { + spinner.fail("Failed to download install script."); + console.error(err); + return null; + } +} + +async function runInstallScript(script: string): Promise { + const bashProcess = spawn("bash", [], { + stdio: ["pipe", "inherit", "inherit"], + env: process.env, + }); + + bashProcess.stdin.write(script); + bashProcess.stdin.end(); + + const code = await new Promise((resolve) => { + bashProcess.on("close", resolve); + }); + + return code === 0; +} + +export async function handleMigrate(): Promise { + const script = await fetchInstallScript(); + if (!script) return false; + + console.log(chalk.cyan("\nInstalling the new Rust sf CLI...\n")); + const ok = await runInstallScript(script); + if (!ok) { + console.error(chalk.red("\nMigration failed.")); + return false; + } + + console.log( + boxen( + chalk.cyan( + `You're on the new sf. + +Your previous CLI is still available as 'sf-old'. + +Next steps: + sf login + sf availability + +Migration guide: ${MIGRATION_GUIDE_URL}`, + ), + { + padding: 1, + borderColor: "cyan", + borderStyle: "round", + }, + ), + ); + return true; +} + +export function registerMigrate(program: Command) { + return program + .command("migrate") + .description("Install the new Rust-based sf CLI") + .action(async () => { + const success = await handleMigrate(); + process.exit(success ? 0 : 1); + }); +} From 8f91f5f28678209e9d6d599bd549a4d0fb04019d Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Thu, 21 May 2026 22:13:33 +0000 Subject: [PATCH 02/10] fix: address review feedback on migrate banner + command - Skip migrate banner when running `sf upgrade` (was showing above upgrade output because checkVersion returns false early for it) - Add SF_CLI_DISABLE_MIGRATE_BANNER opt-out, surfaced in the banner - Use process.argv[2] === "migrate" for tighter scoping, matching the existing pattern in checkVersion.ts - Handle spawn errors in handleMigrate so ENOENT/EACCES on bash surface as a clean failure instead of crashing the CLI Generated with [Indent](https://indent.com) Co-Authored-By: Indent --- src/index.ts | 12 ++++++++++-- src/lib/migrate.ts | 34 +++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5a381c4b..a5ff5320 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,8 +42,16 @@ async function main() { // If the user is already on the latest version of the legacy CLI, nudge // them toward the new Rust CLI instead of showing nothing. We avoid // double-stacking with the upgrade banner since users on outdated builds - // need to upgrade before migrating. - if (!shownUpgradeBanner && !process.argv.slice(2).includes("migrate")) { + // need to upgrade before migrating, and skip the banner for the + // `upgrade` / `migrate` commands themselves (where it'd just be noise) + // and for users who've opted out via SF_CLI_DISABLE_MIGRATE_BANNER. + const subcommand = process.argv[2]; + if ( + !shownUpgradeBanner && + subcommand !== "migrate" && + subcommand !== "upgrade" && + !process.env.SF_CLI_DISABLE_MIGRATE_BANNER + ) { showMigrateBanner(); } } diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index 1a6440de..d29adb57 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -19,7 +19,8 @@ We're moving to a Rust CLI with new commands like Run 'sf migrate' to install it. Your current sf will be moved to 'sf-old' so you can keep using it. -Docs: ${MIGRATION_GUIDE_URL}`; +Docs: ${MIGRATION_GUIDE_URL} +Hide: SF_CLI_DISABLE_MIGRATE_BANNER=1`; console.log( boxen(chalk.cyan(message), { @@ -54,14 +55,33 @@ async function runInstallScript(script: string): Promise { env: process.env, }); - bashProcess.stdin.write(script); - bashProcess.stdin.end(); - - const code = await new Promise((resolve) => { - bashProcess.on("close", resolve); + // Without an error listener, spawn failures (ENOENT/EACCES on bash) emit + // an unhandled 'error' event and crash the CLI instead of returning false. + const spawnError = new Promise((resolve) => { + bashProcess.once("error", resolve); }); - return code === 0; + try { + bashProcess.stdin.write(script); + bashProcess.stdin.end(); + } catch { + // If stdin is already torn down (e.g. spawn failed synchronously), the + // 'error' event handler below will surface the real reason. + } + + const result = await Promise.race([ + new Promise<{ kind: "close"; code: number | null }>((resolve) => { + bashProcess.once("close", (code) => resolve({ kind: "close", code })); + }), + spawnError.then((err) => ({ kind: "error" as const, err })), + ]); + + if (result.kind === "error") { + console.error(chalk.red(`Failed to run bash: ${result.err.message}`)); + return false; + } + + return result.code === 0; } export async function handleMigrate(): Promise { From a9e78ed982bbc725f695042391a07a61ef7c1d25 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Thu, 21 May 2026 22:17:26 +0000 Subject: [PATCH 03/10] copy: rewrite migrate banner around reselling value prop Lead with the Rust rewrite and new commands, then surface the reselling story (the most important reason to migrate, per the April 7 changelog) before the action and fallback text. Generated with [Indent](https://indent.com) Co-Authored-By: Indent --- src/lib/migrate.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index d29adb57..b3ac73d7 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -11,10 +11,12 @@ const MIGRATION_GUIDE_URL = "https://docs.sfcompute.com/preview/guides/migrating-from-nodes"; export function showMigrateBanner() { - const message = `A new sf is here. + const message = `We've rewritten sf in Rust — faster, with new commands +like 'sf availability', 'sf capacities', and 'sf orders'. -We're moving to a Rust CLI with new commands like -'sf availability', 'sf capacities', and 'sf orders'. +Migrating also opts you into our public preview, which +lets you resell unused compute back to the market and +earn credits. Run 'sf migrate' to install it. Your current sf will be moved to 'sf-old' so you can keep using it. From dd5892371432f4ced8697ed9eb945183c85eaebf Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Thu, 21 May 2026 22:21:15 +0000 Subject: [PATCH 04/10] copy: "back to the market" -> "back on our orderbook" Per DTao. Generated with [Indent](https://indent.com) Co-Authored-By: Indent --- src/lib/migrate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index b3ac73d7..5251e633 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -15,8 +15,8 @@ export function showMigrateBanner() { like 'sf availability', 'sf capacities', and 'sf orders'. Migrating also opts you into our public preview, which -lets you resell unused compute back to the market and -earn credits. +lets you resell unused compute back on our orderbook +and earn credits. Run 'sf migrate' to install it. Your current sf will be moved to 'sf-old' so you can keep using it. From a358e64328fb444799960bce69bc11f8dc47a46e Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 16:57:22 -0700 Subject: [PATCH 05/10] fix: install upgrades back to the current binary's path The legacy installer hardcodes ~/.local/bin/sf, which clobbers the new Rust sf when running as sf-old and silently drops a duplicate when sf lives elsewhere (e.g. /usr/local/bin). Honor SF_CLI_TARGET_DIR and SF_CLI_BINARY_NAME so upgrade.ts can point the installer at process.execPath. Skip the PATH onboarding nudge for in-place upgrades since the user is already invoking the binary. Co-Authored-By: Claude Opus 4.7 --- install.sh | 22 +++++++++++++++++++--- src/lib/upgrade.ts | 12 +++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index d2af927a..d0292c5b 100755 --- a/install.sh +++ b/install.sh @@ -4,14 +4,23 @@ set -e # Exit on any error # Define the GitHub repository and the name of the binary. GITHUB_REPO="sfcompute/cli" -BINARY_NAME="sf" +# Allow the caller to override the binary name / install dir so an in-place +# upgrade lands wherever the existing binary actually lives (e.g. `sf-old`, +# or an `sf` that's not under ~/.local/bin). The TS CLI sets these from +# process.execPath when it shells out to this script. +BINARY_NAME="${SF_CLI_BINARY_NAME:-sf}" # Check the operating system OS="$(uname -s)" ARCH="$(uname -m)" -TARGET_DIR_UNEXPANDED="\${HOME}/.local/bin" -TARGET_DIR="${HOME}/.local/bin" +if [ -n "${SF_CLI_TARGET_DIR}" ]; then + TARGET_DIR="${SF_CLI_TARGET_DIR}" + TARGET_DIR_UNEXPANDED="${SF_CLI_TARGET_DIR}" +else + TARGET_DIR_UNEXPANDED="\${HOME}/.local/bin" + TARGET_DIR="${HOME}/.local/bin" +fi # Function to check if a command exists command_exists() { @@ -130,6 +139,13 @@ if [ -f "${TARGET_FILE}" ]; then echo "Successfully installed '${BINARY_NAME}' CLI." echo "The binary is located at '${TARGET_FILE}'." + # In-place upgrades (TARGET_DIR overridden by the caller) skip the PATH + # onboarding nudge — the user is already running the binary, so they + # obviously have it on PATH. + if [ -n "${SF_CLI_TARGET_DIR}" ]; then + exit 0 + fi + # Provide instructions for adding the target directory to the PATH. printf "\033[0;32m\\n" printf "To use the '%s' command, add '%s' to your PATH.\\n" "${BINARY_NAME}" "${TARGET_DIR_UNEXPANDED}" diff --git a/src/lib/upgrade.ts b/src/lib/upgrade.ts index ac3a5540..5edebd15 100644 --- a/src/lib/upgrade.ts +++ b/src/lib/upgrade.ts @@ -1,5 +1,6 @@ import { spawn } from "node:child_process"; import * as console from "node:console"; +import { basename, dirname } from "node:path"; import process from "node:process"; import type { Command } from "@commander-js/extra-typings"; import ora from "ora"; @@ -70,9 +71,18 @@ export async function handleUpgrade( // Execute the script with bash spinner.start("Installing upgrade"); + // Tell the install script to write back to this exact binary's path. Without + // this, the installer hardcodes ~/.local/bin/sf — which would clobber the + // Rust `sf` if we're running as `sf-old`, and would silently drop a + // duplicate copy when `sf` is installed somewhere else (e.g. /usr/local/bin). const bashProcess = spawn("bash", [], { stdio: ["pipe", "pipe", "pipe"], - env: version ? { ...process.env, SF_CLI_VERSION: version } : process.env, + env: { + ...process.env, + ...(version ? { SF_CLI_VERSION: version } : {}), + SF_CLI_TARGET_DIR: dirname(process.execPath), + SF_CLI_BINARY_NAME: basename(process.execPath), + }, }); let stdout = ""; From 7f859860c1485fcb880b18a1c6b385dc112fb88f Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 16:57:27 -0700 Subject: [PATCH 06/10] fix: skip auto-upgrade when running 'sf migrate' The legacy auto-upgrade and 'sf migrate' both target ~/.local/bin/sf, so running them back-to-back risks clobbering the freshly installed Rust binary. Set SF_CLI_DISABLE_AUTO_UPGRADE=1 before dispatching 'sf migrate' so the upgrade path is skipped entirely. Co-Authored-By: Claude Opus 4.7 --- src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index a5ff5320..8612401e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,14 @@ import { registerZones } from "./lib/zones.tsx"; async function main() { const program = new Command(); + // `sf migrate` replaces this binary outright, so auto-upgrading the legacy + // CLI first would be wasted work — and worse, the install scripts target + // the same `~/.local/bin/sf` path, so racing them risks clobbering the new + // Rust binary the user is about to install. + if (process.argv[2] === "migrate") { + process.env.SF_CLI_DISABLE_AUTO_UPGRADE = "1"; + } + if (!process.argv.includes("--json")) { const [shownUpgradeBanner] = await Promise.all([ checkVersion(), From ca4b5d647471b5fc61ccd69f17af5d7ebd52529b Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 16:57:32 -0700 Subject: [PATCH 07/10] chore: rewrite migrate banner copy and update guide URL Banner now leads with the Rust rewrite and frames the orderbook value prop as recouping up to 20% of spend. Migration guide URL switches from the docs preview path to sfcompute.com/migrate. Inlines the install-script helpers back into the command action. Co-Authored-By: Claude Opus 4.7 --- src/lib/migrate.ts | 173 ++++++++++++++++++++------------------------- 1 file changed, 78 insertions(+), 95 deletions(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index 5251e633..ccc574ad 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -7,19 +7,16 @@ import chalk from "chalk"; import ora from "ora"; const NEW_CLI_INSTALL_URL = "https://cli.sfcompute.com"; -const MIGRATION_GUIDE_URL = - "https://docs.sfcompute.com/preview/guides/migrating-from-nodes"; +const MIGRATION_GUIDE_URL = "https://sfcompute.com/migrate"; export function showMigrateBanner() { - const message = `We've rewritten sf in Rust — faster, with new commands -like 'sf availability', 'sf capacities', and 'sf orders'. + const message = `We've rewritten the sf CLI in Rust. -Migrating also opts you into our public preview, which -lets you resell unused compute back on our orderbook -and earn credits. +List idle capacity on the orderbook to +recoup up to 20% of your spend. -Run 'sf migrate' to install it. Your current sf will -be moved to 'sf-old' so you can keep using it. +Run 'sf migrate' to switch. Your current +CLI stays as 'sf-old'. Docs: ${MIGRATION_GUIDE_URL} Hide: SF_CLI_DISABLE_MIGRATE_BANNER=1`; @@ -33,74 +30,69 @@ Hide: SF_CLI_DISABLE_MIGRATE_BANNER=1`; ); } -async function fetchInstallScript(): Promise { - const spinner = ora("Downloading install script").start(); - try { - const response = await fetch(NEW_CLI_INSTALL_URL); - if (!response.ok) { - spinner.fail("Failed to download install script."); - return null; - } - const script = await response.text(); - spinner.succeed(); - return script; - } catch (err) { - spinner.fail("Failed to download install script."); - console.error(err); - return null; - } -} - -async function runInstallScript(script: string): Promise { - const bashProcess = spawn("bash", [], { - stdio: ["pipe", "inherit", "inherit"], - env: process.env, - }); - - // Without an error listener, spawn failures (ENOENT/EACCES on bash) emit - // an unhandled 'error' event and crash the CLI instead of returning false. - const spawnError = new Promise((resolve) => { - bashProcess.once("error", resolve); - }); - - try { - bashProcess.stdin.write(script); - bashProcess.stdin.end(); - } catch { - // If stdin is already torn down (e.g. spawn failed synchronously), the - // 'error' event handler below will surface the real reason. - } - - const result = await Promise.race([ - new Promise<{ kind: "close"; code: number | null }>((resolve) => { - bashProcess.once("close", (code) => resolve({ kind: "close", code })); - }), - spawnError.then((err) => ({ kind: "error" as const, err })), - ]); - - if (result.kind === "error") { - console.error(chalk.red(`Failed to run bash: ${result.err.message}`)); - return false; - } - - return result.code === 0; -} - -export async function handleMigrate(): Promise { - const script = await fetchInstallScript(); - if (!script) return false; - - console.log(chalk.cyan("\nInstalling the new Rust sf CLI...\n")); - const ok = await runInstallScript(script); - if (!ok) { - console.error(chalk.red("\nMigration failed.")); - return false; - } - - console.log( - boxen( - chalk.cyan( - `You're on the new sf. +export function registerMigrate(program: Command) { + return program + .command("migrate") + .description("Install the new Rust-based sf CLI") + .action(async () => { + const spinner = ora("Downloading install script").start(); + let script: string; + try { + const response = await fetch(NEW_CLI_INSTALL_URL); + if (!response.ok) { + spinner.fail("Failed to download install script."); + process.exit(1); + } + script = await response.text(); + spinner.succeed(); + } catch (err) { + spinner.fail("Failed to download install script."); + console.error(err); + process.exit(1); + } + + console.log(chalk.cyan("\nInstalling the new Rust sf CLI...\n")); + + const bashProcess = spawn("bash", [], { + stdio: ["pipe", "inherit", "inherit"], + env: process.env, + }); + + // Without an error listener, spawn failures (ENOENT/EACCES on bash) emit + // an unhandled 'error' event and crash the CLI instead of exiting cleanly. + const spawnError = new Promise((resolve) => { + bashProcess.once("error", resolve); + }); + + try { + bashProcess.stdin.write(script); + bashProcess.stdin.end(); + } catch { + // If stdin is already torn down (e.g. spawn failed synchronously), the + // 'error' event handler below will surface the real reason. + } + + const result = await Promise.race([ + new Promise<{ kind: "close"; code: number | null }>((resolve) => { + bashProcess.once("close", (code) => resolve({ kind: "close", code })); + }), + spawnError.then((err) => ({ kind: "error" as const, err })), + ]); + + if (result.kind === "error") { + console.error(chalk.red(`Failed to run bash: ${result.err.message}`)); + process.exit(1); + } + + if (result.code !== 0) { + console.error(chalk.red("\nMigration failed.")); + process.exit(1); + } + + console.log( + boxen( + chalk.cyan( + `You're on the new sf. Your previous CLI is still available as 'sf-old'. @@ -109,23 +101,14 @@ Next steps: sf availability Migration guide: ${MIGRATION_GUIDE_URL}`, - ), - { - padding: 1, - borderColor: "cyan", - borderStyle: "round", - }, - ), - ); - return true; -} - -export function registerMigrate(program: Command) { - return program - .command("migrate") - .description("Install the new Rust-based sf CLI") - .action(async () => { - const success = await handleMigrate(); - process.exit(success ? 0 : 1); + ), + { + padding: 1, + borderColor: "cyan", + borderStyle: "round", + }, + ), + ); + process.exit(0); }); } From 6abb4828019b04e93a7f296c19d0b82fbfc9a7ee Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 17:05:05 -0700 Subject: [PATCH 08/10] chore: match migrate banner color to upgrade banner Both nudge banners now use yellow so they read as the same class of prompt rather than two competing visual treatments. Co-Authored-By: Claude Opus 4.7 --- src/lib/migrate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index ccc574ad..7b907c30 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -22,9 +22,9 @@ Docs: ${MIGRATION_GUIDE_URL} Hide: SF_CLI_DISABLE_MIGRATE_BANNER=1`; console.log( - boxen(chalk.cyan(message), { + boxen(chalk.yellow(message), { padding: 1, - borderColor: "cyan", + borderColor: "yellow", borderStyle: "round", }), ); From f758517a1129ff5d5bc18270fc451079db8e6d06 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 17:05:21 -0700 Subject: [PATCH 09/10] feat: skip install script in dev mode to preview migrate UI Running 'sf migrate' under IS_DEVELOPMENT_CLI_ENV now prints a short '[dev] Skipping install script execution' notice in place of the bash spawn, while still showing the spinner, install heading, and success boxen. This lets us iterate on the migrate UI copy without touching the local sf binary. Co-Authored-By: Claude Opus 4.7 --- src/lib/migrate.ts | 74 ++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index 7b907c30..0b99d4b0 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -53,40 +53,50 @@ export function registerMigrate(program: Command) { console.log(chalk.cyan("\nInstalling the new Rust sf CLI...\n")); - const bashProcess = spawn("bash", [], { - stdio: ["pipe", "inherit", "inherit"], - env: process.env, - }); - - // Without an error listener, spawn failures (ENOENT/EACCES on bash) emit - // an unhandled 'error' event and crash the CLI instead of exiting cleanly. - const spawnError = new Promise((resolve) => { - bashProcess.once("error", resolve); - }); - - try { - bashProcess.stdin.write(script); - bashProcess.stdin.end(); - } catch { - // If stdin is already torn down (e.g. spawn failed synchronously), the - // 'error' event handler below will surface the real reason. - } - - const result = await Promise.race([ - new Promise<{ kind: "close"; code: number | null }>((resolve) => { - bashProcess.once("close", (code) => resolve({ kind: "close", code })); - }), - spawnError.then((err) => ({ kind: "error" as const, err })), - ]); + if (process.env.IS_DEVELOPMENT_CLI_ENV) { + console.log( + chalk.yellow( + "[dev] Skipping install script execution (IS_DEVELOPMENT_CLI_ENV).\n", + ), + ); + } else { + const bashProcess = spawn("bash", [], { + stdio: ["pipe", "inherit", "inherit"], + env: process.env, + }); + + // Without an error listener, spawn failures (ENOENT/EACCES on bash) emit + // an unhandled 'error' event and crash the CLI instead of exiting cleanly. + const spawnError = new Promise((resolve) => { + bashProcess.once("error", resolve); + }); + + try { + bashProcess.stdin.write(script); + bashProcess.stdin.end(); + } catch { + // If stdin is already torn down (e.g. spawn failed synchronously), the + // 'error' event handler below will surface the real reason. + } - if (result.kind === "error") { - console.error(chalk.red(`Failed to run bash: ${result.err.message}`)); - process.exit(1); - } + const result = await Promise.race([ + new Promise<{ kind: "close"; code: number | null }>((resolve) => { + bashProcess.once("close", (code) => + resolve({ kind: "close", code }), + ); + }), + spawnError.then((err) => ({ kind: "error" as const, err })), + ]); + + if (result.kind === "error") { + console.error(chalk.red(`Failed to run bash: ${result.err.message}`)); + process.exit(1); + } - if (result.code !== 0) { - console.error(chalk.red("\nMigration failed.")); - process.exit(1); + if (result.code !== 0) { + console.error(chalk.red("\nMigration failed.")); + process.exit(1); + } } console.log( From 63fd7bd2431a0f3f87506aa0ef662332940557b1 Mon Sep 17 00:00:00 2001 From: Daniel Tao Date: Tue, 26 May 2026 17:05:28 -0700 Subject: [PATCH 10/10] chore: rename 'Migration guide:' to 'Docs:' in success banner Matches the 'Docs:' label already used in the startup migrate banner. Co-Authored-By: Claude Opus 4.7 --- src/lib/migrate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/migrate.ts b/src/lib/migrate.ts index 0b99d4b0..e2bce95b 100644 --- a/src/lib/migrate.ts +++ b/src/lib/migrate.ts @@ -110,7 +110,7 @@ Next steps: sf login sf availability -Migration guide: ${MIGRATION_GUIDE_URL}`, +Docs: ${MIGRATION_GUIDE_URL}`, ), { padding: 1,