mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 22:17:14 +00:00
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:
+69
-10
@@ -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']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user