2
0

thread.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { cmd } from "@/cli/cmd/cmd"
  2. import { tui } from "./app"
  3. import { Rpc } from "@/util/rpc"
  4. import { type rpc } from "./worker"
  5. import path from "path"
  6. import { UI } from "@/cli/ui"
  7. import { iife } from "@/util/iife"
  8. import { Log } from "@/util/log"
  9. declare global {
  10. const OPENCODE_WORKER_PATH: string
  11. }
  12. export const TuiThreadCommand = cmd({
  13. command: "$0 [project]",
  14. describe: "start opencode tui",
  15. builder: (yargs) =>
  16. yargs
  17. .positional("project", {
  18. type: "string",
  19. describe: "path to start opencode in",
  20. })
  21. .option("model", {
  22. type: "string",
  23. alias: ["m"],
  24. describe: "model to use in the format of provider/model",
  25. })
  26. .option("continue", {
  27. alias: ["c"],
  28. describe: "continue the last session",
  29. type: "boolean",
  30. })
  31. .option("session", {
  32. alias: ["s"],
  33. type: "string",
  34. describe: "session id to continue",
  35. })
  36. .option("prompt", {
  37. alias: ["p"],
  38. type: "string",
  39. describe: "prompt to use",
  40. })
  41. .option("agent", {
  42. type: "string",
  43. describe: "agent to use",
  44. })
  45. .option("port", {
  46. type: "number",
  47. describe: "port to listen on",
  48. default: 0,
  49. })
  50. .option("hostname", {
  51. type: "string",
  52. describe: "hostname to listen on",
  53. default: "127.0.0.1",
  54. }),
  55. handler: async (args) => {
  56. // Resolve relative paths against PWD to preserve behavior when using --cwd flag
  57. const baseCwd = process.env.PWD ?? process.cwd()
  58. const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
  59. const defaultWorker = new URL("./worker.ts", import.meta.url)
  60. // Nix build creates a bundled worker next to the binary; prefer it when present.
  61. const execDir = path.dirname(process.execPath)
  62. const bundledWorker = path.join(execDir, "opencode-worker.js")
  63. const hasBundledWorker = await Bun.file(bundledWorker).exists()
  64. const workerPath = (() => {
  65. if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
  66. if (hasBundledWorker) return bundledWorker
  67. return defaultWorker
  68. })()
  69. try {
  70. process.chdir(cwd)
  71. } catch (e) {
  72. UI.error("Failed to change directory to " + cwd)
  73. return
  74. }
  75. const worker = new Worker(workerPath, {
  76. env: Object.fromEntries(
  77. Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
  78. ),
  79. })
  80. worker.onerror = (e) => {
  81. Log.Default.error(e)
  82. }
  83. const client = Rpc.client<typeof rpc>(worker)
  84. process.on("uncaughtException", (e) => {
  85. Log.Default.error(e)
  86. })
  87. process.on("unhandledRejection", (e) => {
  88. Log.Default.error(e)
  89. })
  90. const server = await client.call("server", {
  91. port: args.port,
  92. hostname: args.hostname,
  93. })
  94. const prompt = await iife(async () => {
  95. const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
  96. if (!args.prompt) return piped
  97. return piped ? piped + "\n" + args.prompt : args.prompt
  98. })
  99. const tuiPromise = tui({
  100. url: server.url,
  101. args: {
  102. continue: args.continue,
  103. sessionID: args.session,
  104. agent: args.agent,
  105. model: args.model,
  106. prompt,
  107. },
  108. onExit: async () => {
  109. await client.call("shutdown", undefined)
  110. },
  111. })
  112. setTimeout(() => {
  113. client.call("checkUpgrade", { directory: cwd }).catch(() => {})
  114. }, 1000)
  115. await tuiPromise
  116. },
  117. })