mirror of
https://github.com/sst/opencode.git
synced 2025-08-31 02:07:24 +00:00
Improve upgrade command with installation method detection (#158)
This commit is contained in:
parent
b929b4f4b9
commit
d054f88130
6 changed files with 153 additions and 122 deletions
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
*/
|
||||
},
|
||||
}
|
||||
|
|
1
packages/opencode/src/global/config.ts
Normal file
1
packages/opencode/src/global/config.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export namespace GlobalConfig {}
|
104
packages/opencode/src/installation/index.ts
Normal file
104
packages/opencode/src/installation/index.ts
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1518,7 +1518,7 @@ export default function Share(props: {
|
|||
desc={desc}
|
||||
data-size="sm"
|
||||
text={
|
||||
command + (result() ? `\n${result}` : "")
|
||||
command + (result() ? `\n${result()}` : "")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue