diff --git a/packages/cli/src/commands/handlers/auth/login.ts b/packages/cli/src/commands/handlers/auth/login.ts index 63641ee3350..67157f69180 100644 --- a/packages/cli/src/commands/handlers/auth/login.ts +++ b/packages/cli/src/commands/handlers/auth/login.ts @@ -74,30 +74,40 @@ const findIntegration = Effect.fn("cli.auth.login.integration")(function* (clien } const integrations = yield* loadIntegrations(client) if (target) return yield* resolveIntegration(integrations, target) - const available = integrations + const options = loginOptions(integrations) + if (options.length === 0) return yield* Effect.fail(new Error("No authentication integrations are available")) + const id = yield* prompt(() => + autocomplete({ + message: "Select an AI provider or MCP server", + maxItems: 8, + options, + }), + ) + return yield* resolveIntegration(integrations, id) +}) + +export function loginOptions(integrations: IntegrationInfo[]) { + return integrations .filter((integration) => connectMethods(integration).length > 0) .toSorted( (a, b) => + Number(b.metadata?.source === "mcp") - Number(a.metadata?.source === "mcp") || (integrationPriority.get(a.id) ?? integrationPriority.size) - (integrationPriority.get(b.id) ?? integrationPriority.size) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id), ) - if (available.length === 0) return yield* Effect.fail(new Error("No authentication integrations are available")) - const id = yield* prompt(() => - autocomplete({ - message: "Select integration", - maxItems: 8, - options: available.map((integration) => { - const option = { value: integration.id, label: integration.name, hint: integration.id } - if (integration.connections.length > 0) return { ...option, hint: "connected" } - if (integration.id === "opencode") return { ...option, hint: "recommended" } - return option - }), - }), - ) - return yield* resolveIntegration(available, id) -}) + .map((integration) => ({ + value: integration.id, + label: `${integration.metadata?.source === "mcp" ? "MCP server" : "AI provider"} · ${integration.name}`, + hint: + integration.connections.length > 0 + ? "connected" + : integration.id === "opencode" + ? "recommended" + : integration.id, + })) +} const chooseMethod = Effect.fn("cli.auth.login.method")(function* (methods: ConnectMethod[], target?: string) { if (target) return yield* resolveMethod(methods, target) diff --git a/packages/cli/test/auth-login-options.test.ts b/packages/cli/test/auth-login-options.test.ts new file mode 100644 index 00000000000..d20630be47d --- /dev/null +++ b/packages/cli/test/auth-login-options.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from "bun:test" +import type { IntegrationInfo } from "@opencode/client" +import { loginOptions } from "../src/commands/handlers/auth/login" + +const integration = (value: Partial & Pick): IntegrationInfo => ({ + methods: [{ type: "key" }], + connections: [], + ...value, +}) + +test("distinguishes MCP servers from AI providers while retaining login targets and status", () => { + expect( + loginOptions([ + integration({ id: "openai", name: "OpenAI" }), + integration({ id: "linear", name: "Linear", metadata: { source: "mcp" } }), + integration({ id: "opencode", name: "OpenCode Zen" }), + integration({ id: "opencode-go", name: "OpenCode Go", connections: [{ type: "env", name: "GO_KEY" }] }), + integration({ id: "unused", name: "Unused", methods: [{ type: "env", names: ["UNUSED_KEY"] }] }), + ]), + ).toEqual([ + { value: "linear", label: "MCP server · Linear", hint: "linear" }, + { value: "opencode-go", label: "AI provider · OpenCode Go", hint: "connected" }, + { value: "opencode", label: "AI provider · OpenCode Zen", hint: "recommended" }, + { value: "openai", label: "AI provider · OpenAI", hint: "openai" }, + ]) +}) + +test("sorts MCP servers by name ahead of providers regardless of ID", () => { + expect( + loginOptions([ + integration({ id: "openai", name: "OpenAI" }), + integration({ id: "zulu", name: "Zulu", metadata: { source: "mcp" } }), + integration({ id: "alpha", name: "Alpha", metadata: { source: "mcp" } }), + ]).map((option) => option.value), + ).toEqual(["alpha", "zulu", "openai"]) +})