diff --git a/src/checkVersion.ts b/src/checkVersion.ts index 4a23fe7..64b68ec 100644 --- a/src/checkVersion.ts +++ b/src/checkVersion.ts @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import * as console from "node:console"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; @@ -8,6 +8,7 @@ import boxen from "boxen"; import chalk from "chalk"; import semver from "semver"; import pkg from "../package.json" with { type: "json" }; +import { handleUpgrade } from "./lib/upgrade.ts"; const CACHE_FILE = join(homedir(), ".sfcompute", "version-cache"); const CACHE_TTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds @@ -125,13 +126,26 @@ export async function checkVersion() { chalk.cyan(`Automatically upgrading ${version} → ${latestVersion}`), ); try { - execSync("sf upgrade", { stdio: "inherit" }); + const success = await handleUpgrade(version, latestVersion); + if (!success) throw new Error("Upgrade failed"); console.log(chalk.gray("\n☁️☁️☁️\n")); - // Re-run the original command - const args = process.argv.slice(2); - execSync(`sf ${args.join(" ")}`, { stdio: "inherit" }); - process.exit(0); + // Re-run the original command with the newly installed binary. + // process.execPath is the binary's own path in a pkg build; the + // upgrade just replaced that file on disk, so re-invoking it runs + // the new version. We use `env -u PKG_EXECPATH` because pkg's + // patched child_process re-adds PKG_EXECPATH even if we delete it + // from the env object, causing the bootstrap to treat argv[1] as a + // script path. spawnSync with an argv array avoids shell injection. + const reRun = spawnSync( + "env", + ["-u", "PKG_EXECPATH", process.execPath, ...process.argv.slice(2)], + { + stdio: "inherit", + env: { ...process.env, SF_CLI_DISABLE_AUTO_UPGRADE: "1" }, + }, + ); + process.exit(reRun.status ?? 0); } catch { // Silent error, just run the command the user wanted to run } diff --git a/src/lib/upgrade.ts b/src/lib/upgrade.ts index 9b4fade..ac3a554 100644 --- a/src/lib/upgrade.ts +++ b/src/lib/upgrade.ts @@ -28,85 +28,89 @@ const getIsOnLatestVersion = async (currentVersion: string | undefined) => { return false; }; -export function registerUpgrade(program: Command) { - return program - .command("upgrade") - .argument("[version]", "The version to upgrade to") - .description("Upgrade to the latest version or a specific version") - .action(async (version) => { - const spinner = ora(); - const currentVersion = program.version(); - - if (version) { - spinner.start(`Checking if version ${version} exists`); - const url = `https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip`; - const response = await fetch(url, { method: "HEAD" }); - - if (response.status === 404) { - spinner.fail(`Version ${version} does not exist.`); - process.exit(1); - } - spinner.succeed(); - } else { - const isOnLatestVersion = await getIsOnLatestVersion(currentVersion); - if (isOnLatestVersion) { - spinner.succeed( - `You are already on the latest version (${currentVersion}).`, - ); - process.exit(0); - } - } - - // Fetch the install script - spinner.start("Downloading install script"); - const scriptResponse = await fetch( - "https://www.sfcompute.com/cli/install", +export async function handleUpgrade( + currentVersion?: string, + version?: string, +): Promise { + const spinner = ora(); + + if (version) { + spinner.start(`Checking if version ${version} exists`); + const url = + `https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip` as const; + const response = await fetch(url, { method: "HEAD" }); + + if (response.status === 404) { + spinner.fail(`Version ${version} does not exist.`); + return false; + } + spinner.succeed(); + } else { + const isOnLatestVersion = await getIsOnLatestVersion(currentVersion); + if (isOnLatestVersion) { + spinner.succeed( + `You are already on the latest version (${currentVersion}).`, ); + return true; + } + } + + // Fetch the install script + spinner.start("Downloading install script"); + const scriptResponse = await fetch("https://www.sfcompute.com/cli/install"); + + if (!scriptResponse.ok) { + spinner.fail("Failed to download install script."); + return false; + } - if (!scriptResponse.ok) { - spinner.fail("Failed to download install script."); - process.exit(1); - } + const script = await scriptResponse.text(); + spinner.succeed(); - const script = await scriptResponse.text(); - spinner.succeed(); + // Execute the script with bash + spinner.start("Installing upgrade"); - // Execute the script with bash - spinner.start("Installing upgrade"); + const bashProcess = spawn("bash", [], { + stdio: ["pipe", "pipe", "pipe"], + env: version ? { ...process.env, SF_CLI_VERSION: version } : process.env, + }); - const bashProcess = spawn("bash", [], { - stdio: ["pipe", "pipe", "pipe"], - env: version - ? { ...process.env, SF_CLI_VERSION: version } - : process.env, - }); + let stdout = ""; + let stderr = ""; - let stdout = ""; - let stderr = ""; + bashProcess.stdout.on("data", (data) => { + stdout += data.toString(); + }); - bashProcess.stdout.on("data", (data) => { - stdout += data.toString(); - }); + bashProcess.stderr.on("data", (data) => { + stderr += data.toString(); + }); - bashProcess.stderr.on("data", (data) => { - stderr += data.toString(); - }); + bashProcess.stdin.write(script); + bashProcess.stdin.end(); - bashProcess.stdin.write(script); - bashProcess.stdin.end(); + const code = await new Promise((resolve) => { + bashProcess.on("close", resolve); + }); - const code = await new Promise((resolve) => { - bashProcess.on("close", resolve); - }); + if (code !== 0) { + spinner.fail("Upgrade failed"); + console.error(stderr); + console.log(stdout); + return false; + } - if (code !== 0) { - spinner.fail("Upgrade failed"); - console.error(stderr); - console.log(stdout); - process.exit(1); - } + spinner.succeed("Upgrade completed successfully"); + return true; +} - spinner.succeed("Upgrade completed successfully"); - process.exit(0); +export function registerUpgrade(program: Command) { + return program + .command("upgrade") + .argument("[version]", "The version to upgrade to") + .description("Upgrade to the latest version or a specific version") + .action(async (version) => { + const success = await handleUpgrade(program.version(), version); + process.exit(success ? 0 : 1); }); }