summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2025-06-17 00:07:17 -0400
committerGitHub <[email protected]>2025-06-17 00:07:17 -0400
commitd054f88130b64cc3a6bbcc41795873b0e47dcce5 (patch)
tree427b015b31994ff6f6b7fd079c15626b46211812
parentb929b4f4b901343eaae3c4fae8033e090848f0a2 (diff)
downloadopencode-d054f88130b64cc3a6bbcc41795873b0e47dcce5.tar.gz
opencode-d054f88130b64cc3a6bbcc41795873b0e47dcce5.zip
Improve upgrade command with installation method detection (#158)
-rw-r--r--opencode.json15
-rw-r--r--packages/opencode/src/cli/cmd/upgrade.ts133
-rw-r--r--packages/opencode/src/global/config.ts1
-rw-r--r--packages/opencode/src/installation/index.ts104
-rw-r--r--packages/opencode/src/server/server.ts20
-rw-r--r--packages/web/src/components/Share.tsx2
6 files changed, 153 insertions, 122 deletions
diff --git a/opencode.json b/opencode.json
index 691bd372f..e20209c66 100644
--- a/opencode.json
+++ b/opencode.json
@@ -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": {}
}
diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts
index c5e60f3a5..2310becab 100644
--- a/packages/opencode/src/cli/cmd/upgrade.ts
+++ b/packages/opencode/src/cli/cmd/upgrade.ts
@@ -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")
+ */
},
}
diff --git a/packages/opencode/src/global/config.ts b/packages/opencode/src/global/config.ts
new file mode 100644
index 000000000..133cd3814
--- /dev/null
+++ b/packages/opencode/src/global/config.ts
@@ -0,0 +1 @@
+export namespace GlobalConfig {}
diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts
new file mode 100644
index 000000000..6bc245d9f
--- /dev/null
+++ b/packages/opencode/src/installation/index.ts
@@ -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))
+ }
+}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 275a0e2ba..30c761733 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -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
}
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index 88933b5b2..03fa89d1c 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -1518,7 +1518,7 @@ export default function Share(props: {
desc={desc}
data-size="sm"
text={
- command + (result() ? `\n${result}` : "")
+ command + (result() ? `\n${result()}` : "")
}
/>
</div>