diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index be5f07d26f12..3768c7a82973 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; -import type { AuthChoice, OnboardOptions } from "../onboard-types.js"; +import type { OnboardOptions } from "../onboard-types.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { resolveGatewayPort, writeConfigFile } from "../../config/config.js"; import { logConfigUpdated } from "../../config/logging.js"; @@ -13,6 +13,7 @@ import { resolveControlUiLinks, waitForGatewayReachable, } from "../onboard-helpers.js"; +import { inferAuthChoiceFromFlags } from "./local/auth-choice-inference.js"; import { applyNonInteractiveAuthChoice } from "./local/auth-choice.js"; import { installGatewayDaemonNonInteractive } from "./local/daemon-install.js"; import { applyNonInteractiveGatewayConfig } from "./local/gateway-config.js"; @@ -20,57 +21,6 @@ import { logNonInteractiveOnboardingJson } from "./local/output.js"; import { applyNonInteractiveSkillsConfig } from "./local/skills-config.js"; import { resolveNonInteractiveWorkspaceDir } from "./local/workspace.js"; -/** - * Infer --auth-choice from provider-specific API key flags when --auth-choice - * is not explicitly provided. This avoids silently skipping credential storage - * when the user passes e.g. `--anthropic-api-key` without `--auth-choice apiKey`. - */ -function inferAuthChoiceFromFlags(opts: OnboardOptions): AuthChoice | undefined { - if (opts.anthropicApiKey) { - return "apiKey"; - } - if (opts.geminiApiKey) { - return "gemini-api-key"; - } - if (opts.openaiApiKey) { - return "openai-api-key"; - } - if (opts.openrouterApiKey) { - return "openrouter-api-key"; - } - if (opts.aiGatewayApiKey) { - return "ai-gateway-api-key"; - } - if (opts.cloudflareAiGatewayApiKey) { - return "cloudflare-ai-gateway-api-key"; - } - if (opts.moonshotApiKey) { - return "moonshot-api-key"; - } - if (opts.kimiCodeApiKey) { - return "kimi-code-api-key"; - } - if (opts.syntheticApiKey) { - return "synthetic-api-key"; - } - if (opts.veniceApiKey) { - return "venice-api-key"; - } - if (opts.zaiApiKey) { - return "zai-api-key"; - } - if (opts.xiaomiApiKey) { - return "xiaomi-api-key"; - } - if (opts.minimaxApiKey) { - return "minimax-api"; - } - if (opts.opencodeZenApiKey) { - return "opencode-zen"; - } - return undefined; -} - export async function runNonInteractiveOnboardingLocal(params: { opts: OnboardOptions; runtime: RuntimeEnv; @@ -100,7 +50,19 @@ export async function runNonInteractiveOnboardingLocal(params: { }, }; - const authChoice = opts.authChoice ?? inferAuthChoiceFromFlags(opts) ?? "skip"; + const inferredAuthChoice = inferAuthChoiceFromFlags(opts); + if (!opts.authChoice && inferredAuthChoice.matches.length > 1) { + runtime.error( + [ + "Multiple API key flags were provided for non-interactive onboarding.", + "Use a single provider flag or pass --auth-choice explicitly.", + `Flags: ${inferredAuthChoice.matches.map((match) => match.label).join(", ")}`, + ].join("\n"), + ); + runtime.exit(1); + return; + } + const authChoice = opts.authChoice ?? inferredAuthChoice.choice ?? "skip"; const nextConfigAfterAuth = await applyNonInteractiveAuthChoice({ nextConfig, authChoice, diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts new file mode 100644 index 000000000000..c747c92d5446 --- /dev/null +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts @@ -0,0 +1,67 @@ +import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; + +type AuthChoiceFlag = { + flag: keyof AuthChoiceFlagOptions; + authChoice: AuthChoice; + label: string; +}; + +type AuthChoiceFlagOptions = Pick< + OnboardOptions, + | "anthropicApiKey" + | "geminiApiKey" + | "openaiApiKey" + | "openrouterApiKey" + | "aiGatewayApiKey" + | "cloudflareAiGatewayApiKey" + | "moonshotApiKey" + | "kimiCodeApiKey" + | "syntheticApiKey" + | "veniceApiKey" + | "zaiApiKey" + | "xiaomiApiKey" + | "minimaxApiKey" + | "opencodeZenApiKey" +>; + +const AUTH_CHOICE_FLAG_MAP = [ + { flag: "anthropicApiKey", authChoice: "apiKey", label: "--anthropic-api-key" }, + { flag: "geminiApiKey", authChoice: "gemini-api-key", label: "--gemini-api-key" }, + { flag: "openaiApiKey", authChoice: "openai-api-key", label: "--openai-api-key" }, + { flag: "openrouterApiKey", authChoice: "openrouter-api-key", label: "--openrouter-api-key" }, + { flag: "aiGatewayApiKey", authChoice: "ai-gateway-api-key", label: "--ai-gateway-api-key" }, + { + flag: "cloudflareAiGatewayApiKey", + authChoice: "cloudflare-ai-gateway-api-key", + label: "--cloudflare-ai-gateway-api-key", + }, + { flag: "moonshotApiKey", authChoice: "moonshot-api-key", label: "--moonshot-api-key" }, + { flag: "kimiCodeApiKey", authChoice: "kimi-code-api-key", label: "--kimi-code-api-key" }, + { flag: "syntheticApiKey", authChoice: "synthetic-api-key", label: "--synthetic-api-key" }, + { flag: "veniceApiKey", authChoice: "venice-api-key", label: "--venice-api-key" }, + { flag: "zaiApiKey", authChoice: "zai-api-key", label: "--zai-api-key" }, + { flag: "xiaomiApiKey", authChoice: "xiaomi-api-key", label: "--xiaomi-api-key" }, + { flag: "minimaxApiKey", authChoice: "minimax-api", label: "--minimax-api-key" }, + { flag: "opencodeZenApiKey", authChoice: "opencode-zen", label: "--opencode-zen-api-key" }, +] satisfies ReadonlyArray; + +export type AuthChoiceInference = { + choice?: AuthChoice; + matches: AuthChoiceFlag[]; +}; + +// Infer auth choice from explicit provider API key flags. +export function inferAuthChoiceFromFlags(opts: OnboardOptions): AuthChoiceInference { + const matches = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) => { + const value = opts[flag]; + if (typeof value === "string") { + return value.trim().length > 0; + } + return Boolean(value); + }); + + return { + choice: matches[0]?.authChoice, + matches, + }; +}