index.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. }
  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. .openapi({
  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. const exec = process.execPath.toLowerCase()
  45. const checks = [
  46. {
  47. name: "npm" as const,
  48. command: () => $`npm list -g --depth=0`.throws(false).text(),
  49. },
  50. {
  51. name: "yarn" as const,
  52. command: () => $`yarn global list`.throws(false).text(),
  53. },
  54. {
  55. name: "pnpm" as const,
  56. command: () => $`pnpm list -g --depth=0`.throws(false).text(),
  57. },
  58. {
  59. name: "bun" as const,
  60. command: () => $`bun pm ls -g`.throws(false).text(),
  61. },
  62. {
  63. name: "brew" as const,
  64. command: () => $`brew list --formula opencode-ai`.throws(false).text(),
  65. },
  66. ]
  67. checks.sort((a, b) => {
  68. const aMatches = exec.includes(a.name)
  69. const bMatches = exec.includes(b.name)
  70. if (aMatches && !bMatches) return -1
  71. if (!aMatches && bMatches) return 1
  72. return 0
  73. })
  74. for (const check of checks) {
  75. const output = await check.command()
  76. if (output.includes("opencode-ai")) {
  77. return check.name
  78. }
  79. }
  80. return "unknown"
  81. }
  82. export const UpgradeFailedError = NamedError.create(
  83. "UpgradeFailedError",
  84. z.object({
  85. stderr: z.string(),
  86. }),
  87. )
  88. export async function upgrade(method: Method, target: string) {
  89. const cmd = (() => {
  90. switch (method) {
  91. case "curl":
  92. return $`curl -fsSL https://opencode.ai/install | bash`.env({
  93. ...process.env,
  94. VERSION: target,
  95. })
  96. case "npm":
  97. return $`npm install -g opencode-ai@${target}`
  98. case "pnpm":
  99. return $`pnpm install -g opencode-ai@${target}`
  100. case "bun":
  101. return $`bun install -g opencode-ai@${target}`
  102. case "brew":
  103. return $`brew install sst/tap/opencode`
  104. default:
  105. throw new Error(`Unknown method: ${method}`)
  106. }
  107. })()
  108. const result = await cmd.quiet().throws(false)
  109. log.info("upgraded", {
  110. method,
  111. target,
  112. stdout: result.stdout.toString(),
  113. stderr: result.stderr.toString(),
  114. })
  115. if (result.exitCode !== 0)
  116. throw new UpgradeFailedError({
  117. stderr: result.stderr.toString("utf8"),
  118. })
  119. }
  120. export const VERSION =
  121. typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
  122. export async function latest() {
  123. return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
  124. .then((res) => res.json())
  125. .then((data) => data.tag_name.slice(1) as string)
  126. }
  127. }