diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 65f3bab4d46..fcc1697eac4 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -50,18 +50,40 @@ export const UpgradeCommand = { return } + // Validate version exists before attempting upgrade + if (args.target) { + const spinner = prompts.spinner() + spinner.start("Checking version availability...") + const exists = await Installation.versionExists(target, method) + if (!exists) { + spinner.stop("Version not found", 1) + if (method === "brew") { + prompts.log.error(`Version ${target} is not available via brew. Only the latest version can be installed.`) + const latestVersion = await Installation.latest(method).catch(() => null) + if (latestVersion) { + prompts.log.info(`Available version: ${latestVersion}`) + } + } else { + prompts.log.error(`Version ${target} was not found. Please check the version number and try again.`) + } + prompts.outro("Done") + return + } + spinner.stop("Version available") + } + prompts.log.info(`From ${Installation.VERSION} → ${target}`) - const spinner = prompts.spinner() - spinner.start("Upgrading...") + const upgradeSpinner = prompts.spinner() + upgradeSpinner.start("Upgrading...") const err = await Installation.upgrade(method, target).catch((err) => err) if (err) { - spinner.stop("Upgrade failed", 1) + upgradeSpinner.stop("Upgrade failed", 1) if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr) else if (err instanceof Error) prompts.log.error(err.message) prompts.outro("Done") return } - spinner.stop("Upgrade complete") + upgradeSpinner.stop("Upgrade complete") prompts.outro("Done") }, } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 9e6dd2b9e92..49a58eeec20 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -202,4 +202,38 @@ export namespace Installation { }) .then((data: any) => data.tag_name.replace(/^v/, "")) } + + export const VersionNotFoundError = NamedError.create( + "VersionNotFoundError", + z.object({ + version: z.string(), + method: z.string(), + }), + ) + + export async function versionExists(version: string, installMethod?: Method): Promise { + const detectedMethod = installMethod || (await method()) + + if (detectedMethod === "brew") { + // Brew doesn't support installing specific versions easily + // Only the current formula version is available + const latestVersion = await latest(detectedMethod).catch(() => null) + return version === latestVersion + } + + if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { + const registry = await iife(async () => { + const r = (await $`npm config get registry`.quiet().nothrow().text()).trim() + const reg = r || "https://registry.npmjs.org" + return reg.endsWith("/") ? reg.slice(0, -1) : reg + }) + // Check if the specific version exists + const res = await fetch(`${registry}/opencode-ai/${version}`) + return res.ok + } + + // For curl method, check GitHub releases + const res = await fetch(`https://api.github.com/repos/anomalyco/opencode/releases/tags/v${version}`) + return res.ok + } }