Skip to content

Commit 2ef578b

Browse files
fix: [PR-1911] avoid recursive sf upgrade calls (#246)
Co-authored-by: Daniel Tao <danieltaox@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 1268ace commit 2ef578b

2 files changed

Lines changed: 92 additions & 74 deletions

File tree

src/checkVersion.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execSync } from "node:child_process";
1+
import { spawnSync } from "node:child_process";
22
import * as console from "node:console";
33
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
44
import { homedir } from "node:os";
@@ -8,6 +8,7 @@ import boxen from "boxen";
88
import chalk from "chalk";
99
import semver from "semver";
1010
import pkg from "../package.json" with { type: "json" };
11+
import { handleUpgrade } from "./lib/upgrade.ts";
1112

1213
const CACHE_FILE = join(homedir(), ".sfcompute", "version-cache");
1314
const CACHE_TTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
@@ -125,13 +126,26 @@ export async function checkVersion() {
125126
chalk.cyan(`Automatically upgrading ${version}${latestVersion}`),
126127
);
127128
try {
128-
execSync("sf upgrade", { stdio: "inherit" });
129+
const success = await handleUpgrade(version, latestVersion);
130+
if (!success) throw new Error("Upgrade failed");
129131
console.log(chalk.gray("\n☁️☁️☁️\n"));
130132

131-
// Re-run the original command
132-
const args = process.argv.slice(2);
133-
execSync(`sf ${args.join(" ")}`, { stdio: "inherit" });
134-
process.exit(0);
133+
// Re-run the original command with the newly installed binary.
134+
// process.execPath is the binary's own path in a pkg build; the
135+
// upgrade just replaced that file on disk, so re-invoking it runs
136+
// the new version. We use `env -u PKG_EXECPATH` because pkg's
137+
// patched child_process re-adds PKG_EXECPATH even if we delete it
138+
// from the env object, causing the bootstrap to treat argv[1] as a
139+
// script path. spawnSync with an argv array avoids shell injection.
140+
const reRun = spawnSync(
141+
"env",
142+
["-u", "PKG_EXECPATH", process.execPath, ...process.argv.slice(2)],
143+
{
144+
stdio: "inherit",
145+
env: { ...process.env, SF_CLI_DISABLE_AUTO_UPGRADE: "1" },
146+
},
147+
);
148+
process.exit(reRun.status ?? 0);
135149
} catch {
136150
// Silent error, just run the command the user wanted to run
137151
}

src/lib/upgrade.ts

Lines changed: 72 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -28,85 +28,89 @@ const getIsOnLatestVersion = async (currentVersion: string | undefined) => {
2828
return false;
2929
};
3030

