mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-28 21:24:19 +01:00
290 lines
9.2 KiB
TypeScript
290 lines
9.2 KiB
TypeScript
import { Auth } from "../../auth"
|
|
import { cmd } from "./cmd"
|
|
import * as prompts from "@clack/prompts"
|
|
import { UI } from "../ui"
|
|
import { ModelsDev } from "../../provider/models"
|
|
import { map, pipe, sortBy, values } from "remeda"
|
|
import path from "path"
|
|
import os from "os"
|
|
import { Global } from "../../global"
|
|
import { Plugin } from "../../plugin"
|
|
import { App } from "../../app/app"
|
|
|
|
export const AuthCommand = cmd({
|
|
command: "auth",
|
|
describe: "manage credentials",
|
|
builder: (yargs) =>
|
|
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
|
|
async handler() {},
|
|
})
|
|
|
|
export const AuthListCommand = cmd({
|
|
command: "list",
|
|
aliases: ["ls"],
|
|
describe: "list providers",
|
|
async handler() {
|
|
UI.empty()
|
|
const authPath = path.join(Global.Path.data, "auth.json")
|
|
const homedir = os.homedir()
|
|
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
|
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
|
const results = await Auth.all().then((x) => Object.entries(x))
|
|
const database = await ModelsDev.get()
|
|
|
|
for (const [providerID, result] of results) {
|
|
const name = database[providerID]?.name || providerID
|
|
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
|
}
|
|
|
|
prompts.outro(`${results.length} credentials`)
|
|
|
|
// Environment variables section
|
|
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
|
|
|
for (const [providerID, provider] of Object.entries(database)) {
|
|
for (const envVar of provider.env) {
|
|
if (process.env[envVar]) {
|
|
activeEnvVars.push({
|
|
provider: provider.name || providerID,
|
|
envVar,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeEnvVars.length > 0) {
|
|
UI.empty()
|
|
prompts.intro("Environment")
|
|
|
|
for (const { provider, envVar } of activeEnvVars) {
|
|
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
|
}
|
|
|
|
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
|
}
|
|
},
|
|
})
|
|
|
|
export const AuthLoginCommand = cmd({
|
|
command: "login [url]",
|
|
describe: "log in to a provider",
|
|
builder: (yargs) =>
|
|
yargs.positional("url", {
|
|
describe: "opencode auth provider",
|
|
type: "string",
|
|
}),
|
|
async handler(args) {
|
|
await App.provide({ cwd: process.cwd() }, async () => {
|
|
UI.empty()
|
|
prompts.intro("Add credential")
|
|
if (args.url) {
|
|
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
|
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
|
const proc = Bun.spawn({
|
|
cmd: wellknown.auth.command,
|
|
stdout: "pipe",
|
|
})
|
|
const exit = await proc.exited
|
|
if (exit !== 0) {
|
|
prompts.log.error("Failed")
|
|
prompts.outro("Done")
|
|
return
|
|
}
|
|
const token = await new Response(proc.stdout).text()
|
|
await Auth.set(args.url, {
|
|
type: "wellknown",
|
|
key: wellknown.auth.env,
|
|
token: token.trim(),
|
|
})
|
|
prompts.log.success("Logged into " + args.url)
|
|
prompts.outro("Done")
|
|
return
|
|
}
|
|
await ModelsDev.refresh().catch(() => {})
|
|
const providers = await ModelsDev.get()
|
|
const priority: Record<string, number> = {
|
|
anthropic: 0,
|
|
"github-copilot": 1,
|
|
openai: 2,
|
|
google: 3,
|
|
openrouter: 4,
|
|
vercel: 5,
|
|
}
|
|
let provider = await prompts.autocomplete({
|
|
message: "Select provider",
|
|
maxItems: 8,
|
|
options: [
|
|
...pipe(
|
|
providers,
|
|
values(),
|
|
sortBy(
|
|
(x) => priority[x.id] ?? 99,
|
|
(x) => x.name ?? x.id,
|
|
),
|
|
map((x) => ({
|
|
label: x.name,
|
|
value: x.id,
|
|
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
|
})),
|
|
),
|
|
{
|
|
value: "other",
|
|
label: "Other",
|
|
},
|
|
],
|
|
})
|
|
|
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
|
|
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
|
|
if (plugin && plugin.auth) {
|
|
let index = 0
|
|
if (plugin.auth.methods.length > 1) {
|
|
const method = await prompts.select({
|
|
message: "Login method",
|
|
options: [
|
|
...plugin.auth.methods.map((x, index) => ({
|
|
label: x.label,
|
|
value: index.toString(),
|
|
})),
|
|
],
|
|
})
|
|
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
|
index = parseInt(method)
|
|
}
|
|
const method = plugin.auth.methods[index]
|
|
if (method.type === "oauth") {
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
const authorize = await method.authorize()
|
|
|
|
if (authorize.url) {
|
|
prompts.log.info("Go to: " + authorize.url)
|
|
}
|
|
|
|
if (authorize.method === "auto") {
|
|
if (authorize.instructions) {
|
|
prompts.log.info(authorize.instructions)
|
|
}
|
|
const spinner = prompts.spinner()
|
|
spinner.start("Waiting for authorization...")
|
|
const result = await authorize.callback()
|
|
if (result.type === "failed") {
|
|
spinner.stop("Failed to authorize", 1)
|
|
}
|
|
if (result.type === "success") {
|
|
if ("refresh" in result) {
|
|
await Auth.set(provider, {
|
|
type: "oauth",
|
|
refresh: result.refresh,
|
|
access: result.access,
|
|
expires: result.expires,
|
|
})
|
|
}
|
|
if ("key" in result) {
|
|
await Auth.set(provider, {
|
|
type: "api",
|
|
key: result.key,
|
|
})
|
|
}
|
|
spinner.stop("Login successful")
|
|
}
|
|
}
|
|
|
|
if (authorize.method === "code") {
|
|
const code = await prompts.text({
|
|
message: "Paste the authorization code here: ",
|
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
})
|
|
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
|
const result = await authorize.callback(code)
|
|
if (result.type === "failed") {
|
|
prompts.log.error("Failed to authorize")
|
|
}
|
|
if (result.type === "success") {
|
|
if ("refresh" in result) {
|
|
await Auth.set(provider, {
|
|
type: "oauth",
|
|
refresh: result.refresh,
|
|
access: result.access,
|
|
expires: result.expires,
|
|
})
|
|
}
|
|
if ("key" in result) {
|
|
await Auth.set(provider, {
|
|
type: "api",
|
|
key: result.key,
|
|
})
|
|
}
|
|
prompts.log.success("Login successful")
|
|
}
|
|
}
|
|
prompts.outro("Done")
|
|
return
|
|
}
|
|
}
|
|
|
|
if (provider === "other") {
|
|
provider = await prompts.text({
|
|
message: "Enter provider id",
|
|
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
|
})
|
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
provider = provider.replace(/^@ai-sdk\//, "")
|
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
|
prompts.log.warn(
|
|
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
|
)
|
|
}
|
|
|
|
if (provider === "amazon-bedrock") {
|
|
prompts.log.info(
|
|
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
|
)
|
|
prompts.outro("Done")
|
|
return
|
|
}
|
|
|
|
if (provider === "vercel") {
|
|
prompts.log.info("You can create an api key in the dashboard")
|
|
}
|
|
|
|
const key = await prompts.password({
|
|
message: "Enter your API key",
|
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
})
|
|
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
await Auth.set(provider, {
|
|
type: "api",
|
|
key,
|
|
})
|
|
|
|
prompts.outro("Done")
|
|
})
|
|
},
|
|
})
|
|
|
|
export const AuthLogoutCommand = cmd({
|
|
command: "logout",
|
|
describe: "log out from a configured provider",
|
|
async handler() {
|
|
UI.empty()
|
|
const credentials = await Auth.all().then((x) => Object.entries(x))
|
|
prompts.intro("Remove credential")
|
|
if (credentials.length === 0) {
|
|
prompts.log.error("No credentials found")
|
|
return
|
|
}
|
|
const database = await ModelsDev.get()
|
|
const providerID = await prompts.select({
|
|
message: "Select provider",
|
|
options: credentials.map(([key, value]) => ({
|
|
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
|
value: key,
|
|
})),
|
|
})
|
|
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
|
|
await Auth.remove(providerID)
|
|
prompts.outro("Logout successful")
|
|
},
|
|
})
|