tui.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { Global } from "../../global"
  2. import { Provider } from "../../provider/provider"
  3. import { Server } from "../../server/server"
  4. import { UI } from "../ui"
  5. import { cmd } from "./cmd"
  6. import path from "path"
  7. import fs from "fs/promises"
  8. import { Installation } from "../../installation"
  9. import { Config } from "../../config/config"
  10. import { Bus } from "../../bus"
  11. import { Log } from "../../util/log"
  12. import { Ide } from "../../ide"
  13. import { Flag } from "../../flag/flag"
  14. import { Session } from "../../session"
  15. import { $ } from "bun"
  16. import { bootstrap } from "../bootstrap"
  17. declare global {
  18. const OPENCODE_TUI_PATH: string
  19. }
  20. if (typeof OPENCODE_TUI_PATH !== "undefined") {
  21. await import(OPENCODE_TUI_PATH as string, {
  22. with: { type: "file" },
  23. })
  24. }
  25. export const TuiCommand = cmd({
  26. command: "$0 [project]",
  27. describe: "start opencode tui",
  28. builder: (yargs) =>
  29. yargs
  30. .positional("project", {
  31. type: "string",
  32. describe: "path to start opencode in",
  33. })
  34. .option("model", {
  35. type: "string",
  36. alias: ["m"],
  37. describe: "model to use in the format of provider/model",
  38. })
  39. .option("continue", {
  40. alias: ["c"],
  41. describe: "continue the last session",
  42. type: "boolean",
  43. })
  44. .option("session", {
  45. alias: ["s"],
  46. describe: "session id to continue",
  47. type: "string",
  48. })
  49. .option("prompt", {
  50. alias: ["p"],
  51. type: "string",
  52. describe: "prompt to use",
  53. })
  54. .option("agent", {
  55. type: "string",
  56. describe: "agent to use",
  57. })
  58. .option("port", {
  59. type: "number",
  60. describe: "port to listen on",
  61. default: 0,
  62. })
  63. .option("hostname", {
  64. type: "string",
  65. describe: "hostname to listen on",
  66. default: "127.0.0.1",
  67. }),
  68. handler: async (args) => {
  69. while (true) {
  70. const cwd = args.project ? path.resolve(args.project) : process.cwd()
  71. try {
  72. process.chdir(cwd)
  73. } catch (e) {
  74. UI.error("Failed to change directory to " + cwd)
  75. return
  76. }
  77. const result = await bootstrap(cwd, async () => {
  78. const sessionID = await (async () => {
  79. if (args.continue) {
  80. const it = Session.list()
  81. try {
  82. for await (const s of it) {
  83. if (s.parentID === undefined) {
  84. return s.id
  85. }
  86. }
  87. return
  88. } finally {
  89. await it.return()
  90. }
  91. }
  92. if (args.session) {
  93. return args.session
  94. }
  95. return undefined
  96. })()
  97. const providers = await Provider.list()
  98. if (Object.keys(providers).length === 0) {
  99. return "needs_provider"
  100. }
  101. const server = Server.listen({
  102. port: args.port,
  103. hostname: args.hostname,
  104. })
  105. let cmd = [] as string[]
  106. const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
  107. if (tui) {
  108. let binaryName = tui.name
  109. if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
  110. binaryName += ".exe"
  111. }
  112. const binary = path.join(Global.Path.cache, "tui", binaryName)
  113. const file = Bun.file(binary)
  114. if (!(await file.exists())) {
  115. await Bun.write(file, tui, { mode: 0o755 })
  116. if (process.platform !== "win32") await fs.chmod(binary, 0o755)
  117. }
  118. cmd = [binary]
  119. }
  120. if (!tui) {
  121. const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
  122. let binaryName = `./dist/tui${process.platform === "win32" ? ".exe" : ""}`
  123. await $`go build -o ${binaryName} ./main.go`.cwd(dir)
  124. cmd = [path.join(dir, binaryName)]
  125. }
  126. Log.Default.info("tui", {
  127. cmd,
  128. })
  129. const proc = Bun.spawn({
  130. cmd: [
  131. ...cmd,
  132. ...(args.model ? ["--model", args.model] : []),
  133. ...(args.prompt ? ["--prompt", args.prompt] : []),
  134. ...(args.agent ? ["--agent", args.agent] : []),
  135. ...(sessionID ? ["--session", sessionID] : []),
  136. ],
  137. cwd,
  138. stdout: "inherit",
  139. stderr: "inherit",
  140. stdin: "inherit",
  141. env: {
  142. ...process.env,
  143. CGO_ENABLED: "0",
  144. OPENCODE_SERVER: server.url.toString(),
  145. },
  146. onExit: () => {
  147. server.stop()
  148. },
  149. })
  150. ;(async () => {
  151. // if (Installation.isLocal()) return
  152. const config = await Config.get()
  153. if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
  154. const latest = await Installation.latest().catch(() => {})
  155. if (!latest) return
  156. if (Installation.VERSION === latest) return
  157. const method = await Installation.method()
  158. if (method === "unknown") return
  159. await Installation.upgrade(method, latest)
  160. .then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
  161. .catch(() => {})
  162. })()
  163. ;(async () => {
  164. if (Ide.alreadyInstalled()) return
  165. const ide = Ide.ide()
  166. if (ide === "unknown") return
  167. await Ide.install(ide)
  168. .then(() => Bus.publish(Ide.Event.Installed, { ide }))
  169. .catch(() => {})
  170. })()
  171. await proc.exited
  172. server.stop()
  173. return "done"
  174. })
  175. if (result === "done") break
  176. if (result === "needs_provider") {
  177. UI.empty()
  178. UI.println(UI.logo(" "))
  179. const result = await Bun.spawn({
  180. cmd: [...getOpencodeCommand(), "auth", "login"],
  181. cwd: process.cwd(),
  182. stdout: "inherit",
  183. stderr: "inherit",
  184. stdin: "inherit",
  185. }).exited
  186. if (result !== 0) return
  187. UI.empty()
  188. }
  189. }
  190. },
  191. })
  192. /**
  193. * Get the correct command to run opencode CLI
  194. * In development: ["bun", "run", "packages/opencode/src/index.ts"]
  195. * In production: ["/path/to/opencode"]
  196. */
  197. function getOpencodeCommand(): string[] {
  198. // Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts)
  199. if (process.env["OPENCODE_BIN_PATH"]) {
  200. return [process.env["OPENCODE_BIN_PATH"]]
  201. }
  202. const execPath = process.execPath.toLowerCase()
  203. if (Installation.isLocal()) {
  204. // In development, use bun to run the TypeScript entry point
  205. return [execPath, "run", process.argv[1]]
  206. }
  207. // In production, use the current executable path
  208. return [process.execPath]
  209. }