fix(cli): routed npm self updates through npm
- Detected npm-owned global bin paths and Windows npm launcher shims before falling back to binary replacement. - Added npm install argv construction that pins the registry and native package versions. - Covered npm shim resolution and npm update arguments in update-cli tests. Fixes #5053
This commit is contained in:
@@ -109,6 +109,19 @@ async function getBunGlobalBinDir(): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getNpmGlobalBinDir(): Promise<string | undefined> {
|
||||
if (!$which("npm")) return undefined;
|
||||
try {
|
||||
const result = await $`npm prefix -g`.quiet().nothrow();
|
||||
if (result.exitCode !== 0) return undefined;
|
||||
const prefix = result.text().trim();
|
||||
if (prefix.length === 0) return undefined;
|
||||
return process.platform === "win32" ? prefix : path.join(prefix, "bin");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getHomebrewFormulaPrefix(): Promise<string | undefined> {
|
||||
if (!$which("brew")) return undefined;
|
||||
for (const formula of [HOMEBREW_FORMULA, APP_NAME]) {
|
||||
@@ -189,26 +202,36 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
||||
return isPathInDirectoryLexical(resolvedFile, dirReal);
|
||||
}
|
||||
|
||||
type UpdateMethod = "brew" | "mise" | "bun" | "binary";
|
||||
type UpdateMethod = "brew" | "mise" | "bun" | "npm" | "binary";
|
||||
|
||||
interface UpdateMethodResolutionOptions {
|
||||
homebrewPrefix?: string;
|
||||
miseBinDirs?: readonly string[];
|
||||
miseDataDir?: string;
|
||||
npmBinDir?: string;
|
||||
}
|
||||
|
||||
type UpdateTarget = { method: "brew" } | { method: "mise" } | { method: "bun" } | { method: "binary"; path: string };
|
||||
type UpdateTarget =
|
||||
| { method: "brew" }
|
||||
| { method: "mise" }
|
||||
| { method: "bun" }
|
||||
| { method: "npm" }
|
||||
| { method: "binary"; path: string };
|
||||
|
||||
function resolveUpdateMethod(
|
||||
ompPath: string,
|
||||
bunBinDir: string | undefined,
|
||||
options: UpdateMethodResolutionOptions = {},
|
||||
): UpdateMethod {
|
||||
const { homebrewPrefix, miseBinDirs = [], miseDataDir } = options;
|
||||
const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
|
||||
const launcherExtension = path.extname(ompPath).toLowerCase();
|
||||
const isWindowsScriptLauncher =
|
||||
launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
|
||||
if (homebrewPrefix && isPathInDirectory(ompPath, path.join(homebrewPrefix, "bin"))) return "brew";
|
||||
if (miseBinDirs.some(dir => isPathInDirectory(ompPath, dir))) return "mise";
|
||||
if (miseDataDir && isPathInDirectory(ompPath, path.join(miseDataDir, "shims"))) return "mise";
|
||||
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
|
||||
if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
|
||||
return "binary";
|
||||
}
|
||||
|
||||
@@ -221,6 +244,7 @@ export function resolveUpdateMethodForTest(
|
||||
}
|
||||
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
||||
const bunBinDir = await getBunGlobalBinDir();
|
||||
const npmBinDir = await getNpmGlobalBinDir();
|
||||
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
||||
const miseAvailable = $which("mise") !== undefined;
|
||||
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
||||
@@ -228,7 +252,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
||||
const ompPath = resolveOmpPath();
|
||||
|
||||
if (ompPath) {
|
||||
const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir });
|
||||
const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
|
||||
if (method === "binary") return { method, path: ompPath };
|
||||
return { method };
|
||||
}
|
||||
@@ -715,6 +739,14 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
||||
}
|
||||
}
|
||||
|
||||
function buildVersionedPackageInstallArgs(expectedVersion: string, nativeTag: string): string[] {
|
||||
const args = [`${PACKAGE}@${expectedVersion}`, `${NATIVES_PACKAGE}@${expectedVersion}`];
|
||||
if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
|
||||
args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the bun argv used to globally install a specific omp version.
|
||||
*
|
||||
@@ -746,17 +778,23 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
||||
* See #1824.
|
||||
*/
|
||||
export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
||||
const args = [
|
||||
return [
|
||||
"install",
|
||||
"-g",
|
||||
"--no-cache",
|
||||
`--registry=${NPM_REGISTRY}`,
|
||||
`${PACKAGE}@${expectedVersion}`,
|
||||
`${NATIVES_PACKAGE}@${expectedVersion}`,
|
||||
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
||||
];
|
||||
}
|
||||
|
||||
/** Build the npm argv used to update npm-managed global installs. */
|
||||
export function buildNpmInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
||||
const args = [
|
||||
"install",
|
||||
"-g",
|
||||
`--registry=${NPM_REGISTRY}`,
|
||||
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
||||
];
|
||||
if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
|
||||
args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -773,7 +811,7 @@ export function buildMiseForceInstallArgs(expectedVersion: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update via bun package manager.
|
||||
* Update via package manager.
|
||||
*/
|
||||
async function updateViaBun(expectedVersion: string): Promise<void> {
|
||||
console.log(chalk.dim("Updating via bun..."));
|
||||
@@ -794,6 +832,17 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateViaNpm(expectedVersion: string): Promise<void> {
|
||||
console.log(chalk.dim("Updating via npm..."));
|
||||
const args = buildNpmInstallArgs(expectedVersion);
|
||||
const result = await $`npm ${args}`.nothrow();
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`npm install failed with exit code ${result.exitCode}`);
|
||||
}
|
||||
|
||||
await printVerification(expectedVersion);
|
||||
}
|
||||
|
||||
async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
|
||||
console.log(chalk.dim("Updating Homebrew formulae..."));
|
||||
const update = await $`brew update`.nothrow();
|
||||
@@ -920,6 +969,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
||||
await updateViaMise(release.version, opts.force);
|
||||
} else if (target.method === "bun") {
|
||||
await updateViaBun(release.version);
|
||||
} else if (target.method === "npm") {
|
||||
await updateViaNpm(release.version);
|
||||
} else {
|
||||
await updateViaBinaryAt(target.path, release.version);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user