index.ts 3.9 KB

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