Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions packages/opencode/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@ export const AuthCommand = cmd({
async handler() {},
})

export type AuthListEntry = {
providerID: string
type: "api" | "oauth" | "wellknown" | "config"
}

export async function collectAuthListEntries(): Promise<AuthListEntry[]> {
const entries: AuthListEntry[] = []
const authEntries = Object.entries(await Auth.all())
for (const [providerID, result] of authEntries) {
entries.push({ providerID, type: result.type })
}

const seen = new Set(entries.map((entry) => entry.providerID))
const config = await Config.get()

for (const [providerID, provider] of Object.entries(config.provider ?? {})) {
const apiKey = provider.options?.apiKey
if (typeof apiKey !== "string" || apiKey.trim() === "") continue
if (seen.has(providerID)) continue
entries.push({ providerID, type: "config" })
}

return entries
}

export const AuthListCommand = cmd({
command: "list",
aliases: ["ls"],
Expand All @@ -177,12 +202,12 @@ export const AuthListCommand = cmd({
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = Object.entries(await Auth.all())
const results = await collectAuthListEntries()
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}`)
for (const entry of results) {
const name = database[entry.providerID]?.name || entry.providerID
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${entry.type}`)
}

prompts.outro(`${results.length} credentials`)
Expand Down
94 changes: 94 additions & 0 deletions packages/opencode/test/cli/auth-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, test } from "bun:test"
import { Auth } from "../../src/auth"
import { collectAuthListEntries } from "../../src/cli/cmd/auth"
import { Config } from "../../src/config/config"

async function withMocks<T>(
mocks: {
authAll: () => Promise<Record<string, Auth.Info>>
configGet: () => Promise<Config.Info>
},
fn: () => Promise<T>,
): Promise<T> {
const originalAuthAll = Auth.all
const originalConfigGet = Config.get
Auth.all = mocks.authAll
Config.get = mocks.configGet
try {
return await fn()
} finally {
Auth.all = originalAuthAll
Config.get = originalConfigGet
}
}

describe("collectAuthListEntries", () => {
test("includes config apiKey when no auth entry exists", async () => {
const entries = await withMocks(
{
authAll: () => Promise.resolve({}),
configGet: () =>
Promise.resolve({
provider: {
avalai: {
options: {
apiKey: "test-key",
},
},
},
} as Config.Info),
},
() => collectAuthListEntries(),
)

expect(entries).toEqual([{ providerID: "avalai", type: "config" }])
})

test("prefers auth entries over config apiKey", async () => {
const entries = await withMocks(
{
authAll: () =>
Promise.resolve({
openai: {
type: "api",
key: "from-auth",
},
} as Record<string, Auth.Info>),
configGet: () =>
Promise.resolve({
provider: {
openai: {
options: {
apiKey: "from-config",
},
},
},
} as Config.Info),
},
() => collectAuthListEntries(),
)

expect(entries).toEqual([{ providerID: "openai", type: "api" }])
})

test("ignores blank config apiKey values", async () => {
const entries = await withMocks(
{
authAll: () => Promise.resolve({}),
configGet: () =>
Promise.resolve({
provider: {
openrouter: {
options: {
apiKey: " ",
},
},
},
} as Config.Info),
},
() => collectAuthListEntries(),
)

expect(entries).toEqual([])
})
})