Merge pull request #4147 from NilsR0711/fix/bun-update-command

fix(gsd): use bun for update when installed via Bun
This commit is contained in:
Jeremy McSpadden 2026-04-13 18:03:03 -05:00 committed by GitHub
commit ef6abf48bc
5 changed files with 79 additions and 17 deletions

View file

@ -28,6 +28,11 @@ import { loadPrompt } from "./prompt-loader.js";
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
const UPDATE_FETCH_TIMEOUT_MS = 5000;
function resolveInstallCommand(pkg: string): string {
if ('bun' in process.versions) return `bun add -g ${pkg}`;
return `npm install -g ${pkg}`;
}
async function fetchLatestVersionForCommand(): Promise<string | null> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
@ -431,8 +436,9 @@ export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void>
ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
try {
execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
execSync(installCmd, {
stdio: ["ignore", "pipe", "ignore"],
});
ctx.ui.notify(
@ -441,7 +447,7 @@ export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void>
);
} catch {
ctx.ui.notify(
`Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest`,
`Update failed. Try manually: ${installCmd}`,
"error",
);
}

View file

@ -77,13 +77,9 @@ const ALLOW_HARDCODED_TMP: Array<[string, string]> = [
/** Pattern 4 — shell commands with interpolated variables */
const ALLOW_SHELL_INTERPOLATION: Array<[string, string]> = [
// NPM_PACKAGE is a compile-time constant ('gsd-pi'), not user input.
["update-cmd.ts", "npm view ${NPM_PACKAGE}"],
["update-cmd.ts", "npm install -g ${NPM_PACKAGE}"],
["update-check.ts", "npm install -g ${NPM_PACKAGE_NAME}"],
// Same constant forwarded through commands-handlers.
["resources/extensions/gsd/commands-handlers.ts", "npm view ${NPM_PACKAGE}"],
["resources/extensions/gsd/commands-handlers.ts", "npm install -g ${NPM_PACKAGE}"],
// update-cmd.ts, update-check.ts, and commands-handlers.ts all pass a
// pre-built variable (installCmd) to execSync — no template literal inside
// the execSync call, so no entries are needed here.
];
function isAllowlisted(

View file

@ -1,6 +1,7 @@
/**
* Regression test for #3445: gsd update must print both current and latest
* versions for diagnostics, and bypass npm cache.
* Regression test for #4145: gsd update must use bun when installed via Bun.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
@ -32,3 +33,52 @@ test("update commands use the registry fetch helper instead of npm view (#3806)"
);
assert.ok(!handlerSrc.includes("npm view "), "/gsd update should no longer shell out to npm view");
});
test("update-check exports resolveInstallCommand (#4145)", async () => {
const { resolveInstallCommand } = await import("../update-check.js");
assert.equal(typeof resolveInstallCommand, "function", "resolveInstallCommand must be exported from update-check");
});
test("resolveInstallCommand returns bun command when running under Bun (#4145)", async () => {
const { resolveInstallCommand } = await import("../update-check.js");
const orig = (process.versions as Record<string, string | undefined>).bun;
try {
(process.versions as Record<string, string | undefined>).bun = "1.0.0";
assert.equal(resolveInstallCommand("gsd-pi@latest"), "bun add -g gsd-pi@latest");
} finally {
if (orig === undefined) {
delete (process.versions as Record<string, string | undefined>).bun;
} else {
(process.versions as Record<string, string | undefined>).bun = orig;
}
}
});
test("resolveInstallCommand returns npm command when not running under Bun (#4145)", async () => {
const { resolveInstallCommand } = await import("../update-check.js");
const orig = (process.versions as Record<string, string | undefined>).bun;
try {
delete (process.versions as Record<string, string | undefined>).bun;
assert.equal(resolveInstallCommand("gsd-pi@latest"), "npm install -g gsd-pi@latest");
} finally {
if (orig !== undefined) {
(process.versions as Record<string, string | undefined>).bun = orig;
}
}
});
test("update-cmd uses resolveInstallCommand instead of hardcoded npm (#4145)", () => {
const src = readFileSync(join(__dirname, "..", "update-cmd.ts"), "utf-8");
assert.ok(
src.includes("resolveInstallCommand"),
"update-cmd should use resolveInstallCommand for package manager detection",
);
});
test("commands-handlers uses resolveInstallCommand instead of hardcoded npm (#4145)", () => {
const handlerSrc = readFileSync(join(__dirname, "..", "resources", "extensions", "gsd", "commands-handlers.ts"), "utf-8");
assert.ok(
handlerSrc.includes("resolveInstallCommand"),
"/gsd update handler should use resolveInstallCommand for package manager detection",
);
});

View file

@ -74,10 +74,18 @@ export async function fetchLatestVersionFromRegistry(
}
}
export function resolveInstallCommand(pkg: string): string {
if ('bun' in process.versions) {
return `bun add -g ${pkg}`
}
return `npm install -g ${pkg}`
}
function printUpdateBanner(current: string, latest: string): void {
const installCmd = resolveInstallCommand('gsd-pi')
process.stderr.write(
` ${chalk.yellow('Update available:')} ${chalk.dim(`v${current}`)}${chalk.bold(`v${latest}`)}\n` +
` ${chalk.dim('Run')} npm update -g gsd-pi ${chalk.dim('or')} /gsd update ${chalk.dim('to upgrade')}\n\n`,
` ${chalk.dim('Run')} ${installCmd} ${chalk.dim('or')} /gsd update ${chalk.dim('to upgrade')}\n\n`,
)
}
@ -184,7 +192,7 @@ export async function checkAndPromptForUpdates(options: UpdateCheckOptions = {})
const choice = await new Promise<string>((resolve) => {
process.stderr.write(
` ${chalk.bold('[1]')} Update now ${chalk.dim(`npm install -g ${NPM_PACKAGE_NAME}@latest`)}\n` +
` ${chalk.bold('[1]')} Update now ${chalk.dim(resolveInstallCommand(`${NPM_PACKAGE_NAME}@latest`))}\n` +
` ${chalk.bold('[2]')} Skip\n\n`,
)
@ -210,13 +218,14 @@ export async function checkAndPromptForUpdates(options: UpdateCheckOptions = {})
process.stdin.pause()
if (choice === '1') {
process.stderr.write(`\n ${chalk.dim('Running:')} npm install -g ${NPM_PACKAGE_NAME}@latest\n\n`)
const installCmd = resolveInstallCommand(`${NPM_PACKAGE_NAME}@latest`)
process.stderr.write(`\n ${chalk.dim('Running:')} ${installCmd}\n\n`)
try {
execSync(`npm install -g ${NPM_PACKAGE_NAME}@latest`, { stdio: 'inherit' })
execSync(installCmd, { stdio: 'inherit' })
process.stderr.write(`\n ${chalk.green.bold(`✓ Updated to v${latestVersion}`)}\n\n`)
return true
} catch {
process.stderr.write(`\n ${chalk.yellow(`Update failed. You can run: npm install -g ${NPM_PACKAGE_NAME}@latest`)}\n\n`)
process.stderr.write(`\n ${chalk.yellow(`Update failed. You can run: ${installCmd}`)}\n\n`)
}
} else {
process.stderr.write(` ${chalk.dim('Skipped. Run')} gsd update ${chalk.dim('anytime to upgrade.')}\n\n`)

View file

@ -1,5 +1,5 @@
import { execSync } from 'node:child_process'
import { compareSemver, fetchLatestVersionFromRegistry } from './update-check.js'
import { compareSemver, fetchLatestVersionFromRegistry, resolveInstallCommand } from './update-check.js'
const NPM_PACKAGE = 'gsd-pi'
@ -29,13 +29,14 @@ export async function runUpdate(): Promise<void> {
process.stdout.write(`${dim}Updating:${reset} v${current}${bold}v${latest}${reset}\n`)
const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`)
try {
execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
execSync(installCmd, {
stdio: 'inherit',
})
process.stdout.write(`\n${green}${bold}Updated to v${latest}${reset}\n`)
} catch {
process.stderr.write(`\n${yellow}Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest${reset}\n`)
process.stderr.write(`\n${yellow}Update failed. Try manually: ${installCmd}${reset}\n`)
process.exit(1)
}
}