From 3a0bf2f39f784d1b1f54e9fbe6c4df32a2ab67a7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 23 Mar 2026 13:11:38 -0400 Subject: [PATCH] fix console account URL handling (#18809) --- packages/opencode/src/cli/cmd/account.ts | 56 ++++++++++++++++++---- packages/opencode/test/cli/account.test.ts | 24 ++++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 packages/opencode/test/cli/account.test.ts diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index fe8747bce..c09294edd 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -10,6 +10,26 @@ const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() => const println = (msg: string) => Effect.sync(() => UI.println(msg)) +const dim = (value: string) => UI.Style.TEXT_DIM + value + UI.Style.TEXT_NORMAL + +const activeSuffix = (isActive: boolean) => (isActive ? dim(" (active)") : "") + +export const formatAccountLabel = (account: { email: string; url: string }, isActive: boolean) => + `${account.email} ${dim(account.url)}${activeSuffix(isActive)}` + +const formatOrgChoiceLabel = (account: { email: string }, org: { name: string }, isActive: boolean) => + `${org.name} (${account.email})${activeSuffix(isActive)}` + +export const formatOrgLine = ( + account: { email: string; url: string }, + org: { id: string; name: string }, + isActive: boolean, +) => { + const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " " + const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name + return ` ${dot} ${name} ${dim(account.email)} ${dim(account.url)} ${dim(org.id)}` +} + const isActiveOrgChoice = ( active: Option.Option<{ id: AccountID; active_org_id: OrgID | null }>, choice: { accountID: AccountID; orgID: OrgID }, @@ -76,10 +96,9 @@ const logoutEffect = Effect.fn("logout")(function* (email?: string) { const opts = accounts.map((a) => { const isActive = Option.isSome(activeID) && activeID.value === a.id - const server = UI.Style.TEXT_DIM + a.url + UI.Style.TEXT_NORMAL return { value: a, - label: isActive ? `${a.email} ${server}` + UI.Style.TEXT_DIM + " (active)" : `${a.email} ${server}`, + label: formatAccountLabel(a, isActive), } }) @@ -109,9 +128,7 @@ const switchEffect = Effect.fn("switch")(function* () { const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id }) return { value: { orgID: org.id, accountID: group.account.id, label: org.name }, - label: isActive - ? `${org.name} (${group.account.email})` + UI.Style.TEXT_DIM + " (active)" - : `${org.name} (${group.account.email})`, + label: formatOrgChoiceLabel(group.account, org, isActive), } }), ) @@ -139,15 +156,21 @@ const orgsEffect = Effect.fn("orgs")(function* () { for (const group of groups) { for (const org of group.orgs) { const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id }) - const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " " - const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name - const email = UI.Style.TEXT_DIM + group.account.email + UI.Style.TEXT_NORMAL - const id = UI.Style.TEXT_DIM + org.id + UI.Style.TEXT_NORMAL - yield* println(` ${dot} ${name} ${email} ${id}`) + yield* println(formatOrgLine(group.account, org, isActive)) } } }) +const openEffect = Effect.fn("open")(function* () { + const service = yield* Account.Service + const active = yield* service.active() + if (Option.isNone(active)) return yield* println("No active account") + + const url = active.value.url + yield* openBrowser(url) + yield* Prompt.outro("Opened " + url) +}) + export const LoginCommand = cmd({ command: "login ", describe: false, @@ -195,6 +218,15 @@ export const OrgsCommand = cmd({ }, }) +export const OpenCommand = cmd({ + command: "open", + describe: false, + async handler() { + UI.empty() + await Account.runPromise((_svc) => openEffect()) + }, +}) + export const ConsoleCommand = cmd({ command: "console", describe: false, @@ -216,6 +248,10 @@ export const ConsoleCommand = cmd({ ...OrgsCommand, describe: "list orgs", }) + .command({ + ...OpenCommand, + describe: "open active console account", + }) .demandCommand(), async handler() {}, }) diff --git a/packages/opencode/test/cli/account.test.ts b/packages/opencode/test/cli/account.test.ts new file mode 100644 index 000000000..3f8196887 --- /dev/null +++ b/packages/opencode/test/cli/account.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import stripAnsi from "strip-ansi" + +import { formatAccountLabel, formatOrgLine } from "../../src/cli/cmd/account" + +describe("console account display", () => { + test("includes the account url in account labels", () => { + expect(stripAnsi(formatAccountLabel({ email: "one@example.com", url: "https://one.example.com" }, false))).toBe( + "one@example.com https://one.example.com", + ) + }) + + test("includes the active marker in account labels", () => { + expect(stripAnsi(formatAccountLabel({ email: "one@example.com", url: "https://one.example.com" }, true))).toBe( + "one@example.com https://one.example.com (active)", + ) + }) + + test("includes the account url in org rows", () => { + expect( + stripAnsi(formatOrgLine({ email: "one@example.com", url: "https://one.example.com" }, { id: "org-1", name: "One" }, true)), + ).toBe(" ● One one@example.com https://one.example.com org-1") + }) +})