From 8d95de9fd37cfbe8be9e83481cce07dedc418106 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 25 Feb 2026 17:01:32 +0700 Subject: [PATCH] fix(cli): improve cliproxy target parsing edge cases - support POSIX -- terminator so dash-prefixed variant names remain positional - export variant parser for direct unit tests - add --target parsing coverage for API and cliproxy arg parsers --- src/commands/cliproxy/variant-subcommand.ts | 27 ++++++---- tests/unit/commands/api-command-args.test.ts | 30 +++++++++++ .../commands/cliproxy-variant-args.test.ts | 51 +++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 tests/unit/commands/cliproxy-variant-args.test.ts diff --git a/src/commands/cliproxy/variant-subcommand.ts b/src/commands/cliproxy/variant-subcommand.ts index 53171b20..b3a00056 100644 --- a/src/commands/cliproxy/variant-subcommand.ts +++ b/src/commands/cliproxy/variant-subcommand.ts @@ -48,17 +48,24 @@ function parseTargetValue(rawValue: string): TargetType | null { return null; } -function parseProfileArgs(args: string[]): CliproxyProfileArgs { +export function parseProfileArgs(args: string[]): CliproxyProfileArgs { const result: CliproxyProfileArgs = { errors: [] }; + let parseOptions = true; + for (let i = 0; i < args.length; i++) { const arg = args[i]; - if (arg === '--provider' && args[i + 1]) { + if (parseOptions && arg === '--') { + parseOptions = false; + continue; + } + + if (parseOptions && arg === '--provider' && args[i + 1]) { result.provider = args[++i] as CLIProxyProfileName; - } else if (arg === '--model' && args[i + 1]) { + } else if (parseOptions && arg === '--model' && args[i + 1]) { result.model = args[++i]; - } else if (arg === '--account' && args[i + 1]) { + } else if (parseOptions && arg === '--account' && args[i + 1]) { result.account = args[++i]; - } else if (arg === '--target') { + } else if (parseOptions && arg === '--target') { const rawValue = args[i + 1]; if (!rawValue || rawValue.startsWith('-')) { result.errors.push('Missing value for --target'); @@ -71,7 +78,7 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs { result.target = parsedTarget; } } - } else if (arg.startsWith('--target=')) { + } else if (parseOptions && arg.startsWith('--target=')) { const rawValue = arg.slice('--target='.length); const parsedTarget = parseTargetValue(rawValue); if (!parsedTarget) { @@ -79,13 +86,13 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs { } else { result.target = parsedTarget; } - } else if (arg === '--force') { + } else if (parseOptions && arg === '--force') { result.force = true; - } else if (arg === '--yes' || arg === '-y') { + } else if (parseOptions && (arg === '--yes' || arg === '-y')) { result.yes = true; - } else if (arg === '--composite') { + } else if (parseOptions && arg === '--composite') { result.composite = true; - } else if (!arg.startsWith('-') && !result.name) { + } else if ((!parseOptions || !arg.startsWith('-')) && !result.name) { result.name = arg; } } diff --git a/tests/unit/commands/api-command-args.test.ts b/tests/unit/commands/api-command-args.test.ts index fc5ac004..b73e1e08 100644 --- a/tests/unit/commands/api-command-args.test.ts +++ b/tests/unit/commands/api-command-args.test.ts @@ -44,10 +44,40 @@ describe('api-command arg parser', () => { expect(parsed.errors).toEqual([]); }); + test('parses --target=value for default profile target', () => { + const parsed = parseApiCommandArgs(['my-api', '--target=droid']); + + expect(parsed.name).toBe('my-api'); + expect(parsed.target).toBe('droid'); + expect(parsed.errors).toEqual([]); + }); + test('validates invalid --target values', () => { const parsed = parseApiCommandArgs(['my-api', '--target', 'invalid-target']); expect(parsed.target).toBeUndefined(); expect(parsed.errors).toEqual(['Invalid --target value "invalid-target". Use: claude or droid']); }); + + test('collects missing-value error for --target with no value', () => { + const parsed = parseApiCommandArgs(['my-api', '--target']); + + expect(parsed.target).toBeUndefined(); + expect(parsed.errors).toEqual(['Missing value for --target']); + }); + + test('treats empty --target=value as missing value', () => { + const parsed = parseApiCommandArgs(['my-api', '--target=']); + + expect(parsed.target).toBeUndefined(); + expect(parsed.errors).toEqual(['Missing value for --target']); + }); + + test('uses last --target value when repeated', () => { + const parsed = parseApiCommandArgs(['my-api', '--target', 'claude', '--target=droid']); + + expect(parsed.name).toBe('my-api'); + expect(parsed.target).toBe('droid'); + expect(parsed.errors).toEqual([]); + }); }); diff --git a/tests/unit/commands/cliproxy-variant-args.test.ts b/tests/unit/commands/cliproxy-variant-args.test.ts new file mode 100644 index 00000000..1d3ad82e --- /dev/null +++ b/tests/unit/commands/cliproxy-variant-args.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from 'bun:test'; + +import { parseProfileArgs } from '../../../src/commands/cliproxy/variant-subcommand'; + +describe('cliproxy variant arg parser', () => { + test('parses --target value form', () => { + const parsed = parseProfileArgs(['variant-a', '--target', 'droid']); + + expect(parsed.name).toBe('variant-a'); + expect(parsed.target).toBe('droid'); + expect(parsed.errors).toEqual([]); + }); + + test('parses --target=value form', () => { + const parsed = parseProfileArgs(['variant-a', '--target=droid']); + + expect(parsed.name).toBe('variant-a'); + expect(parsed.target).toBe('droid'); + expect(parsed.errors).toEqual([]); + }); + + test('collects missing value error for --target with no value', () => { + const parsed = parseProfileArgs(['variant-a', '--target']); + + expect(parsed.target).toBeUndefined(); + expect(parsed.errors).toEqual(['Missing value for --target']); + }); + + test('uses last --target value when repeated', () => { + const parsed = parseProfileArgs(['variant-a', '--target', 'claude', '--target=droid']); + + expect(parsed.target).toBe('droid'); + expect(parsed.errors).toEqual([]); + }); + + test('supports option terminator for variant names that start with dash', () => { + const parsed = parseProfileArgs(['--yes', '--', '-variant-a']); + + expect(parsed.yes).toBe(true); + expect(parsed.name).toBe('-variant-a'); + expect(parsed.errors).toEqual([]); + }); + + test('does not parse flags after option terminator', () => { + const parsed = parseProfileArgs(['--', '--target', 'droid']); + + expect(parsed.target).toBeUndefined(); + expect(parsed.name).toBe('--target'); + expect(parsed.errors).toEqual([]); + }); +});