index.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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-ai`.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("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. export async function upgrade(method: Method, target: string) {
  91. const cmd = (() => {
  92. switch (method) {
  93. case "curl":
  94. return $`curl -fsSL https://opencode.ai/install | bash`.env({
  95. ...process.env,
  96. VERSION: target,
  97. })
  98. case "npm":
  99. return $`npm install -g opencode-ai@${target}`
  100. case "pnpm":
  101. return $`pnpm install -g opencode-ai@${target}`
  102. case "bun":
  103. return $`bun install -g opencode-ai@${target}`
  104. case "brew":
  105. return $`brew install sst/tap/opencode`.env({
  106. HOMEBREW_NO_AUTO_UPDATE: "1",
  107. })
  108. default:
  109. throw new Error(`Unknown method: ${method}`)
  110. }
  111. })()
  112. const result = await cmd.quiet().throws(false)
  113. log.info("upgraded", {
  114. method,
  115. target,
  116. stdout: result.stdout.toString(),
  117. stderr: result.stderr.toString(),
  118. })
  119. if (result.exitCode !== 0)
  120. throw new UpgradeFailedError({
  121. stderr: result.stderr.toString("utf8"),
  122. })
  123. }
  124. export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
  125. export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
  126. export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
  127. export async function latest() {
  128. const [major] = VERSION.split(".").map((x) => Number(x))
  129. const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
  130. return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
  131. .then((res) => {
  132. if (!res.ok) throw new Error(res.statusText)
  133. return res.json()
  134. })
  135. .then((data: any) => data.version)
  136. }
  137. }