index.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import path from "path"
  4. import { $ } from "bun"
  5. import z from "zod"
  6. import { NamedError } from "@opencode-ai/util/error"
  7. import { Log } from "../util/log"
  8. import { iife } from "@/util/iife"
  9. import { Flag } from "../flag/flag"
  10. declare global {
  11. const OPENCODE_VERSION: string
  12. const OPENCODE_CHANNEL: string
  13. }
  14. export namespace Installation {
  15. const log = Log.create({ service: "installation" })
  16. export type Method = Awaited<ReturnType<typeof method>>
  17. export const Event = {
  18. Updated: BusEvent.define(
  19. "installation.updated",
  20. z.object({
  21. version: z.string(),
  22. }),
  23. ),
  24. UpdateAvailable: BusEvent.define(
  25. "installation.update-available",
  26. z.object({
  27. version: z.string(),
  28. }),
  29. ),
  30. }
  31. export const Info = z
  32. .object({
  33. version: z.string(),
  34. latest: z.string(),
  35. })
  36. .meta({
  37. ref: "InstallationInfo",
  38. })
  39. export type Info = z.infer<typeof Info>
  40. export async function info() {
  41. return {
  42. version: VERSION,
  43. latest: await latest(),
  44. }
  45. }
  46. export function isPreview() {
  47. return CHANNEL !== "latest"
  48. }
  49. export function isLocal() {
  50. return CHANNEL === "local"
  51. }
  52. export async function method() {
  53. if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
  54. if (process.execPath.includes(path.join(".local", "bin"))) return "curl"
  55. const exec = process.execPath.toLowerCase()
  56. const checks = [
  57. {
  58. name: "npm" as const,
  59. command: () => $`npm list -g --depth=0`.throws(false).text(),
  60. },
  61. {
  62. name: "yarn" as const,
  63. command: () => $`yarn global list`.throws(false).text(),
  64. },
  65. {
  66. name: "pnpm" as const,
  67. command: () => $`pnpm list -g --depth=0`.throws(false).text(),
  68. },
  69. {
  70. name: "bun" as const,
  71. command: () => $`bun pm ls -g`.throws(false).text(),
  72. },
  73. {
  74. name: "brew" as const,
  75. command: () => $`brew list --formula opencode`.throws(false).text(),
  76. },
  77. ]
  78. checks.sort((a, b) => {
  79. const aMatches = exec.includes(a.name)
  80. const bMatches = exec.includes(b.name)
  81. if (aMatches && !bMatches) return -1
  82. if (!aMatches && bMatches) return 1
  83. return 0
  84. })
  85. for (const check of checks) {
  86. const output = await check.command()
  87. if (output.includes(check.name === "brew" ? "opencode" : "opencode-ai")) {
  88. return check.name
  89. }
  90. }
  91. return "unknown"
  92. }
  93. export const UpgradeFailedError = NamedError.create(
  94. "UpgradeFailedError",
  95. z.object({
  96. stderr: z.string(),
  97. }),
  98. )
  99. async function getBrewFormula() {
  100. const tapFormula = await $`brew list --formula sst/tap/opencode`.throws(false).text()
  101. if (tapFormula.includes("opencode")) return "sst/tap/opencode"
  102. const coreFormula = await $`brew list --formula opencode`.throws(false).text()
  103. if (coreFormula.includes("opencode")) return "opencode"
  104. return "opencode"
  105. }
  106. export async function upgrade(method: Method, target: string) {
  107. let cmd
  108. switch (method) {
  109. case "curl":
  110. cmd = $`curl -fsSL https://opencode.ai/install | bash`.env({
  111. ...process.env,
  112. VERSION: target,
  113. })
  114. break
  115. case "npm":
  116. cmd = $`npm install -g opencode-ai@${target}`
  117. break
  118. case "pnpm":
  119. cmd = $`pnpm install -g opencode-ai@${target}`
  120. break
  121. case "bun":
  122. cmd = $`bun install -g opencode-ai@${target}`
  123. break
  124. case "brew": {
  125. const formula = await getBrewFormula()
  126. cmd = $`brew install ${formula}`.env({
  127. HOMEBREW_NO_AUTO_UPDATE: "1",
  128. ...process.env,
  129. })
  130. break
  131. }
  132. default:
  133. throw new Error(`Unknown method: ${method}`)
  134. }
  135. const result = await cmd.quiet().throws(false)
  136. log.info("upgraded", {
  137. method,
  138. target,
  139. stdout: result.stdout.toString(),
  140. stderr: result.stderr.toString(),
  141. })
  142. if (result.exitCode !== 0)
  143. throw new UpgradeFailedError({
  144. stderr: result.stderr.toString("utf8"),
  145. })
  146. }
  147. export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
  148. export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
  149. export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
  150. export async function latest(installMethod?: Method) {
  151. const detectedMethod = installMethod || (await method())
  152. if (detectedMethod === "brew") {
  153. const formula = await getBrewFormula()
  154. if (formula === "opencode") {
  155. return fetch("https://formulae.brew.sh/api/formula/opencode.json")
  156. .then((res) => {
  157. if (!res.ok) throw new Error(res.statusText)
  158. return res.json()
  159. })
  160. .then((data: any) => data.versions.stable)
  161. }
  162. }
  163. const registry = await iife(async () => {
  164. const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
  165. const reg = r || "https://registry.npmjs.org"
  166. return reg.endsWith("/") ? reg.slice(0, -1) : reg
  167. })
  168. const [major] = VERSION.split(".").map((x) => Number(x))
  169. // const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
  170. const channel = CHANNEL
  171. return fetch(`${registry}/opencode-ai/${channel}`)
  172. .then((res) => {
  173. if (!res.ok) throw new Error(res.statusText)
  174. return res.json()
  175. })
  176. .then((data: any) => data.version)
  177. }
  178. }