From 22dd7acdc74cd81351c4073a8a7f024c033bca0c Mon Sep 17 00:00:00 2001 From: yuanhe Date: Wed, 1 Apr 2026 15:16:46 +0800 Subject: [PATCH] refactor: decouple flag parsing from hardcoded lists, extract status-bar - args.ts: replace hardcoded boolean/number/array lists with schema-driven parseFlags(argv, OptionDef[]) + scanCommandPath(); adding a new flag no longer requires touching the parser - command.ts: add GLOBAL_OPTIONS as the single source of truth for global flag types; main.ts merges with command options for two-pass parsing - output/status-bar.ts: extract status-bar rendering out of http.ts; HTTP client is now pure transport with no UI concerns - registry.ts: remove commented-out File API dead code - package.json: add build:dev for single-platform Bun standalone binary - commands: annotate numeric (type:'number') and array (type:'array') flags in text/chat, speech/synthesize, music/generate, video/generate, image/generate Co-Authored-By: Claude Sonnet 4.6 --- package.json | 1 + src/args.ts | 117 ++++++++++++++++++------------ src/client/http.ts | 72 ++++-------------- src/command.ts | 18 +++++ src/commands/image/generate.ts | 2 +- src/commands/music/generate.ts | 4 +- src/commands/speech/synthesize.ts | 34 ++++----- src/commands/text/chat.ts | 16 ++-- src/commands/video/generate.ts | 2 +- src/main.ts | 18 +++-- src/output/status-bar.ts | 28 +++++++ src/registry.ts | 16 +--- 12 files changed, 175 insertions(+), 153 deletions(-) create mode 100644 src/output/status-bar.ts diff --git a/package.json b/package.json index 8a1c5c4..6f3f238 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "scripts": { "dev": "bun run src/main.ts", "build": "bun run build.ts", + "build:dev": "bun build src/main.ts --compile --minify --outfile dist/minimax --define \"process.env.CLI_VERSION='$(node -p \"require('./package.json').version\")'\"", "lint": "eslint src/ test/", "typecheck": "tsc --noEmit", "test": "bun test", diff --git a/src/args.ts b/src/args.ts index 6ac3630..a8e3f8d 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,12 +1,65 @@ import type { GlobalFlags } from './types/flags'; +import type { OptionDef } from './command'; -export interface ParsedArgs { - commandPath: string[]; - flags: GlobalFlags; +function kebabToCamel(str: string): string { + return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); +} + +/** Extract camelCase flag name from an OptionDef.flag string, e.g. '--max-tokens ' → 'maxTokens' */ +function flagKey(def: OptionDef): string | null { + const m = def.flag.match(/^--([a-z][a-z0-9-]*)/i); + return m ? kebabToCamel(m[1]!) : null; +} + +/** Boolean when no value placeholder and type is not string/number/array */ +function isBooleanDef(def: OptionDef): boolean { + if (def.type === 'boolean') return true; + if (def.type === 'string' || def.type === 'number' || def.type === 'array') return false; + return !def.flag.includes('<') && !def.flag.includes('['); } -export function parseArgs(argv: string[]): ParsedArgs { - const commandPath: string[] = []; +interface FlagSchema { + booleans: Set; + numbers: Set; + arrays: Set; +} + +function buildSchema(options: OptionDef[]): FlagSchema { + const booleans = new Set(); + const numbers = new Set(); + const arrays = new Set(); + for (const opt of options) { + const key = flagKey(opt); + if (!key) continue; + if (isBooleanDef(opt)) booleans.add(key); + else if (opt.type === 'number') numbers.add(key); + else if (opt.type === 'array') arrays.add(key); + } + return { booleans, numbers, arrays }; +} + +/** + * Quick scan: collect positional (non-dash) args to determine the command path. + * Does not consume flag values — just skips dash-prefixed tokens. + */ +export function scanCommandPath(argv: string[]): string[] { + const path: string[] = []; + for (const arg of argv) { + if (arg === '--') break; + if (!arg.startsWith('-')) path.push(arg); + } + return path; +} + +/** + * Full flag parse. Types are derived entirely from the provided OptionDef schema: + * - boolean: no placeholder in flag string (or type: 'boolean') + * - number: type: 'number' + * - array: type: 'array' (repeatable via multiple --flag occurrences) + * - default: string + */ +export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags { + const schema = buildSchema(options); const flags: GlobalFlags = { quiet: false, verbose: false, @@ -22,77 +75,49 @@ export function parseArgs(argv: string[]): ParsedArgs { while (i < argv.length) { const arg = argv[i]!; - if (arg === '--help' || arg === '-h') { - flags.help = true; - i++; - continue; - } - - if (arg === '--') { - i++; - break; - } + if (arg === '--help' || arg === '-h') { flags.help = true; i++; continue; } + if (arg === '--') { i++; break; } if (arg.startsWith('--')) { - const eqIndex = arg.indexOf('='); + const eqIdx = arg.indexOf('='); let key: string; let value: string | undefined; - if (eqIndex !== -1) { - key = arg.slice(2, eqIndex); - value = arg.slice(eqIndex + 1); + if (eqIdx !== -1) { + key = arg.slice(2, eqIdx); + value = arg.slice(eqIdx + 1); } else { key = arg.slice(2); } const camelKey = kebabToCamel(key); - // Boolean flags - if (['quiet', 'verbose', 'noColor', 'yes', 'dryRun', 'help', 'stream', - 'subtitles', 'wait', 'noWait', 'noBrowser', - 'nonInteractive', 'async'].includes(camelKey)) { + if (schema.booleans.has(camelKey)) { (flags as Record)[camelKey] = true; i++; continue; } - // Value flags if (value === undefined) { i++; value = argv[i]; } - if (value === undefined) { - throw new Error(`Flag --${key} requires a value.`); - } + if (value === undefined) throw new Error(`Flag --${key} requires a value.`); - // Repeatable flags - if (['message', 'tool', 'pronunciation'].includes(camelKey)) { + if (schema.arrays.has(camelKey)) { const arr = (flags as Record)[camelKey] as string[] | undefined; - if (arr) { - arr.push(value); - } else { - (flags as Record)[camelKey] = [value]; - } - } else if (['maxTokens', 'temperature', 'topP', 'speed', 'volume', - 'pitch', 'sampleRate', 'bitrate', 'channels', 'n', - 'timeout', 'pollInterval'].includes(camelKey)) { + if (arr) arr.push(value); + else (flags as Record)[camelKey] = [value]; + } else if (schema.numbers.has(camelKey)) { (flags as Record)[camelKey] = Number(value); } else { (flags as Record)[camelKey] = value; } - i++; - continue; } - // Positional argument — part of command path - commandPath.push(arg); i++; } - return { commandPath, flags }; -} - -function kebabToCamel(str: string): string { - return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); + return flags; } diff --git a/src/client/http.ts b/src/client/http.ts index 4cef543..4376954 100644 --- a/src/client/http.ts +++ b/src/client/http.ts @@ -2,8 +2,7 @@ import type { Config } from '../config/schema'; import type { ApiErrorBody } from '../errors/api'; import { resolveCredential } from '../auth/resolver'; import { mapApiError } from '../errors/api'; -import { CLIError } from '../errors/base'; -import { ExitCode } from '../errors/codes'; +import { maybeShowStatusBar } from '../output/status-bar'; export interface RequestOpts { url: string; @@ -16,15 +15,8 @@ export interface RequestOpts { authStyle?: 'bearer' | 'x-api-key'; } -// Printed once per process invocation to avoid repeating on every request. -let statusBarPrinted = false; - -export async function request( - config: Config, - opts: RequestOpts, -): Promise { - const isFormData = - typeof FormData !== 'undefined' && opts.body instanceof FormData; +export async function request(config: Config, opts: RequestOpts): Promise { + const isFormData = typeof FormData !== 'undefined' && opts.body instanceof FormData; const version = process.env.CLI_VERSION ?? '0.3.1'; const headers: Record = { @@ -32,13 +24,13 @@ export async function request( ...opts.headers, }; - // Only set Content-Type for non-FormData bodies; FormData lets fetch set the multipart boundary automatically if (!isFormData && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } if (!opts.noAuth) { const credential = await resolveCredential(config); + if (opts.authStyle === 'x-api-key') { headers['x-api-key'] = credential.token; } else { @@ -46,51 +38,22 @@ export async function request( } if (config.verbose) { - process.stderr.write(`> ${opts.method || 'GET'} ${opts.url}\n`); + process.stderr.write(`> ${opts.method ?? 'GET'} ${opts.url}\n`); process.stderr.write(`> Auth: ${credential.token.slice(0, 8)}...\n`); } - // ANSI 真彩色 (24-bit) 与基础排版 - if (!config.quiet && !statusBarPrinted && process.stderr.isTTY) { - statusBarPrinted = true; - const reset = '\x1b[0m'; - const dim = '\x1b[2m'; - const bold = '\x1b[1m'; // 新增加粗效果 - - // 从 MiniMax Logo/品牌视觉提取的 RGB 颜色 - const mmBlue = '\x1b[38;2;43;82;255m'; // 主品牌色:MiniMax 科技蓝 (#2B52FF) - const mmPurple = '\x1b[38;2;147;51;234m'; // 辅助品牌色:活力紫 (#9333EA) - const mmCyan = '\x1b[38;2;6;184;212m'; // 点缀色:青色 (#06B8D4) - const mmPink = '\x1b[38;2;236;72;153m'; // 点缀色:粉红 (#EC4899) - - // 提取 Region (根据 baseUrl 推断) - const region = config.baseUrl.includes('minimaxi.com') ? 'CN' : 'Global'; - - // 提取脱敏的 Key - const token = credential.token; - const maskedKey = token.length > 8 ? `${token.slice(0, 4)}...${token.slice(-4)}` : '***'; - - // 尝试从 body 中提取 Model - let modelStr = ''; - if (opts.body && typeof opts.body === 'object' && 'model' in opts.body) { - modelStr = ` ${dim}|${reset} ${dim}Model:${reset} ${mmPurple}${(opts.body as any).model}${reset}`; - } - - // 打印带有完整 MINIMAX 标识的状态栏 - process.stderr.write( - `${bold}${mmBlue}MINIMAX${reset} ` + - `${dim}Region:${reset} ${mmCyan}${region}${reset} ` + - `${dim}|${reset} ` + - `${dim}Key:${reset} ${mmPink}${maskedKey}${reset}` + - `${modelStr}\n` - ); - } + const model = + opts.body && typeof opts.body === 'object' && 'model' in opts.body + ? String((opts.body as Record).model) + : undefined; + + maybeShowStatusBar(config, credential.token, model); } - const timeoutMs = (opts.timeout || config.timeout) * 1000; + const timeoutMs = (opts.timeout ?? config.timeout) * 1000; const res = await fetch(opts.url, { - method: opts.method || 'GET', + method: opts.method ?? 'GET', headers, body: opts.body ? isFormData @@ -106,11 +69,7 @@ export async function request( if (!res.ok) { let body: ApiErrorBody = {}; - try { - body = (await res.json()) as ApiErrorBody; - } catch { - // Response body is not JSON - } + try { body = (await res.json()) as ApiErrorBody; } catch { /* non-JSON */ } throw mapApiError(res.status, body, opts.url); } @@ -121,8 +80,7 @@ export async function requestJson(config: Config, opts: RequestOpts): Promise const res = await request(config, opts); const data = (await res.json()) as T & { base_resp?: { status_code?: number; status_msg?: string } }; - // MiniMax APIs return HTTP 200 with error details in base_resp - if (data.base_resp && data.base_resp.status_code && data.base_resp.status_code !== 0) { + if (data.base_resp?.status_code && data.base_resp.status_code !== 0) { throw mapApiError(200, { base_resp: data.base_resp }, opts.url); } diff --git a/src/command.ts b/src/command.ts index 6e13edf..0ecf643 100644 --- a/src/command.ts +++ b/src/command.ts @@ -36,3 +36,21 @@ export function defineCommand(spec: CommandSpec): Command { execute: spec.run, }; } + +/** Global flags shared by all commands — drives the parser's type resolution. */ +export const GLOBAL_OPTIONS: OptionDef[] = [ + { flag: '--api-key ', description: 'API key' }, + { flag: '--region ', description: 'API region: global, cn' }, + { flag: '--base-url ', description: 'API base URL' }, + { flag: '--output ', description: 'Output format: text, json, yaml' }, + { flag: '--timeout ', description: 'Request timeout', type: 'number' }, + { flag: '--quiet', description: 'Suppress non-essential output' }, + { flag: '--verbose', description: 'Print HTTP request/response details' }, + { flag: '--no-color', description: 'Disable ANSI colors' }, + { flag: '--yes', description: 'Skip confirmation prompts' }, + { flag: '--dry-run', description: 'Dry run mode' }, + { flag: '--non-interactive', description: 'Disable interactive prompts' }, + { flag: '--async', description: 'Return task ID immediately' }, + { flag: '--help', description: 'Show help' }, + { flag: '--version', description: 'Print version' }, +]; diff --git a/src/commands/image/generate.ts b/src/commands/image/generate.ts index 912d312..4fc7295 100644 --- a/src/commands/image/generate.ts +++ b/src/commands/image/generate.ts @@ -20,7 +20,7 @@ export default defineCommand({ options: [ { flag: '--prompt ', description: 'Image description', required: true }, { flag: '--aspect-ratio ', description: 'Aspect ratio (e.g. 16:9, 1:1)' }, - { flag: '--n ', description: 'Number of images to generate (default: 1)' }, + { flag: '--n ', description: 'Number of images to generate (default: 1)', type: 'number' }, { flag: '--subject-ref ', description: 'Subject reference (type=character,image=path)' }, { flag: '--out-dir ', description: 'Download images to directory' }, { flag: '--out-prefix ', description: 'Filename prefix (default: image)' }, diff --git a/src/commands/music/generate.ts b/src/commands/music/generate.ts index 3ab2edf..3bcdbf9 100644 --- a/src/commands/music/generate.ts +++ b/src/commands/music/generate.ts @@ -18,8 +18,8 @@ export default defineCommand({ { flag: '--lyrics ', description: 'Song lyrics' }, { flag: '--lyrics-file ', description: 'Read lyrics from file (use - for stdin)' }, { flag: '--format ', description: 'Audio format (default: mp3)' }, - { flag: '--sample-rate ', description: 'Sample rate (default: 44100)' }, - { flag: '--bitrate ', description: 'Bitrate (default: 256000)' }, + { flag: '--sample-rate ', description: 'Sample rate (default: 44100)', type: 'number' }, + { flag: '--bitrate ', description: 'Bitrate (default: 256000)', type: 'number' }, { flag: '--stream', description: 'Stream raw audio to stdout' }, { flag: '--out ', description: 'Save audio to file (uses hex decoding)' }, ], diff --git a/src/commands/speech/synthesize.ts b/src/commands/speech/synthesize.ts index efcc564..7df869c 100644 --- a/src/commands/speech/synthesize.ts +++ b/src/commands/speech/synthesize.ts @@ -14,23 +14,23 @@ export default defineCommand({ description: 'Synchronous TTS, up to 10k chars (speech-2.8-hd / 2.6 / 02)', usage: 'minimax speech synthesize --text [--out ] [flags]', options: [ - { flag: '--model ', description: 'Model ID (default: speech-2.8-hd)' }, - { flag: '--text ', description: 'Text to synthesize' }, - { flag: '--text-file ', description: 'Read text from file (use - for stdin)' }, - { flag: '--voice ', description: 'Voice ID (default: English_expressive_narrator)' }, - { flag: '--speed ', description: 'Speech speed multiplier' }, - { flag: '--volume ', description: 'Volume level' }, - { flag: '--pitch ', description: 'Pitch adjustment' }, - { flag: '--format ', description: 'Audio format (default: mp3)' }, - { flag: '--sample-rate ', description: 'Sample rate (default: 32000)' }, - { flag: '--bitrate ', description: 'Bitrate (default: 128000)' }, - { flag: '--channels ', description: 'Audio channels (default: 1)' }, - { flag: '--language ', description: 'Language boost' }, - { flag: '--subtitles', description: 'Include subtitle timing data' }, - { flag: '--pronunciation ', description: 'Custom pronunciation (repeatable)' }, - { flag: '--sound-effect ', description: 'Add sound effect' }, - { flag: '--out ', description: 'Save audio to file (uses hex decoding)' }, - { flag: '--stream', description: 'Stream raw audio to stdout' }, + { flag: '--model ', description: 'Model ID (default: speech-2.8-hd)' }, + { flag: '--text ', description: 'Text to synthesize' }, + { flag: '--text-file ', description: 'Read text from file (use - for stdin)' }, + { flag: '--voice ', description: 'Voice ID (default: English_expressive_narrator)' }, + { flag: '--speed ', description: 'Speech speed multiplier', type: 'number' }, + { flag: '--volume ', description: 'Volume level', type: 'number' }, + { flag: '--pitch ', description: 'Pitch adjustment', type: 'number' }, + { flag: '--format ', description: 'Audio format (default: mp3)' }, + { flag: '--sample-rate ', description: 'Sample rate (default: 32000)', type: 'number' }, + { flag: '--bitrate ', description: 'Bitrate (default: 128000)', type: 'number' }, + { flag: '--channels ', description: 'Audio channels (default: 1)', type: 'number' }, + { flag: '--language ', description: 'Language boost' }, + { flag: '--subtitles', description: 'Include subtitle timing data' }, + { flag: '--pronunciation ', description: 'Custom pronunciation (repeatable)', type: 'array' }, + { flag: '--sound-effect ', description: 'Add sound effect' }, + { flag: '--out ', description: 'Save audio to file (uses hex decoding)' }, + { flag: '--stream', description: 'Stream raw audio to stdout' }, ], examples: [ 'minimax speech synthesize --text "Hello, world!"', diff --git a/src/commands/text/chat.ts b/src/commands/text/chat.ts index 2d764b9..6cfa94c 100644 --- a/src/commands/text/chat.ts +++ b/src/commands/text/chat.ts @@ -83,14 +83,14 @@ export default defineCommand({ usage: 'minimax text chat --message [flags]', options: [ { flag: '--model ', description: 'Model ID (default: MiniMax-M2.7)' }, - { flag: '--message ', description: 'Message text (repeatable, prefix role: to set role)', required: true }, - { flag: '--messages-file ', description: 'JSON file with messages array (use - for stdin)' }, - { flag: '--system ', description: 'System prompt' }, - { flag: '--max-tokens ', description: 'Maximum tokens to generate (default: 4096)' }, - { flag: '--temperature ', description: 'Sampling temperature (0.0, 1.0]' }, - { flag: '--top-p ', description: 'Nucleus sampling threshold' }, - { flag: '--stream', description: 'Stream response tokens (default: on in TTY)' }, - { flag: '--tool ', description: 'Tool definition as JSON or file path (repeatable)' }, + { flag: '--message ', description: 'Message text (repeatable, prefix role: to set role)', required: true, type: 'array' }, + { flag: '--messages-file ', description: 'JSON file with messages array (use - for stdin)' }, + { flag: '--system ', description: 'System prompt' }, + { flag: '--max-tokens ', description: 'Maximum tokens to generate (default: 4096)', type: 'number' }, + { flag: '--temperature ', description: 'Sampling temperature (0.0, 1.0]', type: 'number' }, + { flag: '--top-p ', description: 'Nucleus sampling threshold', type: 'number' }, + { flag: '--stream', description: 'Stream response tokens (default: on in TTY)' }, + { flag: '--tool ', description: 'Tool definition as JSON or file path (repeatable)', type: 'array' }, ], examples: [ 'minimax text chat --message "What is MiniMax?"', diff --git a/src/commands/video/generate.ts b/src/commands/video/generate.ts index 1bacb2a..fae58c7 100644 --- a/src/commands/video/generate.ts +++ b/src/commands/video/generate.ts @@ -25,7 +25,7 @@ export default defineCommand({ { flag: '--download ', description: 'Save video to file on completion' }, { flag: '--no-wait', description: 'Return task ID immediately without waiting' }, { flag: '--async', description: 'Return task ID immediately (agent/CI mode, same as --no-wait but explicit)' }, - { flag: '--poll-interval ', description: 'Polling interval when waiting (default: 5)' }, + { flag: '--poll-interval ', description: 'Polling interval when waiting (default: 5)', type: 'number' }, ], examples: [ 'minimax video generate --prompt "A man reads a book. Static shot."', diff --git a/src/main.ts b/src/main.ts index e72c1e9..915ff49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ -import { parseArgs } from './args'; +import { scanCommandPath, parseFlags } from './args'; import { registry } from './registry'; +import { GLOBAL_OPTIONS } from './command'; import { handleError } from './errors/handler'; import { loadConfig } from './config/loader'; import { detectRegion, saveDetectedRegion } from './config/detect-region'; @@ -9,26 +10,29 @@ import { checkForUpdate, getPendingUpdateNotification } from './update/checker'; const CLI_VERSION = process.env.CLI_VERSION ?? '0.3.1'; async function main() { - const args = process.argv.slice(2); + const argv = process.argv.slice(2); - if (args.includes('--version') || args.includes('-v')) { + if (argv.includes('--version') || argv.includes('-v')) { console.log(`minimax ${CLI_VERSION}`); process.exit(0); } - const { commandPath, flags } = parseArgs(args); + // Pass 1: find command path from positional args + const commandPath = scanCommandPath(argv); - if (flags.help || commandPath.length === 0) { + if (argv.includes('--help') || argv.includes('-h') || commandPath.length === 0) { registry.printHelp(commandPath, process.stderr); process.exit(0); } + // Pass 2: resolve command, then parse flags with the merged schema const { command, extra } = registry.resolve(commandPath); + const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...(command.options ?? [])]); + if (extra.length > 0) (flags as Record)._positional = extra; const config = loadConfig(flags); - // Auto-detect region when no explicit region is set and the API key has changed if (config.needsRegionDetection) { const apiKey = config.apiKey || config.fileApiKey || config.envApiKey; if (apiKey) { @@ -40,12 +44,10 @@ async function main() { } } - // Fire-and-forget update check (non-blocking) const updateCheckPromise = checkForUpdate(CLI_VERSION).catch(() => {}); await command.execute(config, flags); - // After command finishes, flush the update check and notify if needed await updateCheckPromise; const newVersion = getPendingUpdateNotification(); if (newVersion && !config.quiet) { diff --git a/src/output/status-bar.ts b/src/output/status-bar.ts new file mode 100644 index 0000000..2751450 --- /dev/null +++ b/src/output/status-bar.ts @@ -0,0 +1,28 @@ +import type { Config } from '../config/schema'; + +let printed = false; + +const reset = '\x1b[0m'; +const dim = '\x1b[2m'; +const bold = '\x1b[1m'; +const mmBlue = '\x1b[38;2;43;82;255m'; +const mmPurple = '\x1b[38;2;147;51;234m'; +const mmCyan = '\x1b[38;2;6;184;212m'; +const mmPink = '\x1b[38;2;236;72;153m'; + +export function maybeShowStatusBar(config: Config, token: string, model?: string): void { + if (config.quiet || printed || !process.stderr.isTTY) return; + printed = true; + + const region = config.baseUrl.includes('minimaxi.com') ? 'CN' : 'Global'; + const maskedKey = token.length > 8 ? `${token.slice(0, 4)}...${token.slice(-4)}` : '***'; + const modelStr = model ? ` ${dim}|${reset} ${dim}Model:${reset} ${mmPurple}${model}${reset}` : ''; + + process.stderr.write( + `${bold}${mmBlue}MINIMAX${reset} ` + + `${dim}Region:${reset} ${mmCyan}${region}${reset} ` + + `${dim}|${reset} ` + + `${dim}Key:${reset} ${mmPink}${maskedKey}${reset}` + + `${modelStr}\n`, + ); +} diff --git a/src/registry.ts b/src/registry.ts index 2a12685..06edc0c 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -20,10 +20,6 @@ import quotaShow from './commands/quota/show'; import configShow from './commands/config/show'; import configSet from './commands/config/set'; import configExportSchema from './commands/config/export-schema'; -// File API temporarily disabled (pending permission grant) -// import fileUpload from './commands/file/upload'; -// import fileList from './commands/file/list'; -// import fileDelete from './commands/file/delete'; import update from './commands/update'; export type { Command, OptionDef } from './command'; @@ -237,14 +233,8 @@ export const registry = new CommandRegistry({ 'search query': searchQuery, 'vision describe': visionDescribe, 'quota show': quotaShow, - 'config show': configShow, - 'config set': configSet, + 'config show': configShow, + 'config set': configSet, 'config export-schema': configExportSchema, - - // File API temporarily disabled (pending permission grant) - // 'file upload': fileUpload, - // 'file list': fileList, - // 'file delete': fileDelete, - - 'update': update, + 'update': update, });