From 2efee2fc9f0ca4d50b62f499206de74455380774 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 8 Jul 2026 17:41:02 +0800 Subject: [PATCH 1/4] feat(cli): parse `void github disconnect` args Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/void/src/cli/github-args.ts | 44 ++++++++++++++- packages/void/test/unit/github-args.test.ts | 61 ++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/packages/void/src/cli/github-args.ts b/packages/void/src/cli/github-args.ts index 94f477fb9..8fa2ca6fe 100644 --- a/packages/void/src/cli/github-args.ts +++ b/packages/void/src/cli/github-args.ts @@ -18,6 +18,11 @@ export const githubSubcommands: Array = [ arg: '[project]', description: "Show a project's GitHub connection (repo, branch, executor, workflow)", }, + { + name: 'disconnect', + arg: '[project]', + description: 'Disconnect a project from its GitHub repository', + }, { name: 'link', description: 'Link your GitHub account to Void' }, { name: 'join', description: "Join your organization's shared GitHub App installations" }, ]; @@ -46,6 +51,11 @@ export type GithubArgs = | { subcommand: 'status'; projectName: string; + } + | { + subcommand: 'disconnect'; + projectName: string; + yes?: boolean; }; export function parseGithubArgs(args: Array): GithubArgs { @@ -256,7 +266,39 @@ export function parseGithubArgs(args: Array): GithubArgs { return { subcommand, projectName }; } + if (subcommand === 'disconnect') { + let projectName = ''; + let yes = false; + + for (let i = 0; i < rest.length; i++) { + const arg = rest[i]; + if (arg === '--yes' || arg === '-y') { + yes = true; + } else if (arg === '--project') { + if (i + 1 >= rest.length) { + throw new Error('github: --project requires a value.'); + } + const value = rest[++i]!; + if (value.trim() === '') { + throw new Error('github: --project requires a value.'); + } + if (projectName) { + throw new Error(`github: Unexpected argument '${value}'.`); + } + projectName = value; + } else if (arg.startsWith('-')) { + throw new Error(`github: Unexpected argument '${arg}'.`); + } else if (projectName) { + throw new Error(`github: Unexpected argument '${arg}'.`); + } else { + projectName = arg; + } + } + + return { subcommand, projectName, yes }; + } + throw new Error( - 'github: Usage: void github .', + 'github: Usage: void github .', ); } diff --git a/packages/void/test/unit/github-args.test.ts b/packages/void/test/unit/github-args.test.ts index c24da1f44..621a4bc2c 100644 --- a/packages/void/test/unit/github-args.test.ts +++ b/packages/void/test/unit/github-args.test.ts @@ -516,6 +516,63 @@ describe('parseGithubArgs', () => { }); }); + describe('disconnect', () => { + it('parses with no args (defaults)', () => { + expect(parseGithubArgs(['disconnect'])).toEqual({ + subcommand: 'disconnect', + projectName: '', + yes: false, + }); + }); + + it('parses a positional project', () => { + expect(parseGithubArgs(['disconnect', 'my-app'])).toEqual({ + subcommand: 'disconnect', + projectName: 'my-app', + yes: false, + }); + }); + + it('parses --project', () => { + expect(parseGithubArgs(['disconnect', '--project', 'my-app'])).toEqual({ + subcommand: 'disconnect', + projectName: 'my-app', + yes: false, + }); + }); + + it('parses --yes and -y', () => { + expect(parseGithubArgs(['disconnect', 'my-app', '--yes'])).toEqual({ + subcommand: 'disconnect', + projectName: 'my-app', + yes: true, + }); + expect(parseGithubArgs(['disconnect', '-y', 'my-app'])).toEqual({ + subcommand: 'disconnect', + projectName: 'my-app', + yes: true, + }); + }); + + it('throws on a second positional', () => { + expect(() => parseGithubArgs(['disconnect', 'a', 'b'])).toThrow( + "github: Unexpected argument 'b'.", + ); + }); + + it('throws on an unknown flag', () => { + expect(() => parseGithubArgs(['disconnect', '--nope'])).toThrow( + "github: Unexpected argument '--nope'.", + ); + }); + + it('throws when --project has no value', () => { + expect(() => parseGithubArgs(['disconnect', '--project'])).toThrow( + 'github: --project requires a value.', + ); + }); + }); + describe('link', () => { it('parses link with no args', () => { expect(parseGithubArgs(['link'])).toEqual({ subcommand: 'link' }); @@ -553,13 +610,13 @@ describe('parseGithubArgs', () => { describe('unknown subcommand', () => { it('throws on empty args', () => { expect(() => parseGithubArgs([])).toThrow( - 'github: Usage: void github .', + 'github: Usage: void github .', ); }); it('throws on unknown subcommand', () => { expect(() => parseGithubArgs(['unknown'])).toThrow( - 'github: Usage: void github .', + 'github: Usage: void github .', ); }); }); From 5aef8e247f181e265ad3e65d957d8ab7b1a29b74 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 8 Jul 2026 17:47:29 +0800 Subject: [PATCH 2/4] feat(cli): add PlatformClient.deleteGithubConnection Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/void/src/cli/client.ts | 15 +++++++++++ packages/void/test/unit/github-client.test.ts | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/packages/void/src/cli/client.ts b/packages/void/src/cli/client.ts index 8eea4908c..70fa1c3a9 100644 --- a/packages/void/src/cli/client.ts +++ b/packages/void/src/cli/client.ts @@ -1068,6 +1068,21 @@ export class PlatformClient { return res.json(); } + async deleteGithubConnection(projectId: string): Promise<{ ok: boolean }> { + const res = await fetch(`${this.baseUrl}/projects/${projectId}/github`, { + method: 'DELETE', + headers: { + ...this.authHeaders, + }, + }); + + if (!res.ok) { + await throwPlatformApiError('Failed to disconnect GitHub repository', res); + } + + return res.json(); + } + async getGithubLinkUrl(redirect: string, codeChallenge: string): Promise<{ url: string }> { const params = new URLSearchParams({ redirect, diff --git a/packages/void/test/unit/github-client.test.ts b/packages/void/test/unit/github-client.test.ts index 0e6f1553f..44cdbbf09 100644 --- a/packages/void/test/unit/github-client.test.ts +++ b/packages/void/test/unit/github-client.test.ts @@ -373,6 +373,33 @@ describe('PlatformClient.updateGithubConnection', () => { }); }); +describe('PlatformClient.deleteGithubConnection', () => { + it('DELETEs the right URL with auth headers and no body, returns parsed { ok }', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); + + const client = makeClient(); + const result = await client.deleteGithubConnection(projectId); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(`${apiUrl}/projects/${projectId}/github`); + expect(init.method).toBe('DELETE'); + expect(init.body).toBeUndefined(); + const headers = init.headers as Record; + expect(headers.Authorization).toBe(`Bearer ${token}`); + expect(result).toEqual({ ok: true }); + }); + + it('throws PlatformApiError on non-2xx', async () => { + fetchMock.mockResolvedValueOnce(new Response('forbidden', { status: 403 })); + const client = makeClient(); + await expect(client.deleteGithubConnection(projectId)).rejects.toMatchObject({ + name: 'PlatformApiError', + status: 403, + }); + }); +}); + describe('PlatformClient.getGithubLinkUrl', () => { it('GETs link-url with redirect + S256 challenge query, auth headers, returns { url }', async () => { fetchMock.mockResolvedValueOnce( From e610644f5ee97d171debb9eb54ef5bc53a2ce8d6 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 8 Jul 2026 17:54:20 +0800 Subject: [PATCH 3/4] feat(cli): implement `void github disconnect` Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/void/src/cli/github-cmd.ts | 138 +++++++++++- .../test/unit/github-disconnect-cmd.test.ts | 206 ++++++++++++++++++ 2 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 packages/void/test/unit/github-disconnect-cmd.test.ts diff --git a/packages/void/src/cli/github-cmd.ts b/packages/void/src/cli/github-cmd.ts index 694cc0281..7e3d5aa5f 100644 --- a/packages/void/src/cli/github-cmd.ts +++ b/packages/void/src/cli/github-cmd.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'node:crypto'; -import { cancel, isCancel, log, select, text } from '@clack/prompts'; +import { cancel, confirm, isCancel, log, select, text } from '@clack/prompts'; import { awaitBrowserCallback, createCodeChallenge, @@ -175,6 +175,8 @@ export async function runGithubCommand(root: string, args: GithubArgs): Promise< return runGithubUpdate(root, client, args); case 'status': return runStatus(root, client, args); + case 'disconnect': + return runGithubDisconnect(root, client, args); case 'link': return runGithubLink(root, client); case 'join': @@ -1080,3 +1082,137 @@ export async function runStatus( log.message(formatConnection(connection)); } + +// --------------------------------------------------------------------------- +// disconnect +// --------------------------------------------------------------------------- + +/** + * `void github disconnect [project]` — remove a project's GitHub connection so + * pushes no longer auto-deploy. The server (DELETE /projects/:id/github) + * terminalizes any in-flight builds (revoking their deploy tokens) before + * dropping the connection row. Destructive, so a TTY run confirms first; --yes + * skips the prompt and is REQUIRED in a non-interactive shell. A project with no + * connection is already in the goal state — report it and exit 0. + */ +export async function runGithubDisconnect( + root: string, + client: PlatformClient, + args: Extract, +): Promise { + const isTTY = process.stdin.isTTY === true; + + // ------------------------------------------------------------------------- + // 1. Resolve project (identical resolution order to connect/update/status). + // ------------------------------------------------------------------------- + let projectId: string; + + const requestedSlug = getRequestedProjectSlug({ projectSlug: args.projectName || undefined }); + + if (requestedSlug) { + const projects = await client.listProjects(); + const resolved = resolveProjectBySlug(projects, requestedSlug); + if (!resolved) { + log.error(`disconnect: No project found with slug '${requestedSlug}'.`); + process.exit(1); + } + projectId = resolved.projectId; + } else { + const linked = readProjectConfig(root); + if (linked) { + projectId = linked.projectId; + } else if (!isTTY) { + log.error( + 'disconnect: No project specified. Pass a project name or --project , set VOID_PROJECT, or link one with `void project link`.', + ); + process.exit(1); + } else { + const projects = await client.listProjects(); + if (projects.length === 0) { + log.error('No projects found. Create one first with `void deploy`.'); + process.exit(1); + } + const picked = await select({ + message: 'Select a project:', + options: projects.map((p) => ({ value: p.id, label: p.slug })), + }); + if (isCancel(picked)) { + cancel('Cancelled.'); + process.exit(0); + } + projectId = picked as string; + } + } + + // ------------------------------------------------------------------------- + // 2. Fetch the current connection. A 404 "connection not found" means the + // project already has no connection — disconnect's goal state — so report + // success and stop. Other errors (403 forbidden, 404 project not found) + // map through the shared connect error strings and exit 1. + // ------------------------------------------------------------------------- + let connection: GithubConnection; + try { + connection = await client.getGithubConnection(projectId); + } catch (err) { + if (err instanceof PlatformApiError) { + const body = parsePlatformErrorBody(err.body); + const code = body?.error ?? body?.message ?? ''; + if (err.status === 404 && code.includes('connection not found')) { + log.info('This project is not connected to a GitHub repository. Nothing to disconnect.'); + return; + } + const message = describeConnectError(err.status, code); + if (message !== null) { + log.error(`disconnect: ${message}`); + process.exit(1); + } + } + throw err; + } + + // ------------------------------------------------------------------------- + // 3. Confirm — this cancels in-flight builds and drops the connection. --yes + // skips the prompt; a non-interactive shell has nothing to prompt, so it + // must pass --yes (fail closed, same posture as connect/update). + // ------------------------------------------------------------------------- + if (!args.yes) { + if (!isTTY) { + log.error( + 'disconnect: Refusing to disconnect without confirmation in a non-interactive shell. Re-run with --yes.', + ); + process.exit(1); + } + const ok = await confirm({ + message: `Disconnect ${connection.repo_full_name} from this project? This cancels any in-flight builds.`, + initialValue: false, + }); + if (isCancel(ok)) { + cancel('Cancelled.'); + process.exit(0); + } + if (!ok) { + log.info('Disconnect cancelled.'); + return; + } + } + + // ------------------------------------------------------------------------- + // 4. Disconnect. + // ------------------------------------------------------------------------- + try { + await client.deleteGithubConnection(projectId); + } catch (err) { + if (err instanceof PlatformApiError) { + const body = parsePlatformErrorBody(err.body); + const code = body?.error ?? body?.message ?? ''; + const message = describeConnectError(err.status, code); + if (message !== null) { + log.error(`disconnect: ${message}`); + process.exit(1); + } + } + throw err; + } + + log.success(`Disconnected ${connection.repo_full_name}.`); +} diff --git a/packages/void/test/unit/github-disconnect-cmd.test.ts b/packages/void/test/unit/github-disconnect-cmd.test.ts new file mode 100644 index 000000000..a252f9574 --- /dev/null +++ b/packages/void/test/unit/github-disconnect-cmd.test.ts @@ -0,0 +1,206 @@ +import { confirm, log } from '@clack/prompts'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'; +import { + PlatformApiError, + type GithubConnection, + type PlatformClient, +} from '../../src/cli/client.ts'; +import { runGithubDisconnect } from '../../src/cli/github-cmd.ts'; + +// `vi.spyOn(clack, 'confirm')` cannot intercept the handler's named `confirm` +// import under this project's ESM Vitest config ("Module namespace is not +// configurable in ESM"). Mock the module instead, keeping every other export +// real (log/cancel/isCancel/select/text) so only `confirm` is replaced with a +// drivable spy. `vi.mock` is hoisted above the imports above. +vi.mock('@clack/prompts', async (imp) => ({ + ...(await imp()), + confirm: vi.fn(), +})); + +const projectId = 'proj_1'; +const slug = 'my-app'; + +function makeProject() { + return { id: projectId, slug } as unknown as Awaited< + ReturnType + >[number]; +} + +function makeConnection(overrides: Partial = {}): GithubConnection { + return { + id: 'conn_1', + project_id: projectId, + installation_id: '123456', + repo_full_name: 'octocat/hello-world', + repository_id: '9876', + branch: 'main', + build_executor: 'container', + workflow_path: '.github/workflows/void-deploy.yml', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-02T00:00:00Z', + ...overrides, + }; +} + +type FakeClient = Pick< + PlatformClient, + 'listProjects' | 'getGithubConnection' | 'deleteGithubConnection' +>; + +function makeClient(over: Partial = {}): { + client: PlatformClient; + getGithubConnection: ReturnType; + deleteGithubConnection: ReturnType; +} { + const listProjects = vi.fn(async () => [makeProject()]); + const getGithubConnection = vi.fn(async () => makeConnection()); + const deleteGithubConnection = vi.fn(async () => ({ ok: true })); + const client = { + listProjects, + getGithubConnection, + deleteGithubConnection, + ...over, + } as unknown as PlatformClient; + return { + client, + getGithubConnection: + (over.getGithubConnection as ReturnType) ?? getGithubConnection, + deleteGithubConnection: + (over.deleteGithubConnection as ReturnType) ?? deleteGithubConnection, + }; +} + +let exitSpy: ReturnType; +let successSpy: ReturnType; +let infoSpy: ReturnType; +let errorSpy: ReturnType; +let originalIsTTY: boolean | undefined; + +beforeEach(() => { + // The mocked `confirm` is a persistent vi.fn() from the module factory; + // vi.restoreAllMocks() does not reset its call history, so clear it per-test + // to keep the `toHaveBeenCalledTimes(1)` assertion honest. + vi.mocked(confirm).mockReset(); + originalIsTTY = process.stdin.isTTY; + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); + exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code ?? 0})`); + }) as never); + successSpy = vi.spyOn(log, 'success').mockImplementation(() => {}); + infoSpy = vi.spyOn(log, 'info').mockImplementation(() => {}); + errorSpy = vi.spyOn(log, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true }); + vi.restoreAllMocks(); +}); + +describe('runGithubDisconnect', () => { + it('disconnects with --yes in a non-interactive shell', async () => { + const { client, deleteGithubConnection } = makeClient(); + + await runGithubDisconnect('/project', client, { + subcommand: 'disconnect', + projectName: slug, + yes: true, + }); + + expect(deleteGithubConnection).toHaveBeenCalledWith(projectId); + expect(successSpy).toHaveBeenCalledWith('Disconnected octocat/hello-world.'); + expect(exitSpy).not.toHaveBeenCalled(); + }); + + it('refuses without --yes in a non-interactive shell (exit 1, no delete)', async () => { + const { client, deleteGithubConnection } = makeClient(); + + await expect( + runGithubDisconnect('/project', client, { subcommand: 'disconnect', projectName: slug }), + ).rejects.toThrow('process.exit(1)'); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(deleteGithubConnection).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + 'disconnect: Refusing to disconnect without confirmation in a non-interactive shell. Re-run with --yes.', + ); + }); + + it('reports success and does not delete when the project is not connected', async () => { + const notConnected = new PlatformApiError( + 'Failed to get GitHub connection', + 404, + JSON.stringify({ error: 'connection not found' }), + ); + const { client, deleteGithubConnection } = makeClient({ + getGithubConnection: vi.fn(async () => { + throw notConnected; + }), + }); + + await runGithubDisconnect('/project', client, { + subcommand: 'disconnect', + projectName: slug, + yes: true, + }); + + expect(deleteGithubConnection).not.toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledWith( + 'This project is not connected to a GitHub repository. Nothing to disconnect.', + ); + expect(exitSpy).not.toHaveBeenCalled(); + }); + + it('maps a 403 on read to a no-access message (exit 1)', async () => { + const forbidden = new PlatformApiError( + 'Failed to get GitHub connection', + 403, + JSON.stringify({ error: 'forbidden' }), + ); + const { client, deleteGithubConnection } = makeClient({ + getGithubConnection: vi.fn(async () => { + throw forbidden; + }), + }); + + await expect( + runGithubDisconnect('/project', client, { + subcommand: 'disconnect', + projectName: slug, + yes: true, + }), + ).rejects.toThrow('process.exit(1)'); + + expect(errorSpy).toHaveBeenCalledWith("disconnect: You don't have access to this project."); + expect(deleteGithubConnection).not.toHaveBeenCalled(); + }); + + it('prompts to confirm in a TTY and aborts when declined', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + vi.mocked(confirm).mockResolvedValue(false as never); + const { client, deleteGithubConnection } = makeClient(); + + await runGithubDisconnect('/project', client, { + subcommand: 'disconnect', + projectName: slug, + }); + + expect(confirm).toHaveBeenCalledTimes(1); + expect(deleteGithubConnection).not.toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledWith('Disconnect cancelled.'); + }); + + it('prompts to confirm in a TTY and disconnects when accepted', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + vi.mocked(confirm).mockResolvedValue(true as never); + const { client, deleteGithubConnection } = makeClient(); + + await runGithubDisconnect('/project', client, { + subcommand: 'disconnect', + projectName: slug, + }); + + expect(confirm).toHaveBeenCalledTimes(1); + expect(deleteGithubConnection).toHaveBeenCalledWith(projectId); + expect(successSpy).toHaveBeenCalledWith('Disconnected octocat/hello-world.'); + }); +}); From 9335d0d8443941dea42a667c18ca0c975fce64ba Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 8 Jul 2026 18:02:03 +0800 Subject: [PATCH 4/4] docs(cli): document `void github disconnect` help + reference Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/reference/cli.md | 23 ++++++++++++++++++++++ packages/void/src/cli/cli.ts | 2 +- packages/void/src/cli/help.ts | 25 ++++++++++++++++++++++++ packages/void/test/unit/cli-help.test.ts | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e3f7600d8..ba36ffe34 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -692,6 +692,29 @@ void github status my-app **Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`). +### `void github disconnect` + +``` +void github disconnect [project] +``` + +Disconnect a project from its GitHub repository, stopping automatic deploys. Any in-flight builds for the project are cancelled (their deploy tokens are revoked) before the connection is removed. If the project has no connection, it reports that and exits successfully. To point a project at a different repository, disconnect first, then run `void github connect`. + +You are asked to confirm before anything is removed. Pass `--yes` to skip the prompt; `--yes` is **required** in a non-interactive shell (CI), where there is no prompt to answer. + +**Options** + +| Flag | Description | +| ------------------ | ----------------------------------------------------------------- | +| `--project ` | Project name (alias for the positional argument) | +| `--yes` | Skip the confirmation prompt (required in non-interactive shells) | + +``` +void github disconnect my-app --yes +``` + +**Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`). + ## Build Inspect Deploy-on-GitHub builds. diff --git a/packages/void/src/cli/cli.ts b/packages/void/src/cli/cli.ts index 3356fe56a..5583b43a3 100644 --- a/packages/void/src/cli/cli.ts +++ b/packages/void/src/cli/cli.ts @@ -206,7 +206,7 @@ if (helpPath) { }, { name: 'github', - hint: 'install | installations | connect | update | status | link | join', + hint: 'install | installations | connect | update | status | disconnect | link | join', description: 'Connect a GitHub repository for automatic deploys', }, { diff --git a/packages/void/src/cli/help.ts b/packages/void/src/cli/help.ts index 53a4218d1..233806a72 100644 --- a/packages/void/src/cli/help.ts +++ b/packages/void/src/cli/help.ts @@ -208,6 +208,10 @@ const HELP_TREE: HelpTree = { command: 'status [project]', description: "Show a project's GitHub connection (repo, branch, executor, workflow)", }, + { + command: 'disconnect [project]', + description: 'Disconnect a project from its GitHub repository', + }, { command: 'link', description: 'Link your GitHub account to Void' }, { command: 'join', @@ -311,6 +315,27 @@ const HELP_TREE: HelpTree = { }, ], }, + disconnect: { + summary: 'Disconnect a project from its GitHub repository.', + usage: ['void github disconnect [project] [options]'], + options: [ + { command: '--project ', description: 'Project name (alias for positional)' }, + { + command: '--yes', + description: 'Skip the confirmation prompt (required in non-interactive shells)', + }, + ], + examples: [ + { + command: 'void github disconnect', + description: "Disconnect the linked project's repository", + }, + { + command: 'void github disconnect my-app --yes', + description: 'Disconnect a specific project without confirmation', + }, + ], + }, }, }, build: { diff --git a/packages/void/test/unit/cli-help.test.ts b/packages/void/test/unit/cli-help.test.ts index f9fba8d32..f67d68906 100644 --- a/packages/void/test/unit/cli-help.test.ts +++ b/packages/void/test/unit/cli-help.test.ts @@ -66,6 +66,7 @@ describe('CLI help resolver', () => { expect(hasHelpPage(['github', 'installations'])).toBe(true); expect(hasHelpPage(['github', 'connect'])).toBe(true); expect(hasHelpPage(['github', 'status'])).toBe(true); + expect(hasHelpPage(['github', 'disconnect'])).toBe(true); expect(hasHelpPage(['github', 'missing'])).toBe(false); }); });