diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts index 9d792087..7e8afc82 100644 --- a/packages/opencode/src/app/app.ts +++ b/packages/opencode/src/app/app.ts @@ -97,7 +97,7 @@ export namespace App { log.info("registering service", { name: key }) services.set(key, { state: init(app.info), - shutdown: shutdown, + shutdown, }) } return services.get(key)?.state as State @@ -108,14 +108,15 @@ export namespace App { return ctx.use().info } - export async function provide any>( + export async function provide( input: { cwd: string; version: string }, - cb: T, + cb: (app: Info) => Promise, ) { const app = await create(input) return ctx.provide(app, async () => { const result = await cb(app.info) for (const [key, entry] of app.services.entries()) { + if (!entry.shutdown) continue log.info("shutdown", { name: key }) await entry.shutdown?.(await entry.state) } diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts index 49b10e85..23011c9a 100644 --- a/packages/opencode/src/cli/cmd/provider.ts +++ b/packages/opencode/src/cli/cmd/provider.ts @@ -6,6 +6,7 @@ import * as prompts from "@clack/prompts" import open from "open" import { VERSION } from "../version" import { Provider } from "../../provider/provider" +import { UI } from "../ui" export const ProviderCommand = cmd({ 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") { 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") { // some weird bug where program exits without this @@ -92,7 +93,8 @@ export const ProviderAddCommand = cmd({ message: "Paste the authorization code here: ", 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) .then(() => { prompts.log.success("Login successful") @@ -109,7 +111,7 @@ export const ProviderAddCommand = cmd({ message: "Enter your API key", 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) prompts.outro("Done") diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index ade61fa3..fad8214b 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,4 +1,5 @@ -import { VERSION } from "./version" +import { z } from "zod" +import { NamedError } from "../util/error" export namespace UI { const LOGO = [ @@ -7,6 +8,11 @@ export namespace UI { `▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`, ] + export const CancelledError = NamedError.create( + "UICancelledError", + z.object({}), + ) + export const Style = { TEXT_HIGHLIGHT: "\x1b[96m", TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m", diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 279251ab..07ee6633 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -21,62 +21,76 @@ import { UI } from "./cli/ui" await Log.init({ print: process.argv.includes("--print-logs") }) -yargs(hideBin(process.argv)) - .scriptName("opencode") - .version(VERSION) - .command({ - command: "$0", - describe: "Start OpenCode TUI", - builder: (yargs) => - yargs.option("print-logs", { - type: "boolean", - }), - handler: async (args) => { - UI.logo() - await App.provide({ cwd: process.cwd(), version: VERSION }, async () => { - const providers = await Provider.list() - if (Object.keys(providers).length === 0) { - await ProviderAddCommand.handler(args) - return - } +try { + await yargs(hideBin(process.argv)) + .scriptName("opencode") + .version(VERSION) + .command({ + command: "$0", + describe: "Start OpenCode TUI", + handler: async (args) => { + while (true) { + const result = await App.provide( + { cwd: process.cwd(), version: VERSION }, + async () => { + const providers = await Provider.list() + if (Object.keys(providers).length === 0) { + return "needs_provider" + } - await Share.init() - const server = Server.listen() + await Share.init() + const server = Server.listen() - let cmd = ["go", "run", "./main.go"] - let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname - if (Bun.embeddedFiles.length > 0) { - const blob = Bun.embeddedFiles[0] as File - const binary = path.join(Global.Path.cache, "tui", blob.name) - const file = Bun.file(binary) - if (!(await file.exists())) { - await Bun.write(file, blob, { mode: 0o755 }) - await fs.chmod(binary, 0o755) + let cmd = ["go", "run", "./main.go"] + let cwd = new URL("../../tui/cmd/opencode", import.meta.url) + .pathname + if (Bun.embeddedFiles.length > 0) { + const blob = Bun.embeddedFiles[0] as File + const binary = path.join(Global.Path.cache, "tui", blob.name) + const file = Bun.file(binary) + if (!(await file.exists())) { + 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, - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - env: { - ...process.env, - OPENCODE_SERVER: server.url.toString(), - }, - onExit: () => { - server.stop() - }, - }) - await proc.exited - await server.stop() - }) - }, - }) - .command(RunCommand) - .command(GenerateCommand) - .command(ScrapCommand) - .command(ProviderCommand) - .parse() + }, + }) + .command(RunCommand) + .command(GenerateCommand) + .command(ScrapCommand) + .command(ProviderCommand) + .fail((msg, err) => { + Log.Default.error(msg) + }) + .parse() +} catch (e) { + Log.Default.error(e) +} diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index d7195433..6ad951fe 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -169,7 +169,7 @@ export namespace LSPClient { log.info("shutting down") connection.end() connection.dispose() - server.process.kill() + server.process.kill("SIGKILL") }, } diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index a549a076..8b53d76b 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -32,7 +32,7 @@ export namespace LSPServer { ".cts", ], async spawn(app) { - const tsserver = Bun.resolve( + const tsserver = await Bun.resolve( "typescript/lib/tsserver.js", app.path.cwd, ).catch(() => {}) diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 04323240..07ecd4b6 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -2,7 +2,7 @@ import path from "path" import fs from "fs/promises" import { Global } from "../global" export namespace Log { - const Default = create() + export const Default = create() export interface Options { print: boolean