mirror of
https://github.com/langgenius/dify.git
synced 2026-11-03 14:19:00 +08:00
refactor: improve network error and allow verbose output (#36923)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { CommandOutput } from './output'
|
||||
import type { ArgDefinition, FlagDefinition, ICommand, InferArgs, InferFlags, OptionalArgValueType } from './types'
|
||||
import { parseArgv } from './flags'
|
||||
import { setVerbose } from './context'
|
||||
import { hasBooleanFlag, parseArgv, VERBOSE_CHAR, VERBOSE_FLAG } from './flags'
|
||||
|
||||
export type CommandConstructor = {
|
||||
new(): Command
|
||||
@@ -28,11 +29,16 @@ type ParseResult<C extends CommandConstructor> = {
|
||||
export abstract class Command implements ICommand {
|
||||
static description?: string
|
||||
static flags: Record<string, FlagDefinition<OptionalArgValueType>> = {}
|
||||
|
||||
static args: Record<string, ArgDefinition<string | undefined>> = {}
|
||||
static examples: string[] = []
|
||||
|
||||
abstract run(argv: string[]): Promise<CommandOutput | void>
|
||||
|
||||
processGlobalFlags(argv: readonly string[]): void {
|
||||
setVerbose(hasBooleanFlag(argv, VERBOSE_FLAG, VERBOSE_CHAR))
|
||||
}
|
||||
|
||||
protected parse<C extends CommandConstructor>(ctor: C, argv: string[]): ParseResult<C> {
|
||||
const meta = {
|
||||
flags: ctor.flags ?? {},
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
type CommandContext = {
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
const commandContext: CommandContext = {
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
export function setVerbose(verbose: boolean): void {
|
||||
commandContext.verbose = verbose
|
||||
}
|
||||
|
||||
export function isVerbose(): boolean {
|
||||
return commandContext.verbose
|
||||
}
|
||||
+42
-14
@@ -1,6 +1,24 @@
|
||||
import type { ArgDefinition, CommandMeta, FlagDefinition, ParsedArgs, ParsedFlags } from './types'
|
||||
import { UnsupportedArgValueError } from './errors'
|
||||
|
||||
export const VERBOSE_FLAG = 'verbose'
|
||||
export const VERBOSE_CHAR = 'v'
|
||||
|
||||
export const Flags = {
|
||||
string: stringFlag,
|
||||
stringArray: stringRepeatedFlag,
|
||||
boolean: booleanFlag,
|
||||
integer: integerFlag,
|
||||
outputFormat: outputFormatFlag,
|
||||
}
|
||||
|
||||
const GLOBAL_FLAGS: Record<string, FlagDefinition> = {
|
||||
[VERBOSE_FLAG]: Flags.boolean({
|
||||
char: VERBOSE_CHAR,
|
||||
description: 'enable verbose output',
|
||||
}),
|
||||
}
|
||||
|
||||
function stringFlag<const Opts extends {
|
||||
description: string
|
||||
char?: string
|
||||
@@ -48,14 +66,6 @@ function integerFlag<const Opts extends { description: string, char?: string, de
|
||||
return { type: 'integer', ...opts } as FlagDefinition<Opts extends { default: number } ? number : number | undefined>
|
||||
}
|
||||
|
||||
export const Flags = {
|
||||
string: stringFlag,
|
||||
stringArray: stringRepeatedFlag,
|
||||
boolean: booleanFlag,
|
||||
integer: integerFlag,
|
||||
outputFormat: outputFormatFlag,
|
||||
}
|
||||
|
||||
function stringArg<const Opts extends { description: string, required?: boolean }>(
|
||||
opts: Opts,
|
||||
): ArgDefinition<Opts extends { required: true } ? string : string | undefined> {
|
||||
@@ -99,8 +109,8 @@ function accumulateFlagValue(flags: ParsedFlags, name: string, value: string | b
|
||||
}
|
||||
}
|
||||
|
||||
function resolveByChar(char: string, meta: CommandMeta): [name: string, def: FlagDefinition] | undefined {
|
||||
for (const [name, def] of Object.entries(meta.flags)) {
|
||||
function resolveByChar(char: string, flags: Record<string, FlagDefinition>): [name: string, def: FlagDefinition] | undefined {
|
||||
for (const [name, def] of Object.entries(flags)) {
|
||||
if (def.char === char)
|
||||
return [name, def]
|
||||
}
|
||||
@@ -115,12 +125,12 @@ function validateFlagOptions(name: string, raw: string, def: FlagDefinition): vo
|
||||
|
||||
type ResolvedFlag = { name: string, def: FlagDefinition, label: string, inlineRaw: string | undefined }
|
||||
|
||||
function resolveToken(token: string, meta: CommandMeta): ResolvedFlag | null {
|
||||
function resolveToken(token: string, flags: Record<string, FlagDefinition>): ResolvedFlag | null {
|
||||
if (token.startsWith('--')) {
|
||||
const eqIdx = token.indexOf('=')
|
||||
const name = eqIdx !== -1 ? token.slice(2, eqIdx) : token.slice(2)
|
||||
const inlineRaw = eqIdx !== -1 ? token.slice(eqIdx + 1) : undefined
|
||||
const def = meta.flags[name]
|
||||
const def = flags[name]
|
||||
if (!def)
|
||||
throw new Error(`unknown flag: --${name}`)
|
||||
return { name, def, label: `--${name}`, inlineRaw }
|
||||
@@ -128,7 +138,7 @@ function resolveToken(token: string, meta: CommandMeta): ResolvedFlag | null {
|
||||
|
||||
if (token.length === 2 && token[1] !== undefined) {
|
||||
const char = token[1]
|
||||
const resolved = resolveByChar(char, meta)
|
||||
const resolved = resolveByChar(char, flags)
|
||||
if (!resolved)
|
||||
throw new Error(`unknown flag: -${char}`)
|
||||
const [name, def] = resolved
|
||||
@@ -138,6 +148,21 @@ function resolveToken(token: string, meta: CommandMeta): ResolvedFlag | null {
|
||||
return null
|
||||
}
|
||||
|
||||
// Scans argv for a boolean flag without throwing on unknown tokens, so it is safe
|
||||
// to call before the command-specific flag set is known (e.g. global flags).
|
||||
export function hasBooleanFlag(argv: readonly string[], name: string, char?: string): boolean {
|
||||
for (const token of argv) {
|
||||
if (token === '--')
|
||||
break
|
||||
if (token === `--${name}` || token === `--${name}=true` || token === `--${name}=1`)
|
||||
return true
|
||||
if (char !== undefined && token === `-${char}`)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function parseArgv(argv: readonly string[], meta: CommandMeta): { args: ParsedArgs, flags: ParsedFlags } {
|
||||
const flags: ParsedFlags = {}
|
||||
const positional: string[] = []
|
||||
@@ -159,7 +184,10 @@ export function parseArgv(argv: readonly string[], meta: CommandMeta): { args: P
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = resolveToken(token, meta)
|
||||
const resolved = resolveToken(token, {
|
||||
...meta.flags,
|
||||
...GLOBAL_FLAGS, // pass global flags to prevent unknown flag error
|
||||
})
|
||||
if (!resolved) {
|
||||
positional.push(token)
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CommandConstructor } from './command'
|
||||
import type { CommandTree } from './registry'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BaseError, newError } from '@/errors/base'
|
||||
import { BaseError, HttpClientError, newError } from '@/errors/base'
|
||||
import { ErrorCode, ExitCode } from '@/errors/codes'
|
||||
import { Command } from './command'
|
||||
import { run, sniffOutputFormat } from './run'
|
||||
@@ -171,7 +171,7 @@ describe('run() catch routing', () => {
|
||||
it('routes Server5xx error with http_status line and generic exit', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw newError(ErrorCode.Server5xx, 'upstream boom').withHttpStatus(502)
|
||||
throw HttpClientError.from(newError(ErrorCode.Server5xx, 'upstream boom')).withHttpStatus(502)
|
||||
}
|
||||
}
|
||||
const result = await captureRun(makeTree(Throwing), ['cmd'])
|
||||
@@ -182,7 +182,7 @@ describe('run() catch routing', () => {
|
||||
it('renders request line and http_status when both are present', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw newError(ErrorCode.Server5xx, 'upstream boom')
|
||||
throw HttpClientError.from(newError(ErrorCode.Server5xx, 'upstream boom'))
|
||||
.withRequest('GET', 'https://api.dify.ai/v1/me')
|
||||
.withHttpStatus(502)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ describe('run() catch routing', () => {
|
||||
it('serializes method and url in JSON envelope', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw newError(ErrorCode.Server4xxOther, 'not found')
|
||||
throw HttpClientError.from(newError(ErrorCode.Server4xxOther, 'not found'))
|
||||
.withRequest('GET', 'https://api.dify.ai/v1/apps/x')
|
||||
.withHttpStatus(404)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ export async function run(tree: CommandTree, argv: string[]): Promise<void> {
|
||||
if (typeof Ctor.deprecated === 'string' && Ctor.deprecated.length > 0)
|
||||
process.stderr.write(`deprecated: ${Ctor.deprecated}\n`)
|
||||
const cmd = new Ctor()
|
||||
const output = await cmd.run(argv.slice(resolved.path.length))
|
||||
const commandArgv = argv.slice(resolved.path.length)
|
||||
cmd.processGlobalFlags(commandArgv)
|
||||
|
||||
const output = await cmd.run(commandArgv)
|
||||
if (output !== undefined)
|
||||
process.stdout.write(stringifyOutput(output))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user