+17





![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)
![opencode-agent[bot]](/assets/img/avatar_default.png)
opencode-agent[bot]
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
LukeParkerDev
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
Brendan Allan
Aarav Sareen
Julian Coy
Brendan Allan
usrnk1
opencode
Vladimir Glafirov
Adam
Frank
Jay
Dustin Deus
Kit Langton
James Long
Simon Klee
Jay
Jack
David Hill
Aiden Cline
James Long
冯基魁
Aiden Cline
Victor Navarro
43ecf3ff1b
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com>
199 lines
6.3 KiB
TypeScript
199 lines
6.3 KiB
TypeScript
import { cmd } from "@/cli/cmd/cmd"
|
|
import { Rpc } from "@/util/rpc"
|
|
import { type rpc } from "../tui/worker"
|
|
import path from "path"
|
|
import { fileURLToPath } from "url"
|
|
import { UI } from "@/cli/ui"
|
|
import { errorMessage } from "@opencode-ai/tui/util/error"
|
|
import { withTimeout } from "@/util/timeout"
|
|
import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli/network"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { OpenCode } from "@opencode-ai/client/promise"
|
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
|
import { writeHeapSnapshot } from "v8"
|
|
import { ServerAuth } from "@/server/auth"
|
|
import { validateSession } from "../tui/validate-session"
|
|
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
|
|
|
|
declare global {
|
|
const OPENCODE_WORKER_PATH: string
|
|
}
|
|
|
|
async function target() {
|
|
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
|
const dist = new URL("./cli/tui/worker.js", import.meta.url)
|
|
if (await Filesystem.exists(fileURLToPath(dist))) return dist
|
|
return new URL("../tui/worker.ts", import.meta.url)
|
|
}
|
|
|
|
async function input(value?: string) {
|
|
const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text()
|
|
if (!value) return piped
|
|
if (!piped) return value
|
|
return piped + "\n" + value
|
|
}
|
|
|
|
export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
|
|
const root = Filesystem.resolve(envPWD ?? cwd)
|
|
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
|
|
return Filesystem.resolve(cwd)
|
|
}
|
|
|
|
export const TuiThreadCommand = cmd({
|
|
command: "$0 [project]",
|
|
describe: "start opencode tui",
|
|
builder: (yargs) =>
|
|
withNetworkOptions(yargs)
|
|
.positional("project", {
|
|
type: "string",
|
|
describe: "path to start opencode in",
|
|
})
|
|
.option("model", {
|
|
type: "string",
|
|
alias: ["m"],
|
|
describe: "model to use in the format of provider/model",
|
|
})
|
|
.option("continue", {
|
|
alias: ["c"],
|
|
describe: "continue the last session",
|
|
type: "boolean",
|
|
})
|
|
.option("session", {
|
|
alias: ["s"],
|
|
type: "string",
|
|
describe: "session id to continue",
|
|
})
|
|
.option("fork", {
|
|
type: "boolean",
|
|
describe: "fork the session when continuing (use with --continue or --session)",
|
|
})
|
|
.option("prompt", {
|
|
type: "string",
|
|
describe: "prompt to use",
|
|
})
|
|
.option("agent", {
|
|
type: "string",
|
|
describe: "agent to use",
|
|
})
|
|
.option("auto", {
|
|
type: "boolean",
|
|
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
|
default: false,
|
|
})
|
|
.option("yolo", {
|
|
type: "boolean",
|
|
hidden: true,
|
|
default: false,
|
|
})
|
|
.option("dangerously-skip-permissions", {
|
|
type: "boolean",
|
|
hidden: true,
|
|
default: false,
|
|
}),
|
|
handler: async (args) => {
|
|
const unguard = win32InstallCtrlCGuard()
|
|
try {
|
|
const { TuiConfig } = await import("@/config/tui")
|
|
if (args.fork && !args.continue && !args.session) {
|
|
UI.error("--fork requires --continue or --session")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
// Resolve relative --project paths from PWD, then use the real cwd after
|
|
// chdir so the thread and worker share the same directory key.
|
|
const next = resolveThreadDirectory(args.project)
|
|
const file = await target()
|
|
try {
|
|
process.chdir(next)
|
|
} catch {
|
|
UI.error("Failed to change directory to " + next)
|
|
return
|
|
}
|
|
const cwd = Filesystem.resolve(process.cwd())
|
|
|
|
const worker = new Worker(file, {
|
|
env: Object.fromEntries(
|
|
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
),
|
|
})
|
|
const client = Rpc.client<typeof rpc>(worker)
|
|
const reload = () => {
|
|
client.call("reload", undefined).catch(() => {})
|
|
}
|
|
process.on("SIGUSR2", reload)
|
|
|
|
let stopped = false
|
|
const stop = async () => {
|
|
if (stopped) return
|
|
stopped = true
|
|
process.off("SIGUSR2", reload)
|
|
await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {})
|
|
worker.terminate()
|
|
}
|
|
|
|
const prompt = await input(args.prompt)
|
|
const config = await TuiConfig.get()
|
|
|
|
const network = resolveNetworkOptionsNoConfig(args)
|
|
const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true
|
|
const headers = external ? ServerAuth.headers() : undefined
|
|
const url = (await client.call("server", network)).url
|
|
|
|
try {
|
|
await validateSession({
|
|
url,
|
|
sessionID: args.session,
|
|
directory: cwd,
|
|
headers,
|
|
})
|
|
} catch (error) {
|
|
UI.error(errorMessage(error))
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
setTimeout(() => {
|
|
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
|
}, 1000).unref?.()
|
|
|
|
try {
|
|
const { Effect } = await import("effect")
|
|
const { run } = await import("../tui/layer")
|
|
const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime")
|
|
await Effect.runPromise(
|
|
run({
|
|
// @ts-expect-error V1 does not consume the V2-only server input.
|
|
client: createOpencodeClient({ baseUrl: url, headers, directory: cwd }),
|
|
api: OpenCode.make({ baseUrl: url, headers }),
|
|
async onSnapshot() {
|
|
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
|
const server = await client.call("snapshot", undefined)
|
|
return [tui, server]
|
|
},
|
|
config,
|
|
pluginHost: createLegacyTuiPluginHost(),
|
|
args: {
|
|
continue: args.continue,
|
|
sessionID: args.session,
|
|
agent: args.agent,
|
|
model: args.model,
|
|
prompt,
|
|
fork: args.fork,
|
|
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
|
|
},
|
|
}),
|
|
)
|
|
} finally {
|
|
await stop()
|
|
}
|
|
} finally {
|
|
try {
|
|
unguard?.()
|
|
} catch {}
|
|
}
|
|
process.exit(0)
|
|
},
|
|
})
|
|
// scratch
|