upgrade.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import type { Argv } from "yargs"
  2. import { UI } from "../ui"
  3. import { VERSION } from "../version"
  4. import path from "path"
  5. import fs from "fs/promises"
  6. import os from "os"
  7. import * as prompts from "@clack/prompts"
  8. import { Global } from "../../global"
  9. const API = "https://api.github.com/repos/sst/opencode"
  10. interface Release {
  11. tag_name: string
  12. name: string
  13. assets: Array<{
  14. name: string
  15. browser_download_url: string
  16. }>
  17. }
  18. function asset(): string {
  19. const platform = os.platform()
  20. const arch = os.arch()
  21. if (platform === "darwin") {
  22. return arch === "arm64"
  23. ? "opencode-darwin-arm64.zip"
  24. : "opencode-darwin-x64.zip"
  25. }
  26. if (platform === "linux") {
  27. return arch === "arm64"
  28. ? "opencode-linux-arm64.zip"
  29. : "opencode-linux-x64.zip"
  30. }
  31. if (platform === "win32") {
  32. return "opencode-windows-x64.zip"
  33. }
  34. throw new Error(`Unsupported platform: ${platform}-${arch}`)
  35. }
  36. function compare(current: string, latest: string): number {
  37. const a = current.replace(/^v/, "")
  38. const b = latest.replace(/^v/, "")
  39. const aParts = a.split(".").map(Number)
  40. const bParts = b.split(".").map(Number)
  41. for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
  42. const aPart = aParts[i] || 0
  43. const bPart = bParts[i] || 0
  44. if (aPart < bPart) return -1
  45. if (aPart > bPart) return 1
  46. }
  47. return 0
  48. }
  49. async function latest(): Promise<Release> {
  50. const response = await fetch(`${API}/releases/latest`)
  51. if (!response.ok) {
  52. throw new Error(`Failed to fetch latest release: ${response.statusText}`)
  53. }
  54. return response.json()
  55. }
  56. async function specific(version: string): Promise<Release> {
  57. const tag = version.startsWith("v") ? version : `v${version}`
  58. const response = await fetch(`${API}/releases/tags/${tag}`)
  59. if (!response.ok) {
  60. throw new Error(`Failed to fetch release ${tag}: ${response.statusText}`)
  61. }
  62. return response.json()
  63. }
  64. async function download(url: string): Promise<string> {
  65. const response = await fetch(url)
  66. if (!response.ok) {
  67. throw new Error(`Failed to download: ${response.statusText}`)
  68. }
  69. const buffer = await response.arrayBuffer()
  70. const temp = path.join(Global.Path.cache, `opencode-update-${Date.now()}.zip`)
  71. await Bun.write(temp, buffer)
  72. const extractDir = path.join(
  73. Global.Path.cache,
  74. `opencode-extract-${Date.now()}`,
  75. )
  76. await fs.mkdir(extractDir, { recursive: true })
  77. const proc = Bun.spawn(["unzip", "-o", temp, "-d", extractDir], {
  78. stdout: "pipe",
  79. stderr: "pipe",
  80. })
  81. const result = await proc.exited
  82. if (result !== 0) {
  83. throw new Error("Failed to extract update")
  84. }
  85. await fs.unlink(temp)
  86. const binary = path.join(extractDir, "opencode")
  87. await fs.chmod(binary, 0o755)
  88. return binary
  89. }
  90. export const UpgradeCommand = {
  91. command: "upgrade [target]",
  92. describe: "Upgrade opencode to the latest version or a specific version",
  93. builder: (yargs: Argv) => {
  94. return yargs.positional("target", {
  95. describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
  96. type: "string",
  97. })
  98. },
  99. handler: async (args: { target?: string }) => {
  100. UI.empty()
  101. UI.println(UI.logo(" "))
  102. UI.empty()
  103. prompts.intro("Upgrade")
  104. if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
  105. prompts.log.error(
  106. `opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
  107. )
  108. prompts.outro("Done")
  109. return
  110. }
  111. const release = args.target
  112. ? await specific(args.target).catch(() => {})
  113. : await latest().catch(() => {})
  114. if (!release) {
  115. prompts.log.error("Failed to fetch release information")
  116. prompts.outro("Done")
  117. return
  118. }
  119. const target = release.tag_name
  120. if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
  121. prompts.log.success(`Already up to date`)
  122. prompts.outro("Done")
  123. return
  124. }
  125. prompts.log.info(`From ${VERSION} → ${target}`)
  126. const name = asset()
  127. const found = release.assets.find((a) => a.name === name)
  128. if (!found) {
  129. prompts.log.error(`No binary found for platform: ${name}`)
  130. prompts.outro("Done")
  131. return
  132. }
  133. const spinner = prompts.spinner()
  134. spinner.start("Downloading update...")
  135. const downloadPath = await download(found.browser_download_url).catch(
  136. () => {},
  137. )
  138. if (!downloadPath) {
  139. spinner.stop("Download failed")
  140. prompts.log.error("Download failed")
  141. prompts.outro("Done")
  142. return
  143. }
  144. spinner.stop("Download complete")
  145. const renamed = await fs
  146. .rename(downloadPath, process.execPath)
  147. .catch(() => {})
  148. if (renamed === undefined) {
  149. prompts.log.error("Install failed")
  150. await fs.unlink(downloadPath).catch(() => {})
  151. prompts.outro("Done")
  152. return
  153. }
  154. prompts.log.success(`Successfully upgraded to ${target}`)
  155. prompts.outro("Done")
  156. },
  157. }