Fix shutdown handling, error management, and process lifecycle issues

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
This commit is contained in:
Dax Raad
2025-06-10 18:58:47 -04:00
parent ca3c22dc12
commit 28f5cbbfe9
7 changed files with 89 additions and 66 deletions

View File

@@ -97,7 +97,7 @@ export namespace App {
log.info("registering service", { name: key }) log.info("registering service", { name: key })
services.set(key, { services.set(key, {
state: init(app.info), state: init(app.info),
shutdown: shutdown, shutdown,
}) })
} }
return services.get(key)?.state as State return services.get(key)?.state as State
@@ -108,14 +108,15 @@ export namespace App {
return ctx.use().info return ctx.use().info
} }
export async function provide<T extends (app: Info) => any>( export async function provide<T>(
input: { cwd: string; version: string }, input: { cwd: string; version: string },
cb: T, cb: (app: Info) => Promise<T>,
) { ) {
const app = await create(input) const app = await create(input)
return ctx.provide(app, async () => { return ctx.provide(app, async () => {
const result = await cb(app.info) const result = await cb(app.info)
for (const [key, entry] of app.services.entries()) { for (const [key, entry] of app.services.entries()) {
if (!entry.shutdown) continue
log.info("shutdown", { name: key }) log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state) await entry.shutdown?.(await entry.state)
} }

View File

@@ -6,6 +6,7 @@ import * as prompts from "@clack/prompts"
import open from "open" import open from "open"
import { VERSION } from "../version" import { VERSION } from "../version"
import { Provider } from "../../provider/provider" import { Provider } from "../../provider/provider"
import { UI } from "../ui"
export const ProviderCommand = cmd({ export const ProviderCommand = cmd({
command: "provider", command: "provider",
@@ -62,7 +63,7 @@ export const ProviderAddCommand = cmd({
}, },
], ],
}) })
if (prompts.isCancel(provider)) return if (prompts.isCancel(provider)) throw new UI.CancelledError({})
if (provider === "anthropic") { if (provider === "anthropic") {
const method = await prompts.select({ const method = await prompts.select({
@@ -78,7 +79,7 @@ export const ProviderAddCommand = cmd({
}, },
], ],
}) })
if (prompts.isCancel(method)) return if (prompts.isCancel(method)) throw new UI.CancelledError({})
if (method === "oauth") { if (method === "oauth") {
// some weird bug where program exits without this // some weird bug where program exits without this
@@ -92,7 +93,8 @@ export const ProviderAddCommand = cmd({
message: "Paste the authorization code here: ", message: "Paste the authorization code here: ",
validate: (x) => (x.length > 0 ? undefined : "Required"), validate: (x) => (x.length > 0 ? undefined : "Required"),
}) })
if (prompts.isCancel(code)) return if (prompts.isCancel(code)) throw new UI.CancelledError({})
await AuthAnthropic.exchange(code, verifier) await AuthAnthropic.exchange(code, verifier)
.then(() => { .then(() => {
prompts.log.success("Login successful") prompts.log.success("Login successful")
@@ -109,7 +111,7 @@ export const ProviderAddCommand = cmd({
message: "Enter your API key", message: "Enter your API key",
validate: (x) => (x.length > 0 ? undefined : "Required"), validate: (x) => (x.length > 0 ? undefined : "Required"),
}) })
if (prompts.isCancel(key)) return if (prompts.isCancel(key)) throw new UI.CancelledError({})
await AuthKeys.set(provider, key) await AuthKeys.set(provider, key)
prompts.outro("Done") prompts.outro("Done")

View File

@@ -1,4 +1,5 @@
import { VERSION } from "./version" import { z } from "zod"
import { NamedError } from "../util/error"
export namespace UI { export namespace UI {
const LOGO = [ const LOGO = [
@@ -7,6 +8,11 @@ export namespace UI {
`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`, `▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`,
] ]
export const CancelledError = NamedError.create(
"UICancelledError",
z.object({}),
)
export const Style = { export const Style = {
TEXT_HIGHLIGHT: "\x1b[96m", TEXT_HIGHLIGHT: "\x1b[96m",
TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m", TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",

View File

@@ -21,62 +21,76 @@ import { UI } from "./cli/ui"
await Log.init({ print: process.argv.includes("--print-logs") }) await Log.init({ print: process.argv.includes("--print-logs") })
yargs(hideBin(process.argv)) try {
.scriptName("opencode") await yargs(hideBin(process.argv))
.version(VERSION) .scriptName("opencode")
.command({ .version(VERSION)
command: "$0", .command({
describe: "Start OpenCode TUI", command: "$0",
builder: (yargs) => describe: "Start OpenCode TUI",
yargs.option("print-logs", { handler: async (args) => {
type: "boolean", while (true) {
}), const result = await App.provide(
handler: async (args) => { { cwd: process.cwd(), version: VERSION },
UI.logo() async () => {
await App.provide({ cwd: process.cwd(), version: VERSION }, async () => { const providers = await Provider.list()
const providers = await Provider.list() if (Object.keys(providers).length === 0) {
if (Object.keys(providers).length === 0) { return "needs_provider"
await ProviderAddCommand.handler(args) }
return
}
await Share.init() await Share.init()
const server = Server.listen() const server = Server.listen()
let cmd = ["go", "run", "./main.go"] let cmd = ["go", "run", "./main.go"]
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname let cwd = new URL("../../tui/cmd/opencode", import.meta.url)
if (Bun.embeddedFiles.length > 0) { .pathname
const blob = Bun.embeddedFiles[0] as File if (Bun.embeddedFiles.length > 0) {
const binary = path.join(Global.Path.cache, "tui", blob.name) const blob = Bun.embeddedFiles[0] as File
const file = Bun.file(binary) const binary = path.join(Global.Path.cache, "tui", blob.name)
if (!(await file.exists())) { const file = Bun.file(binary)
await Bun.write(file, blob, { mode: 0o755 }) if (!(await file.exists())) {
await fs.chmod(binary, 0o755) await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
},
onExit: () => {
server.stop()
},
})
await proc.exited
await server.stop()
return "done"
},
)
if (result === "done") break
if (result === "needs_provider") {
UI.logo()
await ProviderAddCommand.handler(args)
} }
cwd = process.cwd()
cmd = [binary]
} }
const proc = Bun.spawn({ },
cmd, })
cwd, .command(RunCommand)
stdout: "inherit", .command(GenerateCommand)
stderr: "inherit", .command(ScrapCommand)
stdin: "inherit", .command(ProviderCommand)
env: { .fail((msg, err) => {
...process.env, Log.Default.error(msg)
OPENCODE_SERVER: server.url.toString(), })
}, .parse()
onExit: () => { } catch (e) {
server.stop() Log.Default.error(e)
}, }
})
await proc.exited
await server.stop()
})
},
})
.command(RunCommand)
.command(GenerateCommand)
.command(ScrapCommand)
.command(ProviderCommand)
.parse()

View File

@@ -169,7 +169,7 @@ export namespace LSPClient {
log.info("shutting down") log.info("shutting down")
connection.end() connection.end()
connection.dispose() connection.dispose()
server.process.kill() server.process.kill("SIGKILL")
}, },
} }

View File

@@ -32,7 +32,7 @@ export namespace LSPServer {
".cts", ".cts",
], ],
async spawn(app) { async spawn(app) {
const tsserver = Bun.resolve( const tsserver = await Bun.resolve(
"typescript/lib/tsserver.js", "typescript/lib/tsserver.js",
app.path.cwd, app.path.cwd,
).catch(() => {}) ).catch(() => {})

View File

@@ -2,7 +2,7 @@ import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Global } from "../global" import { Global } from "../global"
export namespace Log { export namespace Log {
const Default = create() export const Default = create()
export interface Options { export interface Options {
print: boolean print: boolean