diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 3dd7bcc35dd..afb9d420ea0 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -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 { + 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"], @@ -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`) diff --git a/packages/opencode/test/cli/auth-list.test.ts b/packages/opencode/test/cli/auth-list.test.ts new file mode 100644 index 00000000000..2306907295e --- /dev/null +++ b/packages/opencode/test/cli/auth-list.test.ts @@ -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( + mocks: { + authAll: () => Promise> + configGet: () => Promise + }, + fn: () => Promise, +): Promise { + 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), + 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([]) + }) +})