| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- import path from "path"
- import { $ } from "bun"
- import z from "zod"
- import { NamedError } from "../util/error"
- import { Bus } from "../bus"
- import { Log } from "../util/log"
- declare global {
- const OPENCODE_VERSION: string
- const OPENCODE_CHANNEL: string
- }
- export namespace Installation {
- const log = Log.create({ service: "installation" })
- export type Method = Awaited<ReturnType<typeof method>>
- export const Event = {
- Updated: Bus.event(
- "installation.updated",
- z.object({
- version: z.string(),
- }),
- ),
- }
- export const Info = z
- .object({
- version: z.string(),
- latest: z.string(),
- })
- .meta({
- ref: "InstallationInfo",
- })
- export type Info = z.infer<typeof Info>
- export async function info() {
- return {
- version: VERSION,
- latest: await latest(),
- }
- }
- export function isPreview() {
- return CHANNEL !== "latest"
- }
- export function isLocal() {
- return CHANNEL === "local"
- }
- export async function method() {
- if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
- if (process.execPath.includes(path.join(".local", "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(),
- },
- {
- name: "brew" as const,
- command: () => $`brew list --formula opencode`.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(check.name === "brew" ? "opencode" : "opencode-ai")) {
- return check.name
- }
- }
- return "unknown"
- }
- export const UpgradeFailedError = NamedError.create(
- "UpgradeFailedError",
- z.object({
- stderr: z.string(),
- }),
- )
- async function getBrewFormula() {
- const tapFormula = await $`brew list --formula sst/tap/opencode`.throws(false).text()
- if (tapFormula.includes("opencode")) return "sst/tap/opencode"
- const coreFormula = await $`brew list --formula opencode`.throws(false).text()
- if (coreFormula.includes("opencode")) return "opencode"
- return "opencode"
- }
- export async function upgrade(method: Method, target: string) {
- let cmd
- switch (method) {
- case "curl":
- cmd = $`curl -fsSL https://opencode.ai/install | bash`.env({
- ...process.env,
- VERSION: target,
- })
- break
- case "npm":
- cmd = $`npm install -g opencode-ai@${target}`
- break
- case "pnpm":
- cmd = $`pnpm install -g opencode-ai@${target}`
- break
- case "bun":
- cmd = $`bun install -g opencode-ai@${target}`
- break
- case "brew": {
- const formula = await getBrewFormula()
- cmd = $`brew install ${formula}`.env({
- HOMEBREW_NO_AUTO_UPDATE: "1",
- ...process.env,
- })
- break
- }
- default:
- throw new Error(`Unknown method: ${method}`)
- }
- const result = await cmd.quiet().throws(false)
- log.info("upgraded", {
- method,
- target,
- stdout: result.stdout.toString(),
- stderr: result.stderr.toString(),
- })
- if (result.exitCode !== 0)
- throw new UpgradeFailedError({
- stderr: result.stderr.toString("utf8"),
- })
- }
- export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
- export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
- export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
- export async function latest() {
- const [major] = VERSION.split(".").map((x) => Number(x))
- const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
- return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
- .then((res) => {
- if (!res.ok) throw new Error(res.statusText)
- return res.json()
- })
- .then((data: any) => data.version)
- }
- }
|