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 { - 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 -} +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> + + export const Info = z + .object({ + version: z.string(), + latest: z.string(), + }) + .openapi({ + ref: "InstallationInfo", + }) + export type Info = z.infer + + 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()}` : "") } />