mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-01 06:42:26 +00:00
onboarding progress
This commit is contained in:
@@ -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: "API Key",
|
||||
value: "api",
|
||||
label: "OpenAI",
|
||||
value: "openai",
|
||||
hint: providers["openai"] ? "configured" : "",
|
||||
},
|
||||
{
|
||||
label: "Google",
|
||||
value: "google",
|
||||
hint: providers["google"] ? "configured" : "",
|
||||
},
|
||||
],
|
||||
})
|
||||
if (prompts.isCancel(method)) return
|
||||
if (prompts.isCancel(provider)) 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"),
|
||||
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")
|
||||
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"),
|
||||
})
|
||||
.catch(() => {
|
||||
prompts.log.error("Invalid code")
|
||||
})
|
||||
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",
|
||||
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")
|
||||
})
|
||||
if (prompts.isCancel(key)) return
|
||||
await AuthKeys.set(provider, key)
|
||||
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
193
packages/opencode/src/cli/router.ts
Normal file
193
packages/opencode/src/cli/router.ts
Normal file
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user