Improve upgrade command with installation method detection (#158)

This commit is contained in:
Dax 2025-06-17 00:07:17 -04:00 committed by GitHub
parent b929b4f4b9
commit d054f88130
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 153 additions and 122 deletions

View file

@ -1,16 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"qwen3": {},
"deepseek-r1": {},
"llama2": {}
}
}
}
"mcp": {},
"provider": {}
}

View file

@ -1,113 +1,8 @@
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
}
import { Installation } from "../../installation"
export const UpgradeCommand = {
command: "upgrade [target]",
@ -123,14 +18,35 @@ export const UpgradeCommand = {
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Upgrade")
if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
const method = await Installation.method()
if (method === "unknown") {
prompts.log.error(
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
)
prompts.outro("Done")
return
}
const target = args.target ?? (await Installation.latest())
prompts.log.info(`From ${VERSION}${target}`)
const spinner = prompts.spinner()
spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) {
spinner.stop("Upgrade failed")
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")
prompts.outro("Done")
return
/*
if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
return
}
const release = args.target
? await specific(args.target).catch(() => {})
@ -188,5 +104,6 @@ export const UpgradeCommand = {
prompts.log.success(`Successfully upgraded to ${target}`)
prompts.outro("Done")
*/
},
}

View file

@ -0,0 +1 @@
export namespace GlobalConfig {}

View file

@ -0,0 +1,104 @@
import path from "path"
import { $ } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
export namespace Installation {
export type Method = Awaited<ReturnType<typeof method>>
export const Info = z
.object({
version: z.string(),
latest: z.string(),
})
.openapi({
ref: "InstallationInfo",
})
export type Info = z.infer<typeof Info>
export async function info() {
return {
version: VERSION,
latest: await latest(),
}
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
const checks = [
{
name: "npm" as const,
command: () => $`npm list -g --depth=0`.throws(false).text(),
},
{
name: "yarn" as const,
command: () => $`yarn global list`.throws(false).text(),
},
{
name: "pnpm" as const,
command: () => $`pnpm list -g --depth=0`.throws(false).text(),
},
{
name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).text(),
},
]
checks.sort((a, b) => {
const aMatches = exec.includes(a.name)
const bMatches = exec.includes(b.name)
if (aMatches && !bMatches) return -1
if (!aMatches && bMatches) return 1
return 0
})
for (const check of checks) {
const output = await check.command()
if (output.includes("opencode-ai")) {
return check.name
}
}
return "unknown"
}
export const UpgradeFailedError = NamedError.create(
"UpgradeFailedError",
z.object({
stderr: z.string(),
}),
)
export async function upgrade(method: Method, target: string) {
const cmd = (() => {
switch (method) {
case "curl":
return $`curl -fsSL https://opencode.ai/install | bash`
case "npm":
return $`npm install -g opencode-ai@${target}`
case "pnpm":
return $`pnpm install -g opencode-ai@${target}`
case "bun":
return $`bun install -g opencode-ai@${target}`
default:
throw new Error(`Unknown method: ${method}`)
}
})()
const result = await cmd.quiet().throws(false)
if (result.exitCode !== 0)
throw new UpgradeFailedError({
stderr: result.stderr.toString("utf8"),
})
}
export const VERSION =
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json())
.then((data) => data.tag_name.slice(1))
}
}

View file

@ -15,6 +15,7 @@ import { NamedError } from "../util/error"
import { Fzf } from "../external/fzf"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../external/ripgrep"
import { Installation } from "../installation"
const ERRORS = {
400: {
@ -466,6 +467,25 @@ export namespace Server {
return c.json(result)
},
)
.post(
"installation_info",
describeRoute({
description: "Get installation info",
responses: {
200: {
description: "Get installation info",
content: {
"application/json": {
schema: resolver(Installation.Info),
},
},
},
},
}),
async (c) => {
return c.json(Installation.info())
},
)
return result
}

View file

@ -1518,7 +1518,7 @@ export default function Share(props: {
desc={desc}
data-size="sm"
text={
command + (result() ? `\n${result}` : "")
command + (result() ? `\n${result()}` : "")
}
/>
</div>