2025-06-11 12:21:46 +08:00
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
2025-06-17 03:02:25 +08:00
import { map , pipe , sortBy , values } from "remeda"
2025-06-25 10:24:55 +08:00
import path from "path"
import os from "os"
2025-12-02 07:01:10 +08:00
import { Config } from "../../config/config"
2025-06-25 10:24:55 +08:00
import { Global } from "../../global"
2025-08-15 04:24:46 +08:00
import { Plugin } from "../../plugin"
2025-09-02 05:15:49 +08:00
import { Instance } from "../../project/instance"
2025-12-11 06:23:12 +08:00
import type { Hooks } from "@opencode-ai/plugin"
type PluginAuth = NonNullable < Hooks [ " auth " ] >
/ * *
* Handle plugin - based authentication flow .
* Returns true if auth was handled , false if it should fall through to default handling .
* /
async function handlePluginAuth ( plugin : { auth : PluginAuth } , provider : string ) : Promise < boolean > {
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 ]
// Handle prompts for all auth types
2026-01-03 01:12:02 +08:00
await Bun . sleep ( 10 )
2025-12-11 06:23:12 +08:00
const inputs : Record < string , string > = { }
if ( method . prompts ) {
for ( const prompt of method . prompts ) {
if ( prompt . condition && ! prompt . condition ( inputs ) ) {
continue
}
if ( prompt . type === "select" ) {
const value = await prompts . select ( {
message : prompt.message ,
options : prompt.options ,
} )
if ( prompts . isCancel ( value ) ) throw new UI . CancelledError ( )
inputs [ prompt . key ] = value
} else {
const value = await prompts . text ( {
message : prompt.message ,
placeholder : prompt.placeholder ,
validate : prompt.validate ? ( v ) = > prompt . validate ! ( v ? ? "" ) : undefined ,
} )
if ( prompts . isCancel ( value ) ) throw new UI . CancelledError ( )
inputs [ prompt . key ] = value
}
}
}
if ( method . type === "oauth" ) {
const authorize = await method . authorize ( inputs )
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" ) {
const saveProvider = result . provider ? ? provider
if ( "refresh" in result ) {
const { type : _ , provider : __ , refresh , access , expires , . . . extraFields } = result
await Auth . set ( saveProvider , {
type : "oauth" ,
refresh ,
access ,
expires ,
. . . extraFields ,
} )
}
if ( "key" in result ) {
await Auth . set ( saveProvider , {
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" ) {
const saveProvider = result . provider ? ? provider
if ( "refresh" in result ) {
const { type : _ , provider : __ , refresh , access , expires , . . . extraFields } = result
await Auth . set ( saveProvider , {
type : "oauth" ,
refresh ,
access ,
expires ,
. . . extraFields ,
} )
}
if ( "key" in result ) {
await Auth . set ( saveProvider , {
type : "api" ,
key : result.key ,
} )
}
prompts . log . success ( "Login successful" )
}
}
prompts . outro ( "Done" )
return true
}
if ( method . type === "api" ) {
if ( method . authorize ) {
const result = await method . authorize ( inputs )
if ( result . type === "failed" ) {
prompts . log . error ( "Failed to authorize" )
}
if ( result . type === "success" ) {
const saveProvider = result . provider ? ? provider
await Auth . set ( saveProvider , {
type : "api" ,
key : result.key ,
} )
prompts . log . success ( "Login successful" )
}
prompts . outro ( "Done" )
return true
}
}
return false
}
2025-06-11 12:21:46 +08:00
2026-02-18 05:21:49 +08:00
/ * *
* Build a deduplicated list of plugin - registered auth providers that are not
* already present in models . dev , respecting enabled / disabled provider lists .
* Pure function with no side effects ; safe to test without mocking .
* /
export function resolvePluginProviders ( input : {
hooks : Hooks [ ]
existingProviders : Record < string , unknown >
disabled : Set < string >
enabled? : Set < string >
providerNames : Record < string , string | undefined >
} ) : Array < { id : string ; name : string } > {
const seen = new Set < string > ( )
const result : Array < { id : string ; name : string } > = [ ]
for ( const hook of input . hooks ) {
if ( ! hook . auth ) continue
const id = hook . auth . provider
if ( seen . has ( id ) ) continue
seen . add ( id )
if ( Object . hasOwn ( input . existingProviders , id ) ) continue
if ( input . disabled . has ( id ) ) continue
if ( input . enabled && ! input . enabled . has ( id ) ) continue
result . push ( {
id ,
name : input.providerNames [ id ] ? ? id ,
} )
}
return result
}
2025-06-11 12:21:46 +08:00
export const AuthCommand = cmd ( {
command : "auth" ,
2025-06-24 01:00:21 +08:00
describe : "manage credentials" ,
2025-06-11 12:21:46 +08:00
builder : ( yargs ) = >
2025-11-08 09:59:02 +08:00
yargs . command ( AuthLoginCommand ) . command ( AuthLogoutCommand ) . command ( AuthListCommand ) . demandCommand ( ) ,
2025-07-30 07:30:24 +08:00
async handler() { } ,
2025-06-11 12:21:46 +08:00
} )
export const AuthListCommand = cmd ( {
command : "list" ,
aliases : [ "ls" ] ,
describe : "list providers" ,
async handler() {
UI . empty ( )
2025-06-25 10:24:55 +08:00
const authPath = path . join ( Global . Path . data , "auth.json" )
const homedir = os . homedir ( )
2025-07-08 03:53:43 +08:00
const displayPath = authPath . startsWith ( homedir ) ? authPath . replace ( homedir , "~" ) : authPath
2025-06-25 10:24:55 +08:00
prompts . intro ( ` Credentials ${ UI . Style . TEXT_DIM } ${ displayPath } ` )
2025-12-08 12:22:21 +08:00
const results = Object . entries ( await Auth . all ( ) )
2025-06-11 12:21:46 +08:00
const database = await ModelsDev . get ( )
for ( const [ providerID , result ] of results ) {
const name = database [ providerID ] ? . name || providerID
2025-06-25 10:24:55 +08:00
prompts . log . info ( ` ${ name } ${ UI . Style . TEXT_DIM } ${ result . type } ` )
2025-06-11 12:21:46 +08:00
}
prompts . outro ( ` ${ results . length } credentials ` )
2025-06-25 10:24:55 +08:00
// Environment variables section
2025-06-27 10:30:44 +08:00
const activeEnvVars : Array < { provider : string ; envVar : string } > = [ ]
2025-06-25 10:24:55 +08:00
for ( const [ providerID , provider ] of Object . entries ( database ) ) {
for ( const envVar of provider . env ) {
if ( process . env [ envVar ] ) {
2025-06-27 10:30:44 +08:00
activeEnvVars . push ( {
provider : provider.name || providerID ,
envVar ,
2025-06-25 10:24:55 +08:00
} )
}
}
}
if ( activeEnvVars . length > 0 ) {
UI . empty ( )
prompts . intro ( "Environment" )
2025-06-27 10:30:44 +08:00
2025-06-25 10:24:55 +08:00
for ( const { provider , envVar } of activeEnvVars ) {
prompts . log . info ( ` ${ provider } ${ UI . Style . TEXT_DIM } ${ envVar } ` )
}
2025-06-27 10:30:44 +08:00
2025-11-08 09:59:02 +08:00
prompts . outro ( ` ${ activeEnvVars . length } environment variable ` + ( activeEnvVars . length === 1 ? "" : "s" ) )
2025-06-25 10:24:55 +08:00
}
2025-06-11 12:21:46 +08:00
} ,
} )
export const AuthLoginCommand = cmd ( {
2025-07-30 07:30:24 +08:00
command : "login [url]" ,
2025-06-24 01:00:21 +08:00
describe : "log in to a provider" ,
2025-07-30 07:30:24 +08:00
builder : ( yargs ) = >
yargs . positional ( "url" , {
describe : "opencode auth provider" ,
type : "string" ,
} ) ,
async handler ( args ) {
2025-09-19 17:11:29 +08:00
await Instance . provide ( {
directory : process.cwd ( ) ,
async fn() {
UI . empty ( )
prompts . intro ( "Add credential" )
if ( args . url ) {
2025-11-08 09:59:02 +08:00
const wellknown = await fetch ( ` ${ args . url } /.well-known/opencode ` ) . then ( ( x ) = > x . json ( ) as any )
2025-09-19 17:11:29 +08:00
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 )
2025-08-15 04:24:46 +08:00
prompts . outro ( "Done" )
return
}
2025-11-07 02:03:02 +08:00
await ModelsDev . refresh ( ) . catch ( ( ) = > { } )
2025-12-02 07:01:10 +08:00
const config = await Config . get ( )
const disabled = new Set ( config . disabled_providers ? ? [ ] )
const enabled = config . enabled_providers ? new Set ( config . enabled_providers ) : undefined
const providers = await ModelsDev . get ( ) . then ( ( x ) = > {
const filtered : Record < string , ( typeof x ) [ string ] > = { }
for ( const [ key , value ] of Object . entries ( x ) ) {
if ( ( enabled ? enabled . has ( key ) : true ) && ! disabled . has ( key ) ) {
filtered [ key ] = value
}
}
return filtered
} )
2025-11-07 02:03:02 +08:00
const priority : Record < string , number > = {
opencode : 0 ,
anthropic : 1 ,
"github-copilot" : 2 ,
openai : 3 ,
google : 4 ,
openrouter : 5 ,
vercel : 6 ,
}
2026-02-18 05:21:49 +08:00
const pluginProviders = resolvePluginProviders ( {
hooks : await Plugin . list ( ) ,
existingProviders : providers ,
disabled ,
enabled ,
2026-02-18 05:23:23 +08:00
providerNames : Object.fromEntries ( Object . entries ( config . provider ? ? { } ) . map ( ( [ id , p ] ) = > [ id , p . name ] ) ) ,
2026-02-18 05:21:49 +08:00
} )
2025-11-07 02:03:02 +08:00
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 ,
2025-12-09 05:28:32 +08:00
hint : {
opencode : "recommended" ,
anthropic : "Claude Max or API key" ,
2026-01-10 08:45:03 +08:00
openai : "ChatGPT Plus/Pro or API key" ,
2025-12-09 05:28:32 +08:00
} [ x . id ] ,
2025-11-07 02:03:02 +08:00
} ) ) ,
2025-08-15 04:24:46 +08:00
) ,
2026-02-18 05:21:49 +08:00
. . . pluginProviders . map ( ( x ) = > ( {
label : x.name ,
value : x.id ,
hint : "plugin" ,
} ) ) ,
2025-11-07 02:03:02 +08:00
{
value : "other" ,
label : "Other" ,
} ,
] ,
} )
2025-07-15 05:55:06 +08:00
2025-11-07 02:03:02 +08:00
if ( prompts . isCancel ( provider ) ) throw new UI . CancelledError ( )
2025-07-15 05:55:06 +08:00
2026-02-02 00:50:41 +08:00
const plugin = await Plugin . list ( ) . then ( ( x ) = > x . findLast ( ( x ) = > x . auth ? . provider === provider ) )
2025-11-07 02:03:02 +08:00
if ( plugin && plugin . auth ) {
2025-12-11 06:23:12 +08:00
const handled = await handlePluginAuth ( { auth : plugin.auth } , provider )
if ( handled ) return
2025-11-07 02:03:02 +08:00
}
2025-06-23 07:11:37 +08:00
2025-11-07 02:03:02 +08:00
if ( provider === "other" ) {
provider = await prompts . text ( {
message : "Enter provider id" ,
2025-11-08 09:59:02 +08:00
validate : ( x ) = > ( x && x . match ( /^[0-9a-z-]+$/ ) ? undefined : "a-z, 0-9 and hyphens only" ) ,
2025-11-07 02:03:02 +08:00
} )
if ( prompts . isCancel ( provider ) ) throw new UI . CancelledError ( )
provider = provider . replace ( /^@ai-sdk\// , "" )
if ( prompts . isCancel ( provider ) ) throw new UI . CancelledError ( )
2025-12-11 06:23:12 +08:00
// Check if a plugin provides auth for this custom provider
2026-02-02 00:50:41 +08:00
const customPlugin = await Plugin . list ( ) . then ( ( x ) = > x . findLast ( ( x ) = > x . auth ? . provider === provider ) )
2025-12-11 06:23:12 +08:00
if ( customPlugin && customPlugin . auth ) {
const handled = await handlePluginAuth ( { auth : customPlugin.auth } , provider )
if ( handled ) return
}
2025-11-07 02:03:02 +08:00
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 (
2026-01-06 02:51:43 +08:00
"Amazon Bedrock authentication priority:\n" +
" 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
2026-01-15 00:20:47 +08:00
" 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" +
2026-01-06 02:51:43 +08:00
"Configure via opencode.json options (profile, region, endpoint) or\n" +
2026-01-15 00:20:47 +08:00
"AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE)." ,
2025-11-07 02:03:02 +08:00
)
2025-10-04 13:10:38 +08:00
}
2025-11-07 02:03:02 +08:00
if ( provider === "opencode" ) {
prompts . log . info ( "Create an api key at https://opencode.ai/auth" )
}
if ( provider === "vercel" ) {
prompts . log . info ( "You can create an api key at https://vercel.link/ai-gateway-token" )
2025-09-19 17:11:29 +08:00
}
2025-06-23 07:11:37 +08:00
2025-12-30 23:45:09 +08:00
if ( [ "cloudflare" , "cloudflare-ai-gateway" ] . includes ( provider ) ) {
prompts . log . info (
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway" ,
)
}
2025-11-07 02:03:02 +08:00
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 ,
2025-09-19 17:11:29 +08:00
} )
2025-07-19 22:08:24 +08:00
2025-09-19 17:11:29 +08:00
prompts . outro ( "Done" )
} ,
2025-06-11 12:21:46 +08:00
} )
} ,
} )
export const AuthLogoutCommand = cmd ( {
command : "logout" ,
2025-06-24 01:00:21 +08:00
describe : "log out from a configured provider" ,
2025-06-11 12:21:46 +08:00
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 ( {
2025-06-11 12:27:46 +08:00
message : "Select provider" ,
2025-06-11 12:21:46 +08:00
options : credentials.map ( ( [ key , value ] ) = > ( {
2025-07-08 03:53:43 +08:00
label : ( database [ key ] ? . name || key ) + UI . Style . TEXT_DIM + " (" + value . type + ")" ,
2025-06-11 12:21:46 +08:00
value : key ,
} ) ) ,
} )
if ( prompts . isCancel ( providerID ) ) throw new UI . CancelledError ( )
await Auth . remove ( providerID )
prompts . outro ( "Logout successful" )
} ,
} )