Configure: add Keep Existing auth method option

This commit is contained in:
Benjamin Jesuiter
2026-02-17 10:30:17 +01:00
parent 19f8b6bf4f
commit eca9fd94f5
2 changed files with 161 additions and 1 deletions

View File

@@ -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");
});
});

View File

@@ -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<string>();
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) {