Compare commits
5 Commits
feat/teleg
...
fix/config
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b765a60fe | ||
|
|
29327ac473 | ||
|
|
874ec049cb | ||
|
|
1c00fbca36 | ||
|
|
eca9fd94f5 |
29
src/commands/auth-choice-options.test.ts
Normal file
29
src/commands/auth-choice-options.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { buildAuthChoiceGroups } from "./auth-choice-options.js";
|
||||
|
||||
const EMPTY_STORE: AuthProfileStore = { version: 1, profiles: {} };
|
||||
|
||||
describe("buildAuthChoiceGroups", () => {
|
||||
it("labels MiniMax non-OAuth options as API-Key", () => {
|
||||
const { groups } = buildAuthChoiceGroups({
|
||||
store: EMPTY_STORE,
|
||||
includeSkip: false,
|
||||
});
|
||||
const minimaxGroup = groups.find((group) => group.value === "minimax");
|
||||
|
||||
expect(minimaxGroup).toBeDefined();
|
||||
expect(minimaxGroup?.options.find((opt) => opt.value === "minimax-portal")?.label).toContain(
|
||||
"OAuth",
|
||||
);
|
||||
expect(minimaxGroup?.options.find((opt) => opt.value === "minimax-api")?.label).toContain(
|
||||
"API-Key",
|
||||
);
|
||||
expect(
|
||||
minimaxGroup?.options.find((opt) => opt.value === "minimax-api-key-cn")?.label,
|
||||
).toContain("API-Key");
|
||||
expect(
|
||||
minimaxGroup?.options.find((opt) => opt.value === "minimax-api-lightning")?.label,
|
||||
).toContain("API-Key");
|
||||
});
|
||||
});
|
||||
@@ -285,15 +285,15 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
|
||||
label: "OpenCode Zen (multi-model proxy)",
|
||||
hint: "Claude, GPT, Gemini via opencode.ai/zen",
|
||||
},
|
||||
{ value: "minimax-api", label: "MiniMax M2.5" },
|
||||
{ value: "minimax-api", label: "MiniMax M2.5 (API-Key)" },
|
||||
{
|
||||
value: "minimax-api-key-cn",
|
||||
label: "MiniMax M2.5 (CN)",
|
||||
label: "MiniMax M2.5 (CN API-Key)",
|
||||
hint: "China endpoint (api.minimaxi.com)",
|
||||
},
|
||||
{
|
||||
value: "minimax-api-lightning",
|
||||
label: "MiniMax M2.5 Lightning",
|
||||
label: "MiniMax M2.5 Lightning (API-Key)",
|
||||
hint: "Faster, higher output cost",
|
||||
},
|
||||
{ value: "custom-api-key", label: "Custom Provider" },
|
||||
|
||||
122
src/commands/auth-choice-prompt.test.ts
Normal file
122
src/commands/auth-choice-prompt.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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(params.options[0]?.value).toBe("skip");
|
||||
expect(keepExisting?.label).toContain("Keep existing");
|
||||
expect(keepExisting?.label).toContain("\n APIKey:");
|
||||
expect(keepExisting?.label).toContain("\n OAuth:");
|
||||
expect(keepExisting?.hint).toBeUndefined();
|
||||
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");
|
||||
});
|
||||
|
||||
it("does not show Keep existing when provider has no existing auth", async () => {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
|
||||
const select: WizardPrompter["select"] = vi.fn(async (params: WizardSelectParams) => {
|
||||
if (params.message === "Model/auth provider") {
|
||||
return "openai";
|
||||
}
|
||||
if (params.message === "OpenAI auth method") {
|
||||
expect(params.options.some((option) => option.value === "skip")).toBe(false);
|
||||
return "openai-api-key";
|
||||
}
|
||||
return params.options[0]?.value ?? "openai-api-key";
|
||||
});
|
||||
|
||||
const prompter = createWizardPrompter(
|
||||
{ select: select as unknown as WizardPrompter["select"] },
|
||||
{ defaultSelect: "" },
|
||||
);
|
||||
|
||||
await expect(
|
||||
promptAuthChoiceGrouped({
|
||||
prompter,
|
||||
store,
|
||||
includeSkip: true,
|
||||
}),
|
||||
).resolves.toBe("openai-api-key");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,74 @@
|
||||
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 | undefined {
|
||||
const lines = resolveExistingAuthLinesForGroup(params);
|
||||
if (lines.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
value: "skip",
|
||||
label: ["Keep existing", ...lines.map((line) => ` ${line}`)].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function promptAuthChoiceGrouped(params: {
|
||||
prompter: WizardPrompter;
|
||||
store: AuthProfileStore;
|
||||
@@ -46,9 +110,18 @@ export async function promptAuthChoiceGrouped(params: {
|
||||
return group.options[0].value;
|
||||
}
|
||||
|
||||
const keepExistingOption = params.includeSkip
|
||||
? buildKeepExistingOption({ group, store: params.store })
|
||||
: undefined;
|
||||
const methodOptions: Array<{ value: string; label: string; hint?: string }> = [
|
||||
...(keepExistingOption ? [keepExistingOption] : []),
|
||||
...group.options,
|
||||
{ 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) {
|
||||
|
||||
Reference in New Issue
Block a user