31-
export function registerUpgrade(program: Command) {
32-
return program
33-
.command("upgrade")
34-
.argument("[version]", "The version to upgrade to")
35-
.description("Upgrade to the latest version or a specific version")
36-
.action(async (version) => {
37-
const spinner = ora();
38-
const currentVersion = program.version();
39-
40-
if (version) {
41-
spinner.start(`Checking if version ${version} exists`);
42-
const url = `https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip`;
43-
const response = await fetch(url, { method: "HEAD" });
44-
45-
if (response.status === 404) {
46-
spinner.fail(`Version ${version} does not exist.`);
47-
process.exit(1);
48-
}
49-
spinner.succeed();
50-
} else {
51-
const isOnLatestVersion = await getIsOnLatestVersion(currentVersion);
52-
if (isOnLatestVersion) {
53-
spinner.succeed(
54-
`You are already on the latest version (${currentVersion}).`,
55-
);
56-
process.exit(0);
57-
}
58-
}
59-
60-
// Fetch the install script
61-
spinner.start("Downloading install script");
62-
const scriptResponse = await fetch(
63-
"https://www.sfcompute.com/cli/install",
31+
export async function handleUpgrade(
32+
currentVersion?: string,
33+
version?: string,
34+
): Promise<boolean> {
35+
const spinner = ora();
36+
37+
if (version) {
38+
spinner.start(`Checking if version ${version} exists`);
39+
const url =
40+
`https://github.com/sfcompute/cli/archive/refs/tags/${version}.zip` as const;
41+
const response = await fetch(url, { method: "HEAD" });
42+
43+
if (response.status === 404) {
44+
spinner.fail(`Version ${version} does not exist.`);
45+
return false;
46+
}
47+
spinner.succeed();
48+
} else {
49+
const isOnLatestVersion = await getIsOnLatestVersion(currentVersion);
50+
if (isOnLatestVersion) {
51+
spinner.succeed(
52+
`You are already on the latest version (${currentVersion}).`,
6453
);
54+
return true;
55+
}
56+
}
57+
58+
// Fetch the install script
59+
spinner.start("Downloading install script");
60+
const scriptResponse = await fetch("https://www.sfcompute.com/cli/install");
61+
62+
if (!scriptResponse.ok) {
63+
spinner.fail("Failed to download install script.");
64+
return false;
65+
}
6566

66-
if (!scriptResponse.ok) {
67-
spinner.fail("Failed to download install script.");
68-
process.exit(1);
69-
}
67+
const script = await scriptResponse.text();
68+
spinner.succeed();
7069

71-
const script = await scriptResponse.text();
72-
spinner.succeed();
70+
// Execute the script with bash
71+
spinner.start("Installing upgrade");
7372

74-
// Execute the script with bash
75-
spinner.start("Installing upgrade");
73+
const bashProcess = spawn("bash", [], {
74+
stdio: ["pipe", "pipe", "pipe"],
75+
env: version ? { ...process.env, SF_CLI_VERSION: version } : process.env,
76+
});
7677

77-
const bashProcess = spawn("bash", [], {
78-
stdio: ["pipe", "pipe", "pipe"],
79-
env: version
80-
? { ...process.env, SF_CLI_VERSION: version }
81-
: process.env,
82-
});
78+
let stdout = "";
79+
let stderr = "";
8380

84-
let stdout = "";
85-
let stderr = "";
81+
bashProcess.stdout.on("data", (data) => {
82+
stdout += data.toString();
83+
});
8684

87-
bashProcess.stdout.on("data", (data) => {
88-
stdout += data.toString();
89-
});
85+
bashProcess.stderr.on("data", (data) => {
86+
stderr += data.toString();
87+
});
9088

91-
bashProcess.stderr.on("data", (data) => {
92-
stderr += data.toString();
93-
});
89+
bashProcess.stdin.write(script);
90+
bashProcess.stdin.end();
9491

95-
bashProcess.stdin.write(script);
96-
bashProcess.stdin.end();
92+
const code = await new Promise<number | null>((resolve) => {
93+
bashProcess.on("close", resolve);
94+
});
9795

98-
const code = await new Promise<number | null>((resolve) => {
99-
bashProcess.on("close", resolve);
100-
});
96+
if (code !== 0) {
97+
spinner.fail("Upgrade failed");
98+
console.error(stderr);
99+
console.log(stdout);
100+
return false;
101+
}
101102

102-
if (code !== 0) {
103-
spinner.fail("Upgrade failed");
104-
console.error(stderr);
105-
console.log(stdout);
106-
process.exit(1);
107-
}
103+
spinner.succeed("Upgrade completed successfully");
104+
return true;
105+
}
108106

109-
spinner.succeed("Upgrade completed successfully");
110-
process.exit(0);
107+
export function registerUpgrade(program: Command) {
108+
return program
109+
.command("upgrade")
110+
.argument("[version]", "The version to upgrade to")
111+
.description("Upgrade to the latest version or a specific version")
112+
.action(async (version) => {
113+
const success = await handleUpgrade(program.version(), version);
114+
process.exit(success ? 0 : 1);
111115
});
112116
}

0 commit comments

Comments
 (0)