From eca9fd94f5fbe5dfcde18d92ee5c6f7abdb3a58d Mon Sep 17 00:00:00 2001 From: Benjamin Jesuiter Date: Tue, 17 Feb 2026 10:30:17 +0100 Subject: [PATCH] Configure: add Keep Existing auth method option --- src/commands/auth-choice-prompt.test.ts | 92 +++++++++++++++++++++++++ src/commands/auth-choice-prompt.ts | 70 ++++++++++++++++++- 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/commands/auth-choice-prompt.test.ts diff --git a/src/commands/auth-choice-prompt.test.ts b/src/commands/auth-choice-prompt.test.ts new file mode 100644 index 0000000000..ddd29b1fc0 --- /dev/null +++ b/src/commands/auth-choice-prompt.test.ts @@ -0,0 +1,92 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { WizardPrompter, WizardSelectParams } from "../wizard/prompts.js"; +import { buildAuthChoiceGroups } from "./auth-choice-options.js"; +import { promptAuthChoiceGrouped, resolveExistingAuthLinesForGroup } from "./auth-choice-prompt.js"; +import { createWizardPrompter } from "./test-wizard-helpers.js"; + +const ORIGINAL_OPENAI_API_KEY = process.env.OPENAI_API_KEY; + +function createStore(): AuthProfileStore { + return { + version: 1, + profiles: { + "openai-codex:default": { + type: "oauth", + provider: "openai-codex", + access: "access-token", + refresh: "refresh-token", + }, + }, + }; +} + +function requireOpenAiGroup(store: AuthProfileStore) { + const group = buildAuthChoiceGroups({ + store, + includeSkip: false, + }).groups.find((entry) => entry.value === "openai"); + + if (!group) { + throw new Error("openai auth choice group missing"); + } + + return group; +} + +afterEach(() => { + if (ORIGINAL_OPENAI_API_KEY === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = ORIGINAL_OPENAI_API_KEY; + } + vi.restoreAllMocks(); +}); + +describe("auth choice keep existing", () => { + it("lists existing APIKey and OAuth entries for OpenAI", () => { + process.env.OPENAI_API_KEY = "sk-test"; + const store = createStore(); + + const lines = resolveExistingAuthLinesForGroup({ + group: requireOpenAiGroup(store), + store, + }); + + expect(lines).toContain("APIKey: OPENAI_API_KEY (openai)"); + expect(lines).toContain("OAuth: openai-codex:default (openai-codex)"); + }); + + it("offers Keep Existing in the method selector and returns skip", async () => { + process.env.OPENAI_API_KEY = "sk-test"; + const store = createStore(); + + const select: WizardPrompter["select"] = vi.fn(async (params: WizardSelectParams) => { + if (params.message === "Model/auth provider") { + return "openai"; + } + if (params.message === "OpenAI auth method") { + const keepExisting = params.options.find((option) => option.value === "skip"); + expect(keepExisting?.label).toBe("Keep Existing"); + expect(keepExisting?.hint).toContain("APIKey:"); + expect(keepExisting?.hint).toContain("OAuth:"); + expect(keepExisting?.hint).toContain("\n"); + return "skip"; + } + return params.options[0]?.value ?? "skip"; + }); + + const prompter = createWizardPrompter( + { select: select as unknown as WizardPrompter["select"] }, + { defaultSelect: "" }, + ); + + await expect( + promptAuthChoiceGrouped({ + prompter, + store, + includeSkip: true, + }), + ).resolves.toBe("skip"); + }); +}); diff --git a/src/commands/auth-choice-prompt.ts b/src/commands/auth-choice-prompt.ts index 35012b61a5..8298d2e3c0 100644 --- a/src/commands/auth-choice-prompt.ts +++ b/src/commands/auth-choice-prompt.ts @@ -1,10 +1,72 @@ import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import { listProfilesForProvider } from "../agents/auth-profiles.js"; +import { resolveEnvApiKey } from "../agents/model-auth.js"; import type { WizardPrompter } from "../wizard/prompts.js"; +import type { AuthChoiceGroup, AuthChoiceOption } from "./auth-choice-options.js"; import { buildAuthChoiceGroups } from "./auth-choice-options.js"; +import { resolvePreferredProviderForAuthChoice } from "./auth-choice.preferred-provider.js"; import type { AuthChoice } from "./onboard-types.js"; const BACK_VALUE = "__back"; +function toKindLabel(type: "api_key" | "oauth" | "token" | undefined): "APIKey" | "OAuth" { + if (type === "oauth" || type === "token") { + return "OAuth"; + } + return "APIKey"; +} + +function stripEnvSourcePrefix(source: string): string { + return source.replace(/^shell env: /, "").replace(/^env: /, ""); +} + +function resolveProviderKeysForGroup(group: AuthChoiceGroup): string[] { + const keys = group.options + .map((option) => resolvePreferredProviderForAuthChoice(option.value)) + .filter((provider): provider is string => Boolean(provider)); + return [...new Set(keys)]; +} + +export function resolveExistingAuthLinesForGroup(params: { + group: AuthChoiceGroup; + store: AuthProfileStore; +}): string[] { + const providerKeys = resolveProviderKeysForGroup(params.group); + const showProvider = providerKeys.length > 1; + const lines = new Set(); + + for (const providerKey of providerKeys) { + const profileIds = listProfilesForProvider(params.store, providerKey); + for (const profileId of profileIds) { + const kind = toKindLabel(params.store.profiles[profileId]?.type); + const providerSuffix = showProvider ? ` (${providerKey})` : ""; + lines.add(`${kind}: ${profileId}${providerSuffix}`); + } + + const envKey = resolveEnvApiKey(providerKey); + if (envKey) { + const kind = envKey.source.includes("OAUTH_TOKEN") ? "OAuth" : "APIKey"; + const source = stripEnvSourcePrefix(envKey.source); + const providerSuffix = showProvider ? ` (${providerKey})` : ""; + lines.add(`${kind}: ${source}${providerSuffix}`); + } + } + + return [...lines]; +} + +export function buildKeepExistingOption(params: { + group: AuthChoiceGroup; + store: AuthProfileStore; +}): AuthChoiceOption { + const lines = resolveExistingAuthLinesForGroup(params); + return { + value: "skip", + label: "Keep Existing", + hint: lines.length > 0 ? lines.join("\n") : "Use existing auth", + }; +} + export async function promptAuthChoiceGrouped(params: { prompter: WizardPrompter; store: AuthProfileStore; @@ -46,9 +108,15 @@ export async function promptAuthChoiceGrouped(params: { return group.options[0].value; } + const methodOptions: Array<{ value: string; label: string; hint?: string }> = [ + ...group.options, + ...(params.includeSkip ? [buildKeepExistingOption({ group, store: params.store })] : []), + { value: BACK_VALUE, label: "Back" }, + ]; + const methodSelection = await params.prompter.select({ message: `${group.label} auth method`, - options: [...group.options, { value: BACK_VALUE, label: "Back" }], + options: methodOptions, }); if (methodSelection === BACK_VALUE) {