Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/checkVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
// 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";
Expand Down Expand Up @@ -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 = `
Expand All @@ -166,5 +172,8 @@ Run 'sf upgrade' to update to the latest version
borderStyle: "round",
}),
);
return true;
}

return false;
}
22 changes: 21 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -34,7 +35,25 @@ 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, 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();
}
}

program
Expand Down Expand Up @@ -63,6 +82,7 @@ async function main() {
registerBalance(program);
registerTokens(program);
registerUpgrade(program);
registerMigrate(program);
await registerScale(program);
registerMe(program);
await registerVM(program);
Expand Down
131 changes: 131 additions & 0 deletions src/lib/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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 = `We've rewritten sf in Rust — faster, with new commands
Copy link
Copy Markdown
Contributor

@joshi4 joshi4 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the first time some one is seeing this:
- sf capacities etc doesn't mean anything to them.
- rewritten in rust also doesn't entice them to migrate.

Suggested copy below. Tried to highlight better compute utilization/planning and reselling. If that piques their curiosity then can look at our docs.

A new sf is here.

Intuitive tools to plan, buy, and utilize compute.

Resell unused compute back on our orderbook and earn credits.

Opt in to get early access.

Run 'sf migrate'...

Docs: <link to preview docs>

Alternative Resell line:

Save upto 20% by reselling compute back on our orderbook and earn credits. Source: https://sfcompute.slack.com/archives/C09UJ3K6T4P/p1779323745810779

like 'sf availability', 'sf capacities', and 'sf orders'.

Migrating also opts you into our public preview, which
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.

Docs: ${MIGRATION_GUIDE_URL}
Hide: SF_CLI_DISABLE_MIGRATE_BANNER=1`;

console.log(
boxen(chalk.cyan(message), {
padding: 1,
borderColor: "cyan",
borderStyle: "round",
}),
);
}

async function fetchInstallScript(): Promise<string | null> {
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<boolean> {
const bashProcess = spawn("bash", [], {
Comment thread
indent[bot] marked this conversation as resolved.
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<Error>((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<boolean> {
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);
});
}
Loading