index.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import "zod-openapi/extend"
  2. import { App } from "./app/app"
  3. import { Server } from "./server/server"
  4. import fs from "fs/promises"
  5. import path from "path"
  6. import { Share } from "./share/share"
  7. import { Global } from "./global"
  8. import yargs from "yargs"
  9. import { hideBin } from "yargs/helpers"
  10. import { RunCommand } from "./cli/cmd/run"
  11. import { GenerateCommand } from "./cli/cmd/generate"
  12. import { ScrapCommand } from "./cli/cmd/scrap"
  13. import { Log } from "./util/log"
  14. import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
  15. import { UpgradeCommand } from "./cli/cmd/upgrade"
  16. import { Provider } from "./provider/provider"
  17. import { UI } from "./cli/ui"
  18. import { Installation } from "./installation"
  19. import { Bus } from "./bus"
  20. import { Config } from "./config/config"
  21. import { NamedError } from "./util/error"
  22. import { FormatError } from "./cli/error"
  23. const cancel = new AbortController()
  24. const cli = yargs(hideBin(process.argv))
  25. .scriptName("opencode")
  26. .help("help", "show help")
  27. .alias("help", "h")
  28. .version("version", "show version number", Installation.VERSION)
  29. .alias("version", "v")
  30. .option("print-logs", {
  31. describe: "print logs to stderr",
  32. type: "boolean",
  33. })
  34. .middleware(async () => {
  35. await Log.init({ print: process.argv.includes("--print-logs") })
  36. Log.Default.info("opencode", {
  37. version: Installation.VERSION,
  38. args: process.argv.slice(2),
  39. })
  40. })
  41. .usage("\n" + UI.logo())
  42. .command({
  43. command: "$0 [project]",
  44. describe: "start opencode tui",
  45. builder: (yargs) =>
  46. yargs.positional("project", {
  47. type: "string",
  48. describe: "path to start opencode in",
  49. }),
  50. handler: async (args) => {
  51. while (true) {
  52. const cwd = args.project ? path.resolve(args.project) : process.cwd()
  53. process.chdir(cwd)
  54. const result = await App.provide({ cwd }, async (app) => {
  55. const providers = await Provider.list()
  56. if (Object.keys(providers).length === 0) {
  57. return "needs_provider"
  58. }
  59. await Share.init()
  60. const server = Server.listen()
  61. let cmd = ["go", "run", "./main.go"]
  62. let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
  63. if (Bun.embeddedFiles.length > 0) {
  64. const blob = Bun.embeddedFiles[0] as File
  65. const binary = path.join(Global.Path.cache, "tui", blob.name)
  66. const file = Bun.file(binary)
  67. if (!(await file.exists())) {
  68. await Bun.write(file, blob, { mode: 0o755 })
  69. await fs.chmod(binary, 0o755)
  70. }
  71. cwd = process.cwd()
  72. cmd = [binary]
  73. }
  74. const proc = Bun.spawn({
  75. cmd: [...cmd, ...process.argv.slice(2)],
  76. signal: cancel.signal,
  77. cwd,
  78. stdout: "inherit",
  79. stderr: "inherit",
  80. stdin: "inherit",
  81. env: {
  82. ...process.env,
  83. OPENCODE_SERVER: server.url.toString(),
  84. OPENCODE_APP_INFO: JSON.stringify(app),
  85. },
  86. onExit: () => {
  87. server.stop()
  88. },
  89. })
  90. ;(async () => {
  91. if (Installation.VERSION === "dev") return
  92. if (Installation.isSnapshot()) return
  93. const config = await Config.global()
  94. if (config.autoupdate === false) return
  95. const latest = await Installation.latest().catch(() => {})
  96. if (!latest) return
  97. if (Installation.VERSION === latest) return
  98. const method = await Installation.method()
  99. if (method === "unknown") return
  100. await Installation.upgrade(method, latest)
  101. .then(() => {
  102. Bus.publish(Installation.Event.Updated, { version: latest })
  103. })
  104. .catch(() => {})
  105. })()
  106. await proc.exited
  107. server.stop()
  108. return "done"
  109. })
  110. if (result === "done") break
  111. if (result === "needs_provider") {
  112. UI.empty()
  113. UI.println(UI.logo(" "))
  114. UI.empty()
  115. await AuthLoginCommand.handler(args)
  116. }
  117. }
  118. },
  119. })
  120. .command(RunCommand)
  121. .command(GenerateCommand)
  122. .command(ScrapCommand)
  123. .command(AuthCommand)
  124. .command(UpgradeCommand)
  125. .fail((msg) => {
  126. if (
  127. msg.startsWith("Unknown argument") ||
  128. msg.startsWith("Not enough non-option arguments")
  129. ) {
  130. cli.showHelp("log")
  131. }
  132. })
  133. .strict()
  134. try {
  135. await cli.parse()
  136. } catch (e) {
  137. const data: Record<string, any> = {}
  138. if (e instanceof NamedError) {
  139. const obj = e.toObject()
  140. Object.assign(data, {
  141. ...obj.data,
  142. })
  143. }
  144. if (e instanceof Error) {
  145. Object.assign(data, {
  146. name: e.name,
  147. message: e.message,
  148. cause: e.cause?.toString(),
  149. })
  150. }
  151. Log.Default.error("fatal", data)
  152. const formatted = FormatError(e)
  153. if (formatted) UI.error(formatted)
  154. if (!formatted)
  155. UI.error(
  156. "Unexpected error, check log file at " + Log.file() + " for more details",
  157. )
  158. }
  159. cancel.abort()