feat(api): add --target to api command

- parse and validate --target claude|droid on api create flow

- show target in API and CLIProxy list tables and usage output

- add parser unit tests for target success and failure cases
This commit is contained in:
Tam Nhu Tran
2026-02-25 15:51:54 +07:00
parent e833054642
commit 3dacb39deb
2 changed files with 84 additions and 10 deletions
+69 -10
View File
@@ -43,6 +43,7 @@ import {
type ProviderPreset, type ProviderPreset,
} from '../api/services'; } from '../api/services';
import { syncToLocalConfig } from '../cliproxy/sync/local-config-sync'; import { syncToLocalConfig } from '../cliproxy/sync/local-config-sync';
import type { TargetType } from '../targets/target-adapter';
import { extractOption, hasAnyFlag } from './arg-extractor'; import { extractOption, hasAnyFlag } from './arg-extractor';
interface ApiCommandArgs { interface ApiCommandArgs {
@@ -51,13 +52,14 @@ interface ApiCommandArgs {
apiKey?: string; apiKey?: string;
model?: string; model?: string;
preset?: string; preset?: string;
target?: TargetType;
force?: boolean; force?: boolean;
yes?: boolean; yes?: boolean;
errors: string[]; errors: string[];
} }
const API_BOOLEAN_FLAGS = ['--force', '--yes', '-y'] as const; const API_BOOLEAN_FLAGS = ['--force', '--yes', '-y'] as const;
const API_VALUE_FLAGS = ['--base-url', '--api-key', '--model', '--preset'] as const; const API_VALUE_FLAGS = ['--base-url', '--api-key', '--model', '--preset', '--target'] as const;
const API_KNOWN_FLAGS: readonly string[] = [...API_BOOLEAN_FLAGS, ...API_VALUE_FLAGS]; const API_KNOWN_FLAGS: readonly string[] = [...API_BOOLEAN_FLAGS, ...API_VALUE_FLAGS];
const API_VALUE_FLAG_SET = new Set<string>(API_VALUE_FLAGS); const API_VALUE_FLAG_SET = new Set<string>(API_VALUE_FLAGS);
@@ -130,6 +132,14 @@ function extractPositionalArgs(args: string[]): string[] {
return positionals; return positionals;
} }
function parseTargetValue(value: string): TargetType | null {
const normalized = value.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
return normalized;
}
return null;
}
/** Parse command line arguments for api commands */ /** Parse command line arguments for api commands */
export function parseApiCommandArgs(args: string[]): ApiCommandArgs { export function parseApiCommandArgs(args: string[]): ApiCommandArgs {
const result: ApiCommandArgs = { const result: ApiCommandArgs = {
@@ -184,6 +194,22 @@ export function parseApiCommandArgs(args: string[]): ApiCommandArgs {
} }
); );
remaining = applyRepeatedOption(
remaining,
['--target'],
(value) => {
const target = parseTargetValue(value);
if (!target) {
result.errors.push(`Invalid --target value "${value}". Use: claude or droid`);
return;
}
result.target = target;
},
() => {
result.errors.push('Missing value for --target');
}
);
const positionalArgs = extractPositionalArgs(remaining); const positionalArgs = extractPositionalArgs(remaining);
result.name = positionalArgs[0]; result.name = positionalArgs[0];
return result; return result;
@@ -383,12 +409,23 @@ async function handleCreate(args: string[]): Promise<void> {
sonnet: sonnetModel, sonnet: sonnetModel,
haiku: haikuModel, haiku: haikuModel,
}; };
let resolvedTarget: TargetType = parsedArgs.target || 'claude';
if (!parsedArgs.target && !parsedArgs.yes) {
const useDroidByDefault = await InteractivePrompt.confirm(
'Set default target to Factory Droid for this profile?',
{ default: false }
);
if (useDroidByDefault) {
resolvedTarget = 'droid';
}
}
// Create profile // Create profile
console.log(''); console.log('');
console.log(info('Creating API profile...')); console.log(info('Creating API profile...'));
const result = createApiProfile(name, baseUrl, apiKey, models); const result = createApiProfile(name, baseUrl, apiKey, models, resolvedTarget);
if (!result.success) { if (!result.success) {
console.log(fail(`Failed to create API profile: ${result.error}`)); console.log(fail(`Failed to create API profile: ${result.error}`));
@@ -411,7 +448,8 @@ async function handleCreate(args: string[]): Promise<void> {
`Config: ${isUsingUnifiedConfig() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'}\n` + `Config: ${isUsingUnifiedConfig() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'}\n` +
`Settings: ${result.settingsFile}\n` + `Settings: ${result.settingsFile}\n` +
`Base URL: ${baseUrl}\n` + `Base URL: ${baseUrl}\n` +
`Model: ${model}`; `Model: ${model}\n` +
`Target: ${resolvedTarget}`;
if (hasCustomMapping) { if (hasCustomMapping) {
infoMsg += infoMsg +=
@@ -424,7 +462,24 @@ async function handleCreate(args: string[]): Promise<void> {
console.log(infoBox(infoMsg, 'API Profile Created')); console.log(infoBox(infoMsg, 'API Profile Created'));
console.log(''); console.log('');
console.log(header('Usage')); console.log(header('Usage'));
console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`); if (resolvedTarget === 'droid') {
console.log(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
);
} else {
console.log(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# run on droid for this call')}`
);
}
console.log(''); console.log('');
console.log(header('Edit Settings')); console.log(header('Edit Settings'));
console.log(` ${dim('To modify env vars later:')}`); console.log(` ${dim('To modify env vars later:')}`);
@@ -453,13 +508,13 @@ async function handleList(): Promise<void> {
// Build table data // Build table data
const rows: string[][] = profiles.map((p) => { const rows: string[][] = profiles.map((p) => {
const status = p.isConfigured ? color('[OK]', 'success') : color('[!]', 'warning'); const status = p.isConfigured ? color('[OK]', 'success') : color('[!]', 'warning');
return [p.name, p.settingsPath, status]; return [p.name, p.target, p.settingsPath, status];
}); });
const colWidths = isUsingUnifiedConfig() ? [15, 20, 10] : [15, 35, 10]; const colWidths = isUsingUnifiedConfig() ? [15, 10, 20, 10] : [15, 10, 35, 10];
console.log( console.log(
table(rows, { table(rows, {
head: ['API', isUsingUnifiedConfig() ? 'Config' : 'Settings File', 'Status'], head: ['API', 'Target', isUsingUnifiedConfig() ? 'Config' : 'Settings File', 'Status'],
colWidths, colWidths,
}) })
); );
@@ -468,11 +523,11 @@ async function handleList(): Promise<void> {
// Show CLIProxy variants if any // Show CLIProxy variants if any
if (variants.length > 0) { if (variants.length > 0) {
console.log(subheader('CLIProxy Variants')); console.log(subheader('CLIProxy Variants'));
const cliproxyRows = variants.map((v) => [v.name, v.provider, v.settings]); const cliproxyRows = variants.map((v) => [v.name, v.provider, v.target, v.settings]);
console.log( console.log(
table(cliproxyRows, { table(cliproxyRows, {
head: ['Variant', 'Provider', 'Settings'], head: ['Variant', 'Provider', 'Target', 'Settings'],
colWidths: [15, 15, 30], colWidths: [15, 12, 10, 28],
}) })
); );
console.log(''); console.log('');
@@ -582,6 +637,9 @@ async function showHelp(): Promise<void> {
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`); console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
console.log(` ${color('--api-key <key>', 'command')} API key (create)`); console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
console.log(` ${color('--model <model>', 'command')} Default model (create)`); console.log(` ${color('--model <model>', 'command')} Default model (create)`);
console.log(
` ${color('--target <cli>', 'command')} Default target: claude or droid (create)`
);
console.log(` ${color('--force', 'command')} Overwrite existing (create)`); console.log(` ${color('--force', 'command')} Overwrite existing (create)`);
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`);
console.log(''); console.log('');
@@ -605,6 +663,7 @@ async function showHelp(): Promise<void> {
console.log(''); console.log('');
console.log(` ${dim('# Create with name')}`); console.log(` ${dim('# Create with name')}`);
console.log(` ${color('ccs api create myapi', 'command')}`); console.log(` ${color('ccs api create myapi', 'command')}`);
console.log(` ${color('ccs api create mydroid --preset glm --target droid', 'command')}`);
console.log(''); console.log('');
console.log(` ${dim('# Remove API profile')}`); console.log(` ${dim('# Remove API profile')}`);
console.log(` ${color('ccs api remove myapi', 'command')}`); console.log(` ${color('ccs api remove myapi', 'command')}`);
@@ -35,4 +35,19 @@ describe('api-command arg parser', () => {
expect(parsed.yes).toBe(true); expect(parsed.yes).toBe(true);
expect(parsed.name).toBe('-my-api'); expect(parsed.name).toBe('-my-api');
}); });
test('parses --target 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']);
});
}); });