diff --git a/apps/cli/src/commands/auth/logout.test.ts b/apps/cli/src/commands/auth/logout.test.ts index c9acbef3..a527db4b 100644 --- a/apps/cli/src/commands/auth/logout.test.ts +++ b/apps/cli/src/commands/auth/logout.test.ts @@ -1,4 +1,4 @@ -import { loadConfig, removeSpace } from "@repo/config"; +import { loadConfig, removeAllSpaces, removeSpace } from "@repo/config"; import consola from "consola"; import { describe, expect, it, vi } from "vitest"; import { parseCommand } from "@repo/test-utils"; @@ -6,6 +6,7 @@ import { parseCommand } from "@repo/test-utils"; vi.mock("@repo/config", () => ({ loadConfig: vi.fn(), removeSpace: vi.fn(), + removeAllSpaces: vi.fn(), })); vi.mock("consola", () => import("@repo/test-utils/mock-consola")); @@ -75,4 +76,58 @@ describe("auth logout", () => { expect(removeSpace).toHaveBeenCalledWith("only.backlog.com"); expect(consola.success).toHaveBeenCalledWith("Logged out of only.backlog.com."); }); + + describe("--all", () => { + it("logs out of all spaces", async () => { + vi.mocked(loadConfig).mockReturnValue({ + spaces: [ + { + host: "one.backlog.com", + auth: { method: "api-key" as const, apiKey: "key1" }, + }, + { + host: "two.backlog.com", + auth: { method: "api-key" as const, apiKey: "key2" }, + }, + ], + defaultSpace: "one.backlog.com", + aliases: {}, + }); + + await parseCommand(() => import("./logout"), ["--all"]); + + expect(removeAllSpaces).toHaveBeenCalled(); + expect(consola.success).toHaveBeenCalledWith("Logged out of 2 space(s)."); + }); + + it("shows message when no spaces are configured with --all", async () => { + vi.mocked(loadConfig).mockReturnValue({ + spaces: [], + defaultSpace: undefined, + aliases: {}, + }); + + await parseCommand(() => import("./logout"), ["--all"]); + + expect(removeAllSpaces).not.toHaveBeenCalled(); + expect(consola.info).toHaveBeenCalledWith("No spaces are currently authenticated."); + }); + + it("throws error when --all is used with --space", async () => { + vi.mocked(loadConfig).mockReturnValue({ + spaces: [ + { + host: "example.backlog.com", + auth: { method: "api-key" as const, apiKey: "key" }, + }, + ], + defaultSpace: undefined, + aliases: {}, + }); + + await expect( + parseCommand(() => import("./logout"), ["--all", "--space", "example.backlog.com"]), + ).rejects.toThrow("Cannot use --all with --space."); + }); + }); }); diff --git a/apps/cli/src/commands/auth/logout.ts b/apps/cli/src/commands/auth/logout.ts index 3fc7b76d..62310bb7 100644 --- a/apps/cli/src/commands/auth/logout.ts +++ b/apps/cli/src/commands/auth/logout.ts @@ -1,5 +1,5 @@ import { UserError } from "@repo/cli-utils"; -import { loadConfig, removeSpace } from "@repo/config"; +import { loadConfig, removeAllSpaces, removeSpace } from "@repo/config"; import consola from "consola"; import { BeeCommand } from "../../lib/bee-command"; import * as opt from "../../lib/common-options"; @@ -10,16 +10,36 @@ const logout = new BeeCommand("logout") `Removes stored credentials locally. Does not revoke API keys or OAuth tokens on the server.`, ) .addOption(opt.space()) + .option("--all", "Log out of all spaces") .examples([ { description: "Select space via prompt", command: "bee auth logout" }, { description: "Log out of a specific space", command: "bee auth logout -s xxx.backlog.com", }, + { + description: "Log out of all spaces", + command: "bee auth logout --all", + }, ]) .action(async (opts) => { + if (opts.all && opts.space) { + throw new UserError("Cannot use --all with --space."); + } + const config = loadConfig(); + if (opts.all) { + if (config.spaces.length === 0) { + consola.info("No spaces are currently authenticated."); + return; + } + + removeAllSpaces(); + consola.success(`Logged out of ${config.spaces.length} space(s).`); + return; + } + let hostname = opts.space; if (!hostname) { if (config.spaces.length === 0) { diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 664453b6..e711f171 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,3 +1,3 @@ export { loadConfig, updateConfig, writeConfig } from "./config"; -export { addSpace, findSpace, removeSpace, updateSpaceAuth } from "./space"; +export { addSpace, findSpace, removeAllSpaces, removeSpace, updateSpaceAuth } from "./space"; export type { Rc, RcAuth, RcSpace } from "./schema"; diff --git a/packages/config/src/space.test.ts b/packages/config/src/space.test.ts index a83763a1..c94e8f81 100644 --- a/packages/config/src/space.test.ts +++ b/packages/config/src/space.test.ts @@ -5,7 +5,8 @@ vi.mock("./config", () => ({ })); const { updateConfig } = await import("./config"); -const { addSpace, findSpace, removeSpace, updateSpaceAuth } = await import("./space"); +const { addSpace, findSpace, removeAllSpaces, removeSpace, updateSpaceAuth } = + await import("./space"); const mockUpdateConfig = vi.mocked(updateConfig); @@ -92,6 +93,30 @@ describe("removeSpace", () => { }); }); +describe("removeAllSpaces", () => { + it("removes all spaces and clears defaultSpace", () => { + const space1 = makeSpace("one.backlog.com"); + const space2 = makeSpace("two.backlog.com"); + setupUpdateConfig(makeConfig([space1, space2], "one.backlog.com")); + + removeAllSpaces(); + + const result = mockUpdateConfig.mock.results[0]?.value; + expect(result.spaces).toEqual([]); + expect(result.defaultSpace).toBeUndefined(); + }); + + it("works when no spaces exist", () => { + setupUpdateConfig(makeConfig([])); + + removeAllSpaces(); + + const result = mockUpdateConfig.mock.results[0]?.value; + expect(result.spaces).toEqual([]); + expect(result.defaultSpace).toBeUndefined(); + }); +}); + describe("updateSpaceAuth", () => { it("updates auth for an existing space", () => { const space = makeSpace("target.backlog.com"); diff --git a/packages/config/src/space.ts b/packages/config/src/space.ts index 20eb2d0b..24e86037 100644 --- a/packages/config/src/space.ts +++ b/packages/config/src/space.ts @@ -22,6 +22,14 @@ const removeSpace = (host: string): void => { }); }; +const removeAllSpaces = (): void => { + updateConfig((config) => ({ + ...config, + spaces: [], + defaultSpace: undefined, + })); +}; + const updateSpaceAuth = (host: string, auth: RcAuth): void => { updateConfig((config) => { const index = config.spaces.findIndex((space) => space.host === host); @@ -56,4 +64,4 @@ const findSpace = (spaces: readonly RcSpace[], host: string): RcSpace | null => return null; }; -export { addSpace, findSpace, removeSpace, updateSpaceAuth }; +export { addSpace, findSpace, removeAllSpaces, removeSpace, updateSpaceAuth };