summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-10 15:43:14 -0400
committerDax Raad <[email protected]>2025-06-10 15:43:14 -0400
commit5ab2ff9589aadc36c778b919940475f0a966f8d2 (patch)
treef04a797e6242d963bbd3493de6beb4be369fb389
parenta0062d46610719265b3b88dd34f87debebf639ce (diff)
downloadopencode-5ab2ff9589aadc36c778b919940475f0a966f8d2.tar.gz
opencode-5ab2ff9589aadc36c778b919940475f0a966f8d2.zip
onboarding progress
-rw-r--r--packages/opencode/src/cli/cmd/provider.ts161
-rw-r--r--packages/opencode/src/cli/cmd/run.ts10
-rw-r--r--packages/opencode/src/cli/router.ts193
-rw-r--r--packages/opencode/src/cli/ui.ts20
-rw-r--r--packages/opencode/src/index.ts13
-rw-r--r--packages/opencode/src/provider/provider.ts110
-rw-r--r--packages/opencode/src/server/server.ts4
7 files changed, 381 insertions, 130 deletions
diff --git a/packages/opencode/src/cli/cmd/provider.ts b/packages/opencode/src/cli/cmd/provider.ts
index ed74e83f6..49b10e85d 100644
--- a/packages/opencode/src/cli/cmd/provider.ts
+++ b/packages/opencode/src/cli/cmd/provider.ts
@@ -1,15 +1,11 @@
+import { App } from "../../app/app"
import { AuthAnthropic } from "../../auth/anthropic"
import { AuthKeys } from "../../auth/keys"
-import { UI } from "../ui"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import open from "open"
-
-const OPENCODE = [
- `█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀`,
- `█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀`,
- `▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`,
-]
+import { VERSION } from "../version"
+import { Provider } from "../../provider/provider"
export const ProviderCommand = cmd({
command: "provider",
@@ -27,103 +23,96 @@ export const ProviderListCommand = cmd({
aliases: ["ls"],
describe: "list providers",
async handler() {
- prompts.intro("Configured Providers")
- const keys = await AuthKeys.get()
- for (const key of Object.keys(keys)) {
- prompts.log.success(key)
- }
- prompts.outro("3 providers configured")
+ await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
+ prompts.intro("Providers")
+ const providers = await Provider.list().then((x) => Object.values(x))
+ for (const value of providers) {
+ prompts.log.success(value.info.name + " (" + value.source + ")")
+ }
+ prompts.outro(`${providers.length} configured`)
+ })
},
})
-const ProviderAddCommand = cmd({
+export const ProviderAddCommand = cmd({
command: "add",
describe: "add credentials for various providers",
async handler() {
- UI.empty()
- for (const row of OPENCODE) {
- UI.print(" ")
- for (let i = 0; i < row.length; i++) {
- const color =
- i < 18 ? Bun.color("white", "ansi") : Bun.color("gray", "ansi")
- const char = row[i]
- UI.print(color + char)
- }
- UI.println()
- }
- UI.empty()
-
- prompts.intro("Setup")
- const keys = await AuthKeys.get()
- const provider = await prompts.select({
- message: "Configure a provider",
- options: [
- {
- label: "Anthropic",
- value: "anthropic",
- hint: keys["anthropic"] ? "configured" : "",
- },
- {
- label: "OpenAI",
- value: "openai",
- hint: keys["openai"] ? "configured" : "",
- },
- {
- label: "Google",
- value: "google",
- hint: keys["google"] ? "configured" : "",
- },
- ],
- })
- if (prompts.isCancel(provider)) return
-
- if (provider === "anthropic") {
- const method = await prompts.select({
- message: "Login method",
+ await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
+ const providers = await Provider.list()
+ prompts.intro("Add provider")
+ const provider = await prompts.select({
+ message: "Select",
+ maxItems: 2,
options: [
{
- label: "Claude Pro/Max",
- value: "oauth",
+ label: "Anthropic",
+ value: "anthropic",
+ hint: providers["anthropic"] ? "configured" : "",
+ },
+ {
+ label: "OpenAI",
+ value: "openai",
+ hint: providers["openai"] ? "configured" : "",
},
{
- label: "API Key",
- value: "api",
+ label: "Google",
+ value: "google",
+ hint: providers["google"] ? "configured" : "",
},
],
})
- if (prompts.isCancel(method)) return
-
- if (method === "oauth") {
- // some weird bug where program exits without this
- await new Promise((resolve) => setTimeout(resolve, 10))
- const { url, verifier } = await AuthAnthropic.authorize()
- prompts.note("Opening browser...")
- await open(url)
- prompts.log.info(url)
+ if (prompts.isCancel(provider)) return
- const code = await prompts.text({
- message: "Paste the authorization code here: ",
- validate: (x) => (x.length > 0 ? undefined : "Required"),
+ if (provider === "anthropic") {
+ const method = await prompts.select({
+ message: "Login method",
+ options: [
+ {
+ label: "Claude Pro/Max",
+ value: "oauth",
+ },
+ {
+ label: "API Key",
+ value: "api",
+ },
+ ],
})
- if (prompts.isCancel(code)) return
- await AuthAnthropic.exchange(code, verifier)
- .then(() => {
- prompts.log.success("Login successful")
- })
- .catch(() => {
- prompts.log.error("Invalid code")
+ if (prompts.isCancel(method)) return
+
+ if (method === "oauth") {
+ // some weird bug where program exits without this
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ const { url, verifier } = await AuthAnthropic.authorize()
+ prompts.note("Opening browser...")
+ await open(url)
+ prompts.log.info(url)
+
+ const code = await prompts.text({
+ message: "Paste the authorization code here: ",
+ validate: (x) => (x.length > 0 ? undefined : "Required"),
})
- prompts.outro("Done")
- return
+ if (prompts.isCancel(code)) return
+ await AuthAnthropic.exchange(code, verifier)
+ .then(() => {
+ prompts.log.success("Login successful")
+ })
+ .catch(() => {
+ prompts.log.error("Invalid code")
+ })
+ prompts.outro("Done")
+ return
+ }
}
- }
- const key = await prompts.password({
- message: "Enter your API key",
- })
- if (prompts.isCancel(key)) return
- await AuthKeys.set(provider, key)
+ const key = await prompts.password({
+ message: "Enter your API key",
+ validate: (x) => (x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(key)) return
+ await AuthKeys.set(provider, key)
- prompts.outro("Done")
+ prompts.outro("Done")
+ })
},
})
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 837d4b66d..124d500fd 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -41,11 +41,11 @@ export const RunCommand = {
? await Session.get(args.session)
: await Session.create()
- UI.print(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
+ UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
UI.empty()
- UI.print(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
+ UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
UI.empty()
- UI.print(
+ UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://dev.opencode.ai/s?id=" +
session.id.slice(-8),
@@ -53,7 +53,7 @@ export const RunCommand = {
UI.empty()
function printEvent(color: string, type: string, title: string) {
- UI.print(
+ UI.println(
color + `|`,
UI.Style.TEXT_NORMAL +
UI.Style.TEXT_DIM +
@@ -95,7 +95,7 @@ export const RunCommand = {
if (part.type === "text") {
if (part.text.includes("\n")) {
UI.empty()
- UI.print(part.text)
+ UI.println(part.text)
UI.empty()
return
}
diff --git a/packages/opencode/src/cli/router.ts b/packages/opencode/src/cli/router.ts
new file mode 100644
index 000000000..247b82bcb
--- /dev/null
+++ b/packages/opencode/src/cli/router.ts
@@ -0,0 +1,193 @@
+import { createCli, type TrpcCliMeta } from "trpc-cli"
+import { initTRPC } from "@trpc/server"
+import { z } from "zod"
+import { Server } from "../server/server"
+import { AuthAnthropic } from "../auth/anthropic"
+import { UI } from "./ui"
+import { App } from "../app/app"
+import { Bus } from "../bus"
+import { Provider } from "../provider/provider"
+import { Session } from "../session"
+import { Share } from "../share/share"
+import { Message } from "../session/message"
+import { VERSION } from "./version"
+import { LSP } from "../lsp"
+import fs from "fs/promises"
+import path from "path"
+
+const t = initTRPC.meta<TrpcCliMeta>().create()
+
+export const router = t.router({
+ generate: t.procedure
+ .meta({
+ description: "Generate OpenAPI and event specs",
+ })
+ .input(z.object({}))
+ .mutation(async () => {
+ const specs = await Server.openapi()
+ const dir = "gen"
+ await fs.rmdir(dir, { recursive: true }).catch(() => {})
+ await fs.mkdir(dir, { recursive: true })
+ await Bun.write(
+ path.join(dir, "openapi.json"),
+ JSON.stringify(specs, null, 2),
+ )
+ return "Generated OpenAPI specs in gen/ directory"
+ }),
+
+ run: t.procedure
+ .meta({
+ description: "Run OpenCode with a message",
+ })
+ .input(
+ z.object({
+ message: z.array(z.string()).default([]).describe("Message to send"),
+ session: z.string().optional().describe("Session ID to continue"),
+ }),
+ )
+ .mutation(
+ async ({ input }: { input: { message: string[]; session?: string } }) => {
+ const message = input.message.join(" ")
+ await App.provide(
+ {
+ cwd: process.cwd(),
+ version: "0.0.0",
+ },
+ async () => {
+ await Share.init()
+ const session = input.session
+ ? await Session.get(input.session)
+ : await Session.create()
+
+ UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
+ UI.empty()
+ UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
+ UI.empty()
+ UI.println(
+ UI.Style.TEXT_INFO_BOLD +
+ "~ https://dev.opencode.ai/s?id=" +
+ session.id.slice(-8),
+ )
+ UI.empty()
+
+ function printEvent(color: string, type: string, title: string) {
+ UI.println(
+ color + `|`,
+ UI.Style.TEXT_NORMAL +
+ UI.Style.TEXT_DIM +
+ ` ${type.padEnd(7, " ")}`,
+ "",
+ UI.Style.TEXT_NORMAL + title,
+ )
+ }
+
+ Bus.subscribe(Message.Event.PartUpdated, async (message) => {
+ const part = message.properties.part
+ if (
+ part.type === "tool-invocation" &&
+ part.toolInvocation.state === "result"
+ ) {
+ if (part.toolInvocation.toolName === "opencode_todowrite")
+ return
+
+ const args = part.toolInvocation.args as any
+ const tool = part.toolInvocation.toolName
+
+ if (tool === "opencode_edit")
+ printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
+ if (tool === "opencode_bash")
+ printEvent(
+ UI.Style.TEXT_WARNING_BOLD,
+ "Execute",
+ args.command,
+ )
+ if (tool === "opencode_read")
+ printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
+ if (tool === "opencode_write")
+ printEvent(
+ UI.Style.TEXT_SUCCESS_BOLD,
+ "Create",
+ args.filePath,
+ )
+ if (tool === "opencode_list")
+ printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
+ if (tool === "opencode_glob")
+ printEvent(
+ UI.Style.TEXT_INFO_BOLD,
+ "Glob",
+ args.pattern + (args.path ? " in " + args.path : ""),
+ )
+ }
+
+ if (part.type === "text") {
+ if (part.text.includes("\n")) {
+ UI.empty()
+ UI.println(part.text)
+ UI.empty()
+ return
+ }
+ printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
+ }
+ })
+
+ const { providerID, modelID } = await Provider.defaultModel()
+ await Session.chat({
+ sessionID: session.id,
+ providerID,
+ modelID,
+ parts: [
+ {
+ type: "text",
+ text: message,
+ },
+ ],
+ })
+ UI.empty()
+ },
+ )
+ return "Session completed"
+ },
+ ),
+
+ scrap: t.procedure
+ .meta({
+ description: "Test command for scraping files",
+ })
+ .input(
+ z.object({
+ file: z.string().describe("File to process"),
+ }),
+ )
+ .mutation(async ({ input }: { input: { file: string } }) => {
+ await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
+ await LSP.touchFile(input.file, true)
+ await LSP.diagnostics()
+ })
+ return `Processed file: ${input.file}`
+ }),
+
+ login: t.router({
+ anthropic: t.procedure
+ .meta({
+ description: "Login to Anthropic",
+ })
+ .input(z.object({}))
+ .mutation(async () => {
+ const { url, verifier } = await AuthAnthropic.authorize()
+
+ UI.println("Login to Anthropic")
+ UI.println("Open the following URL in your browser:")
+ UI.println(url)
+ UI.println("")
+
+ const code = await UI.input("Paste the authorization code here: ")
+ await AuthAnthropic.exchange(code, verifier)
+ return "Successfully logged in to Anthropic"
+ }),
+ }),
+})
+
+export function createOpenCodeCli() {
+ return createCli({ router })
+}
+
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
index 2579c6fda..e5af7f74f 100644
--- a/packages/opencode/src/cli/ui.ts
+++ b/packages/opencode/src/cli/ui.ts
@@ -1,4 +1,10 @@
export namespace UI {
+ const LOGO = [
+ `█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀`,
+ `█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀`,
+ `▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`,
+ ]
+
export const Style = {
TEXT_HIGHLIGHT: "\x1b[96m",
TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
@@ -33,6 +39,20 @@ export namespace UI {
blank = true
}
+ export function logo() {
+ for (const row of LOGO) {
+ print(" ")
+ for (let i = 0; i < row.length; i++) {
+ const color =
+ i > 18 ? Bun.color("white", "ansi") : Bun.color("gray", "ansi")
+ const char = row[i]
+ print(color + char)
+ }
+ println()
+ }
+ empty()
+ }
+
export async function input(prompt: string): Promise<string> {
const readline = require("readline")
const rl = readline.createInterface({
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index af090d48a..b5530c0cf 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -15,7 +15,9 @@ import { GenerateCommand } from "./cli/cmd/generate"
import { VERSION } from "./cli/version"
import { ScrapCommand } from "./cli/cmd/scrap"
import { Log } from "./util/log"
-import { ProviderCommand } from "./cli/cmd/provider"
+import { ProviderAddCommand, ProviderCommand } from "./cli/cmd/provider"
+import { Provider } from "./provider/provider"
+import { UI } from "./cli/ui"
await Log.init({ print: process.argv.includes("--print-logs") })
@@ -31,6 +33,15 @@ yargs(hideBin(process.argv))
}),
handler: async (args) => {
await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
+ const providers = await Provider.list()
+ if (Object.keys(providers).length === 0) {
+ UI.empty()
+ UI.logo()
+ UI.empty()
+ await ProviderAddCommand.handler(args)
+ return
+ }
+
await Share.init()
const server = Server.listen()
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 5cc72fc7d..3857404b6 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -23,6 +23,7 @@ import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
+import { AuthKeys } from "../auth/keys"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -60,21 +61,32 @@ export namespace Provider {
})
export type Info = z.output<typeof Info>
- type Autodetector = (provider: Info) => Promise<Record<string, any> | false>
+ type Autodetector = (provider: Info) => Promise<
+ | {
+ source: Source
+ options: Record<string, any>
+ }
+ | false
+ >
- function env(...keys: string[]): Autodetector {
- return async () => {
+ function env(...keys: string[]) {
+ const result: Autodetector = async () => {
for (const key of keys) {
- if (process.env[key]) return {}
+ if (process.env[key])
+ return {
+ source: "env",
+ options: {},
+ }
}
return false
}
+
+ return result
}
- const AUTODETECT: Record<
- string,
- (provider: Info) => Promise<Record<string, any> | false>
- > = {
+ type Source = "oauth" | "env" | "config" | "global"
+
+ const AUTODETECT: Record<string, Autodetector> = {
async anthropic(provider) {
const access = await AuthAnthropic.access()
if (access) {
@@ -88,10 +100,13 @@ export namespace Provider {
}
}
return {
- apiKey: "",
- headers: {
- authorization: `Bearer ${access}`,
- "anthropic-beta": "oauth-2025-04-20",
+ source: "oauth",
+ options: {
+ apiKey: "",
+ headers: {
+ authorization: `Bearer ${access}`,
+ "anthropic-beta": "oauth-2025-04-20",
+ },
},
}
}
@@ -107,6 +122,7 @@ export namespace Provider {
const providers: {
[providerID: string]: {
+ source: Source
info: Provider.Info
options: Record<string, any>
}
@@ -116,30 +132,52 @@ export namespace Provider {
log.info("loading")
+ function mergeProvider(
+ id: string,
+ options: Record<string, any>,
+ source: Source,
+ ) {
+ const provider = providers[id]
+ if (!provider) {
+ providers[id] = {
+ source,
+ info: database[id] ?? {
+ id,
+ name: id,
+ models: [],
+ },
+ options,
+ }
+ return
+ }
+ provider.options = {
+ ...provider.options,
+ ...options,
+ }
+ provider.source = source
+ }
+
for (const [providerID, fn] of Object.entries(AUTODETECT)) {
const provider = database[providerID]
if (!provider) continue
- const options = await fn(provider)
- if (!options) continue
- providers[providerID] = {
- info: provider,
- options,
- }
+ const result = await fn(provider)
+ if (!result) continue
+ mergeProvider(providerID, result.options, result.source)
+ }
+
+ const keys = await AuthKeys.get()
+ for (const [providerID, key] of Object.entries(keys)) {
+ mergeProvider(
+ providerID,
+ {
+ apiKey: key,
+ },
+ "global",
+ )
}
for (const [providerID, options] of Object.entries(config.provider ?? {})) {
- const existing = providers[providerID]
- if (existing) {
- existing.options = {
- ...existing.options,
- ...options,
- }
- continue
- }
- providers[providerID] = {
- info: database[providerID],
- options,
- }
+ mergeProvider(providerID, options, "config")
}
for (const providerID of Object.keys(providers)) {
@@ -153,10 +191,8 @@ export namespace Provider {
}
})
- export async function active() {
- return state().then((state) =>
- mapValues(state.providers, (item) => item.info),
- )
+ export async function list() {
+ return state().then((state) => state.providers)
}
async function getSDK(providerID: string) {
@@ -242,12 +278,12 @@ export namespace Provider {
}
export async function defaultModel() {
- const [provider] = await active().then((val) => Object.values(val))
+ const [provider] = await list().then((val) => Object.values(val))
if (!provider) throw new Error("no providers found")
- const [model] = sort(Object.values(provider.models))
+ const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found")
return {
- providerID: provider.id,
+ providerID: provider.info.id,
modelID: model.id,
}
}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 4bb9fb778..1ba5afdef 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -415,7 +415,9 @@ export namespace Server {
},
}),
async (c) => {
- const providers = await Provider.active()
+ const providers = await Provider.list().then((x) =>
+ mapValues(x, (item) => item.info),
+ )
return c.json({
providers: Object.values(providers),
defaults: mapValues(