From 5e6500b52f242afdf436a851759230d5308a99e7 Mon Sep 17 00:00:00 2001 From: Victor Navarro <36263538+vimtor@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:00:33 +0000 Subject: [PATCH] feat(cli): add console logout command --- packages/cli/src/commands/commands.ts | 3 + .../src/commands/handlers/console/logout.ts | 32 +++++++ packages/cli/src/index.ts | 1 + packages/cli/test/console.test.ts | 86 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 packages/cli/src/commands/handlers/console/logout.ts create mode 100644 packages/cli/test/console.test.ts diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index f08884c15a1c..30c724f1dca0 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -79,6 +79,9 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO url: Argument.string("url").pipe(Argument.withDescription("Console server URL"), Argument.optional), }, }), + Spec.make("logout", { + description: "Log out of OpenCode Console", + }), ], }), Spec.make("auth", { diff --git a/packages/cli/src/commands/handlers/console/logout.ts b/packages/cli/src/commands/handlers/console/logout.ts new file mode 100644 index 000000000000..e331d134ee98 --- /dev/null +++ b/packages/cli/src/commands/handlers/console/logout.ts @@ -0,0 +1,32 @@ +import { EOL } from "node:os" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/client" +import { Service } from "@opencode-ai/client/effect/service" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +const integrationID = "opencode" +const location = { directory: process.cwd() } + +export default Runtime.handler( + Commands.commands.console.commands.logout, + Effect.fn("cli.console.logout")(function* () { + const endpoint = yield* Service.ensure(yield* ServiceConfig.options()) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const found = yield* Effect.promise(() => client.integration.get({ integrationID, location })) + const credentials = found.data?.connections.filter((connection) => connection.type === "credential") ?? [] + + if (credentials.length === 0) { + process.stdout.write("Not logged in" + EOL) + return + } + + yield* Effect.forEach( + credentials, + (connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })), + { discard: true }, + ) + process.stdout.write("Logged out from OpenCode Console" + EOL) + }), +) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ecfbd5028267..892d36be221a 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -25,6 +25,7 @@ const Handlers = Runtime.handlers(Commands, { }, console: { login: () => import("./commands/handlers/console/login"), + logout: () => import("./commands/handlers/console/logout"), }, mcp: { list: () => import("./commands/handlers/mcp/list"), diff --git a/packages/cli/test/console.test.ts b/packages/cli/test/console.test.ts new file mode 100644 index 000000000000..f8ccc0175057 --- /dev/null +++ b/packages/cli/test/console.test.ts @@ -0,0 +1,86 @@ +import { afterEach, expect, test } from "bun:test" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { OPENCODE_VERSION } from "../src/version" + +const cleanup: Array<() => Promise> = [] + +afterEach(async () => { + await Promise.all(cleanup.splice(0).map((fn) => fn())) +}) + +async function cli(args: string[], env?: Record) { + const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { + cwd: path.join(import.meta.dir, ".."), + env: { ...process.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + return { stdout, stderr, exitCode } +} + +test("registers the console logout command", async () => { + const result = await cli(["console", "--help"]) + + expect(result.exitCode).toBe(0) + expect(result.stdout).toContain("logout Log out of OpenCode Console") +}) + +test("removes stored OpenCode Console credentials", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-console-logout-")) + const removed: string[] = [] + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/api/health") { + return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid }) + } + if (request.method === "GET" && url.pathname === "/api/integration/opencode") { + return Response.json({ + location: { directory: process.cwd(), project: { id: "global", directory: "/" } }, + data: { + id: "opencode", + name: "OpenCode", + methods: [], + connections: removed.length === 0 ? [{ type: "credential", id: "cred_test", label: "default" }] : [], + }, + }) + } + if (request.method === "DELETE" && url.pathname === "/api/credential/cred_test") { + removed.push(url.pathname) + return new Response(null, { status: 204 }) + } + return new Response(null, { status: 404 }) + }, + }) + cleanup.push(async () => { + server.stop(true) + await fs.rm(root, { recursive: true, force: true }) + }) + await fs.mkdir(path.join(root, "state", "opencode"), { recursive: true }) + await fs.writeFile( + path.join(root, "state", "opencode", "service-local.json"), + JSON.stringify({ version: OPENCODE_VERSION, url: server.url.toString(), pid: process.pid }), + ) + const env = { + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_STATE_HOME: path.join(root, "state"), + } + + const result = await cli(["console", "logout"], env) + + expect(result).toMatchObject({ exitCode: 0, stdout: "Logged out from OpenCode Console\n" }) + expect(removed).toEqual(["/api/credential/cred_test"]) + + const repeated = await cli(["console", "logout"], env) + expect(repeated).toMatchObject({ exitCode: 0, stdout: "Not logged in\n" }) +})