diff --git a/CHANGELOG.md b/CHANGELOG.md index 03350f799..307f37be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.clawd.bot - Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer. - Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x. - Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok. +- Onboarding: honor `--auth-choice apiKey` token provider + token flags to skip prompts. (#1485) Thanks @iHildy. - Agents: surface concrete API error details instead of generic AI service errors. - Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484) - Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj. diff --git a/docs/cli/index.md b/docs/cli/index.md index 46f6d173e..0438fb8e3 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -295,8 +295,8 @@ Options: - `--mode ` - `--flow ` (manual is an alias for advanced) - `--auth-choice ` -- `--token-provider ` (non-interactive; used with `--auth-choice token`) -- `--token ` (non-interactive; used with `--auth-choice token`) +- `--token-provider ` (non-interactive; used with `--auth-choice token` or `apiKey`) +- `--token ` (non-interactive; used with `--auth-choice token` or `apiKey`) - `--token-profile-id ` (non-interactive; default: `:manual`) - `--token-expires-in ` (non-interactive; e.g. `365d`, `12h`) - `--anthropic-api-key ` diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 281464b6f..a10a5fc52 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -56,9 +56,12 @@ export function registerOnboardCommand(program: Command) { ) .option( "--token-provider ", - "Token provider id (non-interactive; used with --auth-choice token)", + "Token provider id (non-interactive; used with --auth-choice token or apiKey)", + ) + .option( + "--token ", + "Token value (non-interactive; used with --auth-choice token or apiKey)", ) - .option("--token ", "Token value (non-interactive; used with --auth-choice token)") .option( "--token-profile-id ", "Auth profile id (non-interactive; default: :manual)", diff --git a/src/commands/auth-choice.api-key-flags.test.ts b/src/commands/auth-choice.api-key-flags.test.ts new file mode 100644 index 000000000..f2a632ef4 --- /dev/null +++ b/src/commands/auth-choice.api-key-flags.test.ts @@ -0,0 +1,183 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { RuntimeEnv } from "../runtime.js"; +import type { WizardPrompter } from "../wizard/prompts.js"; +import { AUTH_STORE_VERSION } from "../agents/auth-profiles/constants.js"; +import { applyAuthChoice } from "./auth-choice.js"; + +const noopAsync = async () => {}; +const noop = () => {}; +const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json"); + +const previousStateDir = process.env.CLAWDBOT_STATE_DIR; +const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; +const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; +const previousAnthropicKey = process.env.ANTHROPIC_API_KEY; +const previousOpenaiKey = process.env.OPENAI_API_KEY; +let tempStateDir: string | null = null; + +async function setupTempState() { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-")); + process.env.CLAWDBOT_STATE_DIR = tempStateDir; + process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; + await fs.mkdir(process.env.CLAWDBOT_AGENT_DIR, { recursive: true }); +} + +function buildPrompter() { + const text = vi.fn(async () => ""); + const confirm = vi.fn(async () => false); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select: vi.fn(async () => "" as never), + multiselect: vi.fn(async () => []), + text, + confirm, + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + return { prompter, text, confirm }; +} + +const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), +}; + +afterEach(async () => { + if (tempStateDir) { + await fs.rm(tempStateDir, { recursive: true, force: true }); + tempStateDir = null; + } + if (previousStateDir === undefined) { + delete process.env.CLAWDBOT_STATE_DIR; + } else { + process.env.CLAWDBOT_STATE_DIR = previousStateDir; + } + if (previousAgentDir === undefined) { + delete process.env.CLAWDBOT_AGENT_DIR; + } else { + process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; + } + if (previousPiAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + } + if (previousAnthropicKey === undefined) { + delete process.env.ANTHROPIC_API_KEY; + } else { + process.env.ANTHROPIC_API_KEY = previousAnthropicKey; + } + if (previousOpenaiKey === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = previousOpenaiKey; + } +}); + +describe("applyAuthChoice with apiKey flags", () => { + it("uses provided openrouter token when authChoice=apiKey", async () => { + await setupTempState(); + const agentDir = process.env.CLAWDBOT_AGENT_DIR ?? ""; + const authProfilePath = authProfilePathFor(agentDir); + await fs.writeFile( + authProfilePath, + JSON.stringify({ + version: AUTH_STORE_VERSION, + profiles: { + "openrouter:legacy": { + type: "oauth", + provider: "openrouter", + access: "access", + refresh: "refresh", + expires: Date.now() + 60_000, + }, + }, + }), + "utf8", + ); + + const { prompter, text, confirm } = buildPrompter(); + const result = await applyAuthChoice({ + authChoice: "apiKey", + config: {}, + prompter, + runtime, + setDefaultModel: true, + opts: { + tokenProvider: "openrouter", + token: "sk-openrouter-flag", + }, + }); + + expect(text).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); + expect(result.config.auth?.profiles?.["openrouter:default"]).toMatchObject({ + provider: "openrouter", + mode: "api_key", + }); + + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { profiles?: Record }; + expect(parsed.profiles?.["openrouter:default"]?.key).toBe("sk-openrouter-flag"); + }); + + it("uses provided openai token when authChoice=apiKey", async () => { + await setupTempState(); + const { prompter, text, confirm } = buildPrompter(); + + await applyAuthChoice({ + authChoice: "apiKey", + config: {}, + prompter, + runtime, + setDefaultModel: true, + opts: { + tokenProvider: "openai", + token: "sk-openai-flag", + }, + }); + + expect(text).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); + expect(process.env.OPENAI_API_KEY).toBe("sk-openai-flag"); + const envPath = path.join(process.env.CLAWDBOT_STATE_DIR ?? "", ".env"); + const envContents = await fs.readFile(envPath, "utf8"); + expect(envContents).toContain("OPENAI_API_KEY=sk-openai-flag"); + }); + + it("uses provided anthropic token when authChoice=apiKey", async () => { + await setupTempState(); + process.env.ANTHROPIC_API_KEY = "sk-env-test"; + const { prompter, text, confirm } = buildPrompter(); + + await applyAuthChoice({ + authChoice: "apiKey", + config: {}, + prompter, + runtime, + setDefaultModel: true, + opts: { + tokenProvider: "anthropic", + token: "sk-anthropic-flag", + }, + }); + + expect(text).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); + + const authProfilePath = authProfilePathFor(process.env.CLAWDBOT_AGENT_DIR ?? ""); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { profiles?: Record }; + expect(parsed.profiles?.["anthropic:default"]?.key).toBe("sk-anthropic-flag"); + }); +}); diff --git a/src/commands/auth-choice.apply.anthropic.ts b/src/commands/auth-choice.apply.anthropic.ts index c5700663c..8d3ee3958 100644 --- a/src/commands/auth-choice.apply.anthropic.ts +++ b/src/commands/auth-choice.apply.anthropic.ts @@ -8,6 +8,7 @@ import { normalizeApiKeyInput, validateApiKeyInput, } from "./auth-choice.api-key.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js"; import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js"; @@ -198,19 +199,22 @@ export async function applyAuthChoiceAnthropic( } if (params.authChoice === "apiKey") { - if (params.opts?.tokenProvider && params.opts.tokenProvider !== "anthropic") { + const tokenProvider = params.opts?.tokenProvider + ? normalizeProviderId(params.opts.tokenProvider) + : undefined; + const explicitToken = + tokenProvider === "anthropic" ? normalizeApiKeyInput(params.opts?.token ?? "") : ""; + if (tokenProvider && tokenProvider !== "anthropic") { return null; } let nextConfig = params.config; let hasCredential = false; const envKey = process.env.ANTHROPIC_API_KEY?.trim(); - - if (params.opts?.token) { - await setAnthropicApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + if (explicitToken) { + await setAnthropicApiKey(explicitToken, params.agentDir); hasCredential = true; } - if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing ANTHROPIC_API_KEY (env, ${formatApiKeyPreview(envKey)})?`, diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index cddb7f8e0..b7aaa929d 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -1,4 +1,5 @@ import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; import { resolveEnvApiKey } from "../agents/model-auth.js"; import { formatApiKeyPreview, @@ -6,6 +7,7 @@ import { validateApiKeyInput, } from "./auth-choice.api-key.js"; import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import type { AuthChoice } from "./onboard-types.js"; import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; import { applyGoogleGeminiModelDefault, @@ -56,84 +58,86 @@ export async function applyAuthChoiceApiProviders( ); }; + const tokenProvider = params.opts?.tokenProvider + ? normalizeProviderId(params.opts.tokenProvider) + : undefined; + const tokenValue = params.opts?.token; let authChoice = params.authChoice; if ( authChoice === "apiKey" && - params.opts?.tokenProvider && - params.opts.tokenProvider !== "anthropic" && - params.opts.tokenProvider !== "openai" + tokenProvider && + tokenProvider !== "anthropic" && + tokenProvider !== "openai" ) { - if (params.opts.tokenProvider === "openrouter") { - authChoice = "openrouter-api-key"; - } else if (params.opts.tokenProvider === "vercel-ai-gateway") { - authChoice = "ai-gateway-api-key"; - } else if (params.opts.tokenProvider === "moonshot") { - authChoice = "moonshot-api-key"; - } else if (params.opts.tokenProvider === "kimi-code") { - authChoice = "kimi-code-api-key"; - } else if (params.opts.tokenProvider === "google") { - authChoice = "gemini-api-key"; - } else if (params.opts.tokenProvider === "zai") { - authChoice = "zai-api-key"; - } else if (params.opts.tokenProvider === "synthetic") { - authChoice = "synthetic-api-key"; - } else if (params.opts.tokenProvider === "opencode") { - authChoice = "opencode-zen"; - } + const mapped: Partial> = { + openrouter: "openrouter-api-key", + "vercel-ai-gateway": "ai-gateway-api-key", + moonshot: "moonshot-api-key", + "kimi-code": "kimi-code-api-key", + google: "gemini-api-key", + zai: "zai-api-key", + synthetic: "synthetic-api-key", + opencode: "opencode-zen", + }; + authChoice = mapped[tokenProvider] ?? authChoice; } if (authChoice === "openrouter-api-key") { - const store = ensureAuthProfileStore(params.agentDir, { - allowKeychainPrompt: false, - }); - const profileOrder = resolveAuthProfileOrder({ - cfg: nextConfig, - store, - provider: "openrouter", - }); - const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); - const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; let profileId = "openrouter:default"; let mode: "api_key" | "oauth" | "token" = "api_key"; let hasCredential = false; + const explicitToken = + tokenProvider === "openrouter" ? normalizeApiKeyInput(tokenValue ?? "") : ""; - if (existingProfileId && existingCred?.type) { - profileId = existingProfileId; - mode = - existingCred.type === "oauth" - ? "oauth" - : existingCred.type === "token" - ? "token" - : "api_key"; + if (explicitToken) { + await setOpenrouterApiKey(explicitToken, params.agentDir); hasCredential = true; - } + } else { + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "openrouter", + }); + const existingProfileId = profileOrder.find((profileId) => + Boolean(store.profiles[profileId]), + ); + const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; + if (existingProfileId && existingCred?.type) { + profileId = existingProfileId; + mode = + existingCred.type === "oauth" + ? "oauth" + : existingCred.type === "token" + ? "token" + : "api_key"; + hasCredential = true; + } - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "openrouter") { - await setOpenrouterApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); - hasCredential = true; - } - - if (!hasCredential) { - const envKey = resolveEnvApiKey("openrouter"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setOpenrouterApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("openrouter"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setOpenrouterApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } - } - if (!hasCredential) { - const key = await params.prompter.text({ - message: "Enter OpenRouter API key", - validate: validateApiKeyInput, - }); - await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir); - hasCredential = true; + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter OpenRouter API key", + validate: validateApiKeyInput, + }); + await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + hasCredential = true; + } } if (hasCredential) { @@ -162,18 +166,14 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "ai-gateway-api-key") { let hasCredential = false; - - if ( - !hasCredential && - params.opts?.token && - params.opts?.tokenProvider === "vercel-ai-gateway" - ) { - await setVercelAiGatewayApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = + tokenProvider === "vercel-ai-gateway" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setVercelAiGatewayApiKey(explicitToken, params.agentDir); hasCredential = true; } - const envKey = resolveEnvApiKey("vercel-ai-gateway"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, @@ -214,14 +214,14 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "moonshot-api-key") { let hasCredential = false; - - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "moonshot") { - await setMoonshotApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = + tokenProvider === "moonshot" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setMoonshotApiKey(explicitToken, params.agentDir); hasCredential = true; } - const envKey = resolveEnvApiKey("moonshot"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, @@ -261,11 +261,12 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "kimi-code-api-key") { let hasCredential = false; - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kimi-code") { - await setKimiCodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = + tokenProvider === "kimi-code" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setKimiCodeApiKey(explicitToken, params.agentDir); hasCredential = true; } - if (!hasCredential) { await params.prompter.note( [ @@ -276,7 +277,7 @@ export async function applyAuthChoiceApiProviders( ); } const envKey = resolveEnvApiKey("kimi-code"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, @@ -317,14 +318,13 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "gemini-api-key") { let hasCredential = false; - - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "google") { - await setGeminiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = tokenProvider === "google" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setGeminiApiKey(explicitToken, params.agentDir); hasCredential = true; } - const envKey = resolveEnvApiKey("google"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, @@ -364,14 +364,13 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "zai-api-key") { let hasCredential = false; - - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") { - await setZaiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = tokenProvider === "zai" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setZaiApiKey(explicitToken, params.agentDir); hasCredential = true; } - const envKey = resolveEnvApiKey("zai"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, @@ -426,8 +425,10 @@ export async function applyAuthChoiceApiProviders( } if (authChoice === "synthetic-api-key") { - if (params.opts?.token && params.opts?.tokenProvider === "synthetic") { - await setSyntheticApiKey(String(params.opts.token).trim(), params.agentDir); + const explicitToken = + tokenProvider === "synthetic" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setSyntheticApiKey(explicitToken, params.agentDir); } else { const key = await params.prompter.text({ message: "Enter Synthetic API key", @@ -459,11 +460,12 @@ export async function applyAuthChoiceApiProviders( if (authChoice === "opencode-zen") { let hasCredential = false; - if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") { - await setOpencodeZenApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + const explicitToken = + tokenProvider === "opencode" ? normalizeApiKeyInput(tokenValue ?? "") : ""; + if (explicitToken) { + await setOpencodeZenApiKey(explicitToken, params.agentDir); hasCredential = true; } - if (!hasCredential) { await params.prompter.note( [ @@ -475,7 +477,7 @@ export async function applyAuthChoiceApiProviders( ); } const envKey = resolveEnvApiKey("opencode"); - if (envKey) { + if (!hasCredential && envKey) { const useExisting = await params.prompter.confirm({ message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, diff --git a/src/commands/auth-choice.apply.openai.ts b/src/commands/auth-choice.apply.openai.ts index 7d96a35a1..1731d3e72 100644 --- a/src/commands/auth-choice.apply.openai.ts +++ b/src/commands/auth-choice.apply.openai.ts @@ -1,5 +1,6 @@ import { loginOpenAICodex } from "@mariozechner/pi-ai"; import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "../agents/auth-profiles.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; import { resolveEnvApiKey } from "../agents/model-auth.js"; import { upsertSharedEnvVar } from "../infra/env-file.js"; import { isRemoteEnvironment } from "./oauth-env.js"; @@ -20,44 +21,47 @@ import { export async function applyAuthChoiceOpenAI( params: ApplyAuthChoiceParams, ): Promise { + const tokenProvider = params.opts?.tokenProvider + ? normalizeProviderId(params.opts.tokenProvider) + : undefined; + const explicitToken = + tokenProvider === "openai" ? normalizeApiKeyInput(params.opts?.token ?? "") : ""; let authChoice = params.authChoice; - if (authChoice === "apiKey" && params.opts?.tokenProvider === "openai") { + if (authChoice === "apiKey" && tokenProvider === "openai") { authChoice = "openai-api-key"; } if (authChoice === "openai-api-key") { - const envKey = resolveEnvApiKey("openai"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing OPENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - const result = upsertSharedEnvVar({ - key: "OPENAI_API_KEY", - value: envKey.apiKey, + if (!explicitToken) { + const envKey = resolveEnvApiKey("openai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OPENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, }); - if (!process.env.OPENAI_API_KEY) { - process.env.OPENAI_API_KEY = envKey.apiKey; + if (useExisting) { + const result = upsertSharedEnvVar({ + key: "OPENAI_API_KEY", + value: envKey.apiKey, + }); + if (!process.env.OPENAI_API_KEY) { + process.env.OPENAI_API_KEY = envKey.apiKey; + } + await params.prompter.note( + `Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`, + "OpenAI API key", + ); + return { config: params.config }; } - await params.prompter.note( - `Copied OPENAI_API_KEY to ${result.path} for launchd compatibility.`, - "OpenAI API key", - ); - return { config: params.config }; } } - let key: string | undefined; - if (params.opts?.token && params.opts?.tokenProvider === "openai") { - key = params.opts.token; - } else { - key = await params.prompter.text({ - message: "Enter OpenAI API key", - validate: validateApiKeyInput, - }); - } - + const key = explicitToken + ? explicitToken + : await params.prompter.text({ + message: "Enter OpenAI API key", + validate: validateApiKeyInput, + }); const trimmed = normalizeApiKeyInput(String(key)); const result = upsertSharedEnvVar({ key: "OPENAI_API_KEY", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index fad8fe483..cb7563f17 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -50,9 +50,9 @@ export type OnboardOptions = { acceptRisk?: boolean; reset?: boolean; authChoice?: AuthChoice; - /** Used when `authChoice=token` in non-interactive mode. */ + /** Used when `authChoice=token` in non-interactive mode, or `authChoice=apiKey` in wizard mode. */ tokenProvider?: string; - /** Used when `authChoice=token` in non-interactive mode. */ + /** Used when `authChoice=token` in non-interactive mode, or `authChoice=apiKey` in wizard mode. */ token?: string; /** Used when `authChoice=token` in non-interactive mode. */ tokenProfileId?: string;