import type { Argv } from "yargs" import { UI } from "../ui" import { VERSION } from "../version" import path from "path" import fs from "fs/promises" import os from "os" import * as prompts from "@clack/prompts" import { Global } from "../../global" const API = "https://api.github.com/repos/sst/opencode" interface Release { tag_name: string name: string assets: Array<{ name: string browser_download_url: string }> } function asset(): string { const platform = os.platform() const arch = os.arch() if (platform === "darwin") { return arch === "arm64" ? "opencode-darwin-arm64.zip" : "opencode-darwin-x64.zip" } if (platform === "linux") { return arch === "arm64" ? "opencode-linux-arm64.zip" : "opencode-linux-x64.zip" } if (platform === "win32") { return "opencode-windows-x64.zip" } throw new Error(`Unsupported platform: ${platform}-${arch}`) } function compare(current: string, latest: string): number { const a = current.replace(/^v/, "") const b = latest.replace(/^v/, "") const aParts = a.split(".").map(Number) const bParts = b.split(".").map(Number) for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aPart = aParts[i] || 0 const bPart = bParts[i] || 0 if (aPart < bPart) return -1 if (aPart > bPart) return 1 } return 0 } async function latest(): Promise { const response = await fetch(`${API}/releases/latest`) if (!response.ok) { throw new Error(`Failed to fetch latest release: ${response.statusText}`) } return response.json() } async function specific(version: string): Promise { const tag = version.startsWith("v") ? version : `v${version}` const response = await fetch(`${API}/releases/tags/${tag}`) if (!response.ok) { throw new Error(`Failed to fetch release ${tag}: ${response.statusText}`) } return response.json() } async function download(url: string): Promise { const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to download: ${response.statusText}`) } const buffer = await response.arrayBuffer() const temp = path.join(Global.Path.cache, `opencode-update-${Date.now()}.zip`) await Bun.write(temp, buffer) const extractDir = path.join( Global.Path.cache, `opencode-extract-${Date.now()}`, ) await fs.mkdir(extractDir, { recursive: true }) const proc = Bun.spawn(["unzip", "-o", temp, "-d", extractDir], { stdout: "pipe", stderr: "pipe", }) const result = await proc.exited if (result !== 0) { throw new Error("Failed to extract update") } await fs.unlink(temp) const binary = path.join(extractDir, "opencode") await fs.chmod(binary, 0o755) return binary } export const UpgradeCommand = { command: "upgrade [target]", describe: "Upgrade opencode to the latest version or a specific version", builder: (yargs: Argv) => { return yargs.positional("target", { describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')", type: "string", }) }, handler: async (args: { target?: string }) => { UI.empty() UI.println(UI.logo(" ")) UI.empty() prompts.intro("Upgrade") if (!process.execPath.includes(path.join(".opencode", "bin")) && false) { prompts.log.error( `opencode is installed to ${process.execPath} and seems to be managed by a package manager`, ) prompts.outro("Done") return } const release = args.target ? await specific(args.target).catch(() => {}) : await latest().catch(() => {}) if (!release) { prompts.log.error("Failed to fetch release information") prompts.outro("Done") return } const target = release.tag_name if (VERSION !== "dev" && compare(VERSION, target) >= 0) { prompts.log.success(`Already up to date`) prompts.outro("Done") return } prompts.log.info(`From ${VERSION} → ${target}`) const name = asset() const found = release.assets.find((a) => a.name === name) if (!found) { prompts.log.error(`No binary found for platform: ${name}`) prompts.outro("Done") return } const spinner = prompts.spinner() spinner.start("Downloading update...") const downloadPath = await download(found.browser_download_url).catch( () => {}, ) if (!downloadPath) { spinner.stop("Download failed") prompts.log.error("Download failed") prompts.outro("Done") return } spinner.stop("Download complete") const renamed = await fs .rename(downloadPath, process.execPath) .catch(() => {}) if (renamed === undefined) { prompts.log.error("Install failed") await fs.unlink(downloadPath).catch(() => {}) prompts.outro("Done") return } prompts.log.success(`Successfully upgraded to ${target}`) prompts.outro("Done") }, }