diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index 60349da823e..d7afcbbb3ce 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -11,6 +11,7 @@ "provider": "anthropic", "method": "cli", "choiceId": "anthropic-cli", + "deprecatedChoiceIds": ["claude-cli"], "choiceLabel": "Anthropic Claude CLI", "choiceHint": "Reuse a local Claude CLI login on this host", "groupId": "anthropic", diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index d129118c87e..f6cbcda832b 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -11,6 +11,7 @@ "provider": "openai-codex", "method": "oauth", "choiceId": "openai-codex", + "deprecatedChoiceIds": ["codex-cli"], "choiceLabel": "OpenAI Codex (ChatGPT OAuth)", "choiceHint": "Browser sign-in", "groupId": "openai", diff --git a/src/commands/auth-choice-legacy.test.ts b/src/commands/auth-choice-legacy.test.ts index 1b44d3366a0..7914a316ca1 100644 --- a/src/commands/auth-choice-legacy.test.ts +++ b/src/commands/auth-choice-legacy.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + resolveLegacyAuthChoiceAliasesForCli, formatDeprecatedNonInteractiveAuthChoiceError, normalizeLegacyOnboardAuthChoice, resolveDeprecatedAuthChoiceReplacement, @@ -16,4 +17,13 @@ describe("auth choice legacy aliases", () => { 'Auth choice "claude-cli" is deprecated.\nUse "--auth-choice anthropic-cli".', ); }); + + it("sources deprecated cli aliases from plugin manifests", () => { + expect(resolveLegacyAuthChoiceAliasesForCli()).toEqual([ + "setup-token", + "oauth", + "claude-cli", + "codex-cli", + ]); + }); }); diff --git a/src/commands/auth-choice-legacy.ts b/src/commands/auth-choice-legacy.ts index b62fa0c9f51..a4cd510fb1c 100644 --- a/src/commands/auth-choice-legacy.ts +++ b/src/commands/auth-choice-legacy.ts @@ -1,53 +1,114 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { + resolveManifestDeprecatedProviderAuthChoice, + resolveManifestProviderAuthChoices, +} from "../plugins/provider-auth-choices.js"; import type { AuthChoice } from "./onboard-types.js"; -export const AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI: ReadonlyArray = [ - "setup-token", - "oauth", - "claude-cli", - "codex-cli", -]; +function resolveLegacyCliBackendChoice( + choice: string, + params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + }, +) { + if (!choice.endsWith("-cli")) { + return undefined; + } + return resolveManifestDeprecatedProviderAuthChoice(choice, params); +} + +function resolveReplacementLabel(choiceLabel: string): string { + return choiceLabel.trim() || "the replacement auth choice"; +} + +export function resolveLegacyAuthChoiceAliasesForCli(params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): ReadonlyArray { + const manifestCliAliases = resolveManifestProviderAuthChoices(params) + .flatMap((choice) => choice.deprecatedChoiceIds ?? []) + .filter((choice): choice is AuthChoice => choice.endsWith("-cli")) + .toSorted((left, right) => left.localeCompare(right)); + return ["setup-token", "oauth", ...manifestCliAliases]; +} export function normalizeLegacyOnboardAuthChoice( authChoice: AuthChoice | undefined, + params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + }, ): AuthChoice | undefined { if (authChoice === "oauth") { return "setup-token"; } - if (authChoice === "claude-cli") { - return "anthropic-cli"; - } - if (authChoice === "codex-cli") { - return "openai-codex"; + if (typeof authChoice === "string") { + const deprecatedChoice = resolveLegacyCliBackendChoice(authChoice, params); + if (deprecatedChoice) { + return deprecatedChoice.choiceId as AuthChoice; + } } return authChoice; } export function isDeprecatedAuthChoice( authChoice: AuthChoice | undefined, -): authChoice is "claude-cli" | "codex-cli" { - return authChoice === "claude-cli" || authChoice === "codex-cli"; + params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + }, +): authChoice is AuthChoice { + return ( + typeof authChoice === "string" && Boolean(resolveLegacyCliBackendChoice(authChoice, params)) + ); } -export function resolveDeprecatedAuthChoiceReplacement(authChoice: "claude-cli" | "codex-cli"): { - normalized: AuthChoice; - message: string; -} { - if (authChoice === "claude-cli") { - return { - normalized: "anthropic-cli", - message: 'Auth choice "claude-cli" is deprecated; using Anthropic Claude CLI setup instead.', - }; +export function resolveDeprecatedAuthChoiceReplacement( + authChoice: AuthChoice, + params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + }, +): + | { + normalized: AuthChoice; + message: string; + } + | undefined { + if (typeof authChoice !== "string") { + return undefined; } + const deprecatedChoice = resolveLegacyCliBackendChoice(authChoice, params); + if (!deprecatedChoice) { + return undefined; + } + const replacementLabel = resolveReplacementLabel(deprecatedChoice.choiceLabel); return { - normalized: "openai-codex", - message: 'Auth choice "codex-cli" is deprecated; using OpenAI Codex OAuth instead.', + normalized: deprecatedChoice.choiceId as AuthChoice, + message: `Auth choice "${authChoice}" is deprecated; using ${replacementLabel} setup instead.`, }; } export function formatDeprecatedNonInteractiveAuthChoiceError( - authChoice: "claude-cli" | "codex-cli", -): string { - const replacement = - authChoice === "claude-cli" ? '"--auth-choice anthropic-cli"' : '"--auth-choice openai-codex"'; - return [`Auth choice "${authChoice}" is deprecated.`, `Use ${replacement}.`].join("\n"); + authChoice: AuthChoice, + params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + }, +): string | undefined { + const replacement = resolveDeprecatedAuthChoiceReplacement(authChoice, params); + if (!replacement) { + return undefined; + } + return [ + `Auth choice "${authChoice}" is deprecated.`, + `Use "--auth-choice ${replacement.normalized}".`, + ].join("\n"); } diff --git a/src/commands/auth-choice-options.static.ts b/src/commands/auth-choice-options.static.ts index 817a683890b..73bf6d48d6e 100644 --- a/src/commands/auth-choice-options.static.ts +++ b/src/commands/auth-choice-options.static.ts @@ -1,4 +1,4 @@ -import { AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI } from "./auth-choice-legacy.js"; +import { resolveLegacyAuthChoiceAliasesForCli } from "./auth-choice-legacy.js"; import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js"; export type { AuthChoiceGroupId }; @@ -33,6 +33,9 @@ export const CORE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ export function formatStaticAuthChoiceChoicesForCli(params?: { includeSkip?: boolean; includeLegacyAliases?: boolean; + config?: import("../config/config.js").OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; }): string { const includeSkip = params?.includeSkip ?? true; const includeLegacyAliases = params?.includeLegacyAliases ?? false; @@ -42,7 +45,7 @@ export function formatStaticAuthChoiceChoicesForCli(params?: { values.push("skip"); } if (includeLegacyAliases) { - values.push(...AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI); + values.push(...resolveLegacyAuthChoiceAliasesForCli(params)); } return values.join("|"); diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index e248c35dfae..0cc771f19e1 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -271,6 +271,25 @@ describe("buildAuthChoiceOptions", () => { }); it("can include legacy aliases in cli help choices", () => { + resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "anthropic", + providerId: "anthropic", + methodId: "cli", + choiceId: "anthropic-cli", + choiceLabel: "Anthropic Claude CLI", + deprecatedChoiceIds: ["claude-cli"], + }, + { + pluginId: "openai", + providerId: "openai-codex", + methodId: "oauth", + choiceId: "openai-codex", + choiceLabel: "OpenAI Codex (ChatGPT OAuth)", + deprecatedChoiceIds: ["codex-cli"], + }, + ]); + const cliChoices = formatAuthChoiceChoicesForCli({ includeLegacyAliases: true, includeSkip: true, diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index a2b8f31c206..128655d887f 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -28,7 +28,10 @@ export async function applyAuthChoice( params: ApplyAuthChoiceParams, ): Promise { const normalizedAuthChoice = - normalizeLegacyOnboardAuthChoice(params.authChoice) ?? params.authChoice; + normalizeLegacyOnboardAuthChoice(params.authChoice, { + config: params.config, + env: process.env, + }) ?? params.authChoice; const normalizedProviderAuthChoice = normalizeApiKeyTokenProviderAuthChoice({ authChoice: normalizedAuthChoice, tokenProvider: params.opts?.tokenProvider, diff --git a/src/commands/auth-choice.preferred-provider.test.ts b/src/commands/auth-choice.preferred-provider.test.ts index 689b9a9f9fc..6e02cc071f9 100644 --- a/src/commands/auth-choice.preferred-provider.test.ts +++ b/src/commands/auth-choice.preferred-provider.test.ts @@ -1,11 +1,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const resolveManifestProviderAuthChoice = vi.hoisted(() => vi.fn()); +const resolveManifestDeprecatedProviderAuthChoice = vi.hoisted(() => vi.fn()); +const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn(() => [])); const resolveProviderPluginChoice = vi.hoisted(() => vi.fn()); const resolvePluginProviders = vi.hoisted(() => vi.fn(() => [])); vi.mock("../plugins/provider-auth-choices.js", () => ({ resolveManifestProviderAuthChoice, + resolveManifestDeprecatedProviderAuthChoice, + resolveManifestProviderAuthChoices, })); vi.mock("../plugins/provider-wizard.js", () => ({ @@ -22,6 +26,8 @@ describe("resolvePreferredProviderForAuthChoice", () => { beforeEach(() => { vi.clearAllMocks(); resolveManifestProviderAuthChoice.mockReturnValue(undefined); + resolveManifestDeprecatedProviderAuthChoice.mockReturnValue(undefined); + resolveManifestProviderAuthChoices.mockReturnValue([]); resolvePluginProviders.mockReturnValue([]); resolveProviderPluginChoice.mockReturnValue(null); }); @@ -42,25 +48,23 @@ describe("resolvePreferredProviderForAuthChoice", () => { }); it("normalizes legacy auth choices before plugin lookup", async () => { - resolveProviderPluginChoice.mockReturnValue({ - provider: { id: "anthropic", label: "Anthropic", auth: [] }, - method: { id: "setup-token", label: "setup-token", kind: "token" }, + resolveManifestDeprecatedProviderAuthChoice.mockReturnValue({ + choiceId: "anthropic-cli", + choiceLabel: "Anthropic Claude CLI", + }); + resolveManifestProviderAuthChoice.mockReturnValue({ + pluginId: "anthropic", + providerId: "anthropic", + methodId: "cli", + choiceId: "anthropic-cli", + choiceLabel: "Anthropic Claude CLI", }); await expect(resolvePreferredProviderForAuthChoice({ choice: "claude-cli" })).resolves.toBe( "anthropic", ); - expect(resolveProviderPluginChoice).toHaveBeenCalledWith( - expect.objectContaining({ - choice: "setup-token", - }), - ); - expect(resolvePluginProviders).toHaveBeenCalledWith( - expect.objectContaining({ - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, - }), - ); + expect(resolveProviderPluginChoice).not.toHaveBeenCalled(); + expect(resolvePluginProviders).not.toHaveBeenCalled(); }); it("falls back to static core choices when no provider plugin claims the choice", async () => { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts index 918995bfb9b..96d3c9c8051 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -13,8 +13,10 @@ vi.mock("../api-keys.js", () => ({ })); const resolveManifestDeprecatedProviderAuthChoice = vi.hoisted(() => vi.fn(() => undefined)); +const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn(() => [])); vi.mock("../../../plugins/provider-auth-choices.js", () => ({ resolveManifestDeprecatedProviderAuthChoice, + resolveManifestProviderAuthChoices, })); beforeEach(() => { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 981255b1b86..80bc1d4f422 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -116,8 +116,13 @@ export async function applyNonInteractiveAuthChoice(params: { ...(params.metadata ? { metadata: params.metadata } : {}), }; }; - if (isDeprecatedAuthChoice(authChoice)) { - runtime.error(formatDeprecatedNonInteractiveAuthChoiceError(authChoice)); + if (isDeprecatedAuthChoice(authChoice, { config: nextConfig, env: process.env })) { + runtime.error( + formatDeprecatedNonInteractiveAuthChoiceError(authChoice, { + config: nextConfig, + env: process.env, + })!, + ); runtime.exit(1); return null; } diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index fc2c0d59d9b..9497c277e45 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -7,7 +7,6 @@ export type BuiltInAuthChoice = // Legacy alias for `setup-token` (kept for backwards CLI compatibility). | "oauth" | "setup-token" - | "claude-cli" | "token" | "chutes" | "deepseek-api-key" @@ -25,7 +24,6 @@ export type BuiltInAuthChoice = | "venice-api-key" | "together-api-key" | "huggingface-api-key" - | "codex-cli" | "apiKey" | "gemini-api-key" | "google-gemini-cli" diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 236fc4e009b..914f8c4565a 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -23,14 +23,22 @@ export async function setupWizardCommand( ) { assertSupportedRuntime(runtime); const originalAuthChoice = opts.authChoice; - const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice); - if (opts.nonInteractive && isDeprecatedAuthChoice(originalAuthChoice)) { - runtime.error(formatDeprecatedNonInteractiveAuthChoiceError(originalAuthChoice)); + const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice, { + env: process.env, + }); + if (opts.nonInteractive && isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) { + runtime.error( + formatDeprecatedNonInteractiveAuthChoiceError(originalAuthChoice, { + env: process.env, + })!, + ); runtime.exit(1); return; } - if (isDeprecatedAuthChoice(originalAuthChoice)) { - runtime.log(resolveDeprecatedAuthChoiceReplacement(originalAuthChoice).message); + if (isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) { + runtime.log( + resolveDeprecatedAuthChoiceReplacement(originalAuthChoice, { env: process.env })!.message, + ); } const flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow; const normalizedOpts = diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index 011f0a5401e..3ce0b4c20da 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -192,6 +192,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ provider: "anthropic", method: "cli", choiceId: "anthropic-cli", + deprecatedChoiceIds: ["claude-cli"], choiceLabel: "Anthropic Claude CLI", choiceHint: "Reuse a local Claude CLI login on this host", groupId: "anthropic", @@ -11374,6 +11375,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ provider: "openai-codex", method: "oauth", choiceId: "openai-codex", + deprecatedChoiceIds: ["codex-cli"], choiceLabel: "OpenAI Codex (ChatGPT OAuth)", choiceHint: "Browser sign-in", groupId: "openai", diff --git a/src/plugins/provider-auth-choice-preference.ts b/src/plugins/provider-auth-choice-preference.ts index 7c3e51048ed..85857c5f3eb 100644 --- a/src/plugins/provider-auth-choice-preference.ts +++ b/src/plugins/provider-auth-choice-preference.ts @@ -3,11 +3,12 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveManifestProviderAuthChoice } from "./provider-auth-choices.js"; const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { + chutes: "chutes", "custom-api-key": "custom", }; function normalizeLegacyAuthChoice(choice: string): string { - return normalizeLegacyOnboardAuthChoice(choice) ?? choice; + return normalizeLegacyOnboardAuthChoice(choice, { env: process.env }) ?? choice; } export async function resolvePreferredProviderForAuthChoice(params: {