fix(cli): make auth devices revoke --yes a real flag (#37740)

This commit is contained in:
Xiyuan Chen 2026-06-22 00:42:13 -07:00 committed by GitHub
parent 7aa20d6d94
commit 1d74bff311
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 14 deletions

View File

@ -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()

View File

@ -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<void
return
}
if (opts.yes !== true && opts.io.isErrTTY) {
const confirmed = await promptConfirm(opts.io, `Revoke ${ids.length} session(s)? [y/N] `)
if (!confirmed) {
throw new BaseError({
code: ErrorCode.UsageMissingArg,
message: 'aborted by user',
hint: 'pass --yes to skip confirmation',
})
}
}
for (const id of ids)
await sessions.revoke(id)

View File

@ -21,7 +21,7 @@ export default class DevicesRevoke extends DifyCommand {
static override flags = {
'all': Flags.boolean({ description: 'revoke every session except the current one', default: false }),
'http-retry': httpRetryFlag,
'yes': Flags.boolean({ description: 'skip confirmation prompt', default: false }),
'yes': Flags.boolean({ char: 'y', description: 'skip confirmation prompt', default: false }),
}
async run(argv: string[]): Promise<void> {

View File

@ -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<boolean> {
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()
}
}

View File

@ -33,6 +33,18 @@ function normalize(raw: string, opts: Pick<PromptTextOptions<unknown>, 'default'
return trimmed
}
export async function promptConfirm(io: IOStreams, message: string): Promise<boolean> {
io.err.write(message)
const rl = readline.createInterface({ input: io.in, output: io.err, terminal: false })
try {
const line = await new Promise<string>(resolve => rl.once('line', resolve))
return line.trim().toLowerCase() === 'y'
}
finally {
rl.close()
}
}
export async function promptText<T>(opts: PromptTextOptions<T>): Promise<T> {
const prompt = buildPromptLine(opts)
const cs = colorScheme(colorEnabled(opts.io.isErrTTY))