From 1d74bff311184abec840677d9d84f05ec4276ec3 Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:42:13 -0700 Subject: [PATCH] fix(cli): make auth devices revoke --yes a real flag (#37740) --- .../auth/devices/_shared/devices.test.ts | 37 +++++++++++++++++++ .../commands/auth/devices/_shared/devices.ts | 12 ++++++ cli/src/commands/auth/devices/revoke/index.ts | 2 +- cli/src/commands/delete/member/run.ts | 14 +------ cli/src/sys/io/prompt.ts | 12 ++++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cli/src/commands/auth/devices/_shared/devices.test.ts b/cli/src/commands/auth/devices/_shared/devices.test.ts index fb510ef1af1..0b64ac8b14e 100644 --- a/cli/src/commands/auth/devices/_shared/devices.test.ts +++ b/cli/src/commands/auth/devices/_shared/devices.test.ts @@ -146,6 +146,43 @@ describe('runDevicesRevoke', () => { expect(saved?.hosts[mock.url]).toBeUndefined() }) + it('TTY without --yes: prompts and aborts on decline (no revoke)', async () => { + const base = bufferStreams('n\n') + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await expect(runDevicesRevoke({ io, reg, active, store, http, all: true })) + .rejects + .toThrow(/aborted by user/) + expect(base.errBuf()).toContain('Revoke 2 session(s)? [y/N]') + expect(base.outBuf()).not.toContain('Revoked') + }) + + it('TTY without --yes: proceeds on accept', async () => { + const base = bufferStreams('y\n') + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await runDevicesRevoke({ io, reg, active, store, http, target: 'tok-2', all: false }) + expect(base.outBuf()).toContain('Revoked 1 session(s)') + }) + + it('TTY with --yes: skips prompt entirely', async () => { + const base = bufferStreams() + const io = { ...base, isErrTTY: true } + const store = new MemStore() + const { reg, active } = buildRegistry(mock.url, 'tester@dify.ai', 'tok-1') + const http = testHttpClient(mock.url, 'dfoa_test') + + await runDevicesRevoke({ io, reg, active, store, http, target: 'tok-2', all: false, yes: true }) + expect(base.errBuf()).not.toContain('[y/N]') + expect(base.outBuf()).toContain('Revoked 1 session(s)') + }) + it('no target + no --all: throws UsageMissingArg', async () => { const io = bufferStreams() const store = new MemStore() diff --git a/cli/src/commands/auth/devices/_shared/devices.ts b/cli/src/commands/auth/devices/_shared/devices.ts index 15f17e0a3e3..b328e59f93f 100644 --- a/cli/src/commands/auth/devices/_shared/devices.ts +++ b/cli/src/commands/auth/devices/_shared/devices.ts @@ -8,6 +8,7 @@ import { BaseError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' import { LIMIT_DEFAULT, LIMIT_MAX, parseLimit } from '@/limit/limit' import { colorEnabled, colorScheme } from '@/sys/io/color' +import { promptConfirm } from '@/sys/io/prompt' import { runWithSpinner } from '@/sys/io/spinner' export type DevicesListOptions = { @@ -96,6 +97,17 @@ export async function runDevicesRevoke(opts: DevicesRevokeOptions): Promise { diff --git a/cli/src/commands/delete/member/run.ts b/cli/src/commands/delete/member/run.ts index 07ec3ad7720..f11e3ee4b93 100644 --- a/cli/src/commands/delete/member/run.ts +++ b/cli/src/commands/delete/member/run.ts @@ -1,11 +1,11 @@ import type { ActiveContext } from '@/auth/hosts' import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' -import * as readline from 'node:readline' import { MembersClient } from '@/api/members' import { BaseError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' import { colorEnabled, colorScheme } from '@/sys/io/color' +import { promptConfirm } from '@/sys/io/prompt' import { runWithSpinner } from '@/sys/io/spinner' import { nullStreams } from '@/sys/io/streams' import { resolveWorkspaceId } from '@/workspace/resolver' @@ -76,15 +76,3 @@ export async function runDeleteMember( workspaceId: wsId, } } - -async function promptConfirm(io: IOStreams, message: string): Promise { - io.err.write(message) - const rl = readline.createInterface({ input: io.in, output: io.err, terminal: false }) - try { - const line: string = await new Promise(resolve => rl.once('line', resolve)) - return line.trim().toLowerCase() === 'y' - } - finally { - rl.close() - } -} diff --git a/cli/src/sys/io/prompt.ts b/cli/src/sys/io/prompt.ts index d5cc2498b96..15b3d95eb5e 100644 --- a/cli/src/sys/io/prompt.ts +++ b/cli/src/sys/io/prompt.ts @@ -33,6 +33,18 @@ function normalize(raw: string, opts: Pick, 'default' return trimmed } +export async function promptConfirm(io: IOStreams, message: string): Promise { + io.err.write(message) + const rl = readline.createInterface({ input: io.in, output: io.err, terminal: false }) + try { + const line = await new Promise(resolve => rl.once('line', resolve)) + return line.trim().toLowerCase() === 'y' + } + finally { + rl.close() + } +} + export async function promptText(opts: PromptTextOptions): Promise { const prompt = buildPromptLine(opts) const cs = colorScheme(colorEnabled(opts.io.isErrTTY))