2025-06-14 22:05:41 -04:00

185 lines
4.8 KiB
TypeScript

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<Release> {
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<Release> {
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<string> {
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) : await latest()
const target = release.tag_name
prompts.log.info(`Upgrade ${VERSION}${target}`)
if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
prompts.log.success(`Already up to date`)
prompts.outro("Done")
return
}
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...")
let downloadPath: string
try {
downloadPath = await download(found.browser_download_url)
spinner.stop("Download complete")
} catch (downloadError) {
spinner.stop("Download failed")
prompts.log.error(
`Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`,
)
prompts.outro("Done")
return
}
try {
await fs.rename(downloadPath, process.execPath)
prompts.log.success(`Successfully upgraded to ${target}`)
} catch (installError) {
prompts.log.error(
`Install failed: ${installError instanceof Error ? installError.message : String(installError)}`,
)
// Clean up downloaded file
await fs.unlink(downloadPath).catch(() => {})
}
prompts.outro("Done")
},
}