index.ts 4.4 KB

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