feat(cliproxy): add target support to subcommands

- add --target parsing and validation in create/edit/remove flows

- show target in list/remove/edit UX and usage examples

- document target option in cliproxy help output
This commit is contained in:
Tam Nhu Tran
2026-02-25 15:52:29 +07:00
parent 5b60784eb8
commit d2d1d599cd
3 changed files with 172 additions and 9 deletions
+5 -2
View File
@@ -45,10 +45,13 @@ export async function handleList(): Promise<void> {
const variant = variants[name];
const providerDisplay = variant.type === 'composite' ? 'composite' : variant.provider;
const portStr = variant.port ? String(variant.port) : '-';
return [name, providerDisplay, portStr, variant.settings || '-'];
return [name, providerDisplay, variant.target || 'claude', portStr, variant.settings || '-'];
});
console.log(
table(rows, { head: ['Variant', 'Provider', 'Port', 'Settings'], colWidths: [15, 12, 8, 30] })
table(rows, {
head: ['Variant', 'Provider', 'Target', 'Port', 'Settings'],
colWidths: [15, 12, 10, 8, 24],
})
);
console.log('');
console.log(dim(`Total: ${variantNames.length} custom variant(s)`));
+1
View File
@@ -81,6 +81,7 @@ export async function showHelp(): Promise<void> {
'Options:',
[
['--backend <type>', 'Use specific backend: original | plus (default: from config)'],
['--target <cli>', 'Default target for created/edited variants: claude | droid'],
['--verbose, -v', 'Show detailed quota fetch diagnostics'],
],
],
+166 -7
View File
@@ -12,6 +12,7 @@ import { triggerOAuth } from '../../cliproxy/auth/oauth-handler';
import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
import type { TargetType } from '../../targets/target-adapter';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { initUI, header, color, ok, fail, warn, info, infoBox, dim } from '../../utils/ui';
import { InteractivePrompt } from '../../utils/prompt';
@@ -32,13 +33,23 @@ interface CliproxyProfileArgs {
provider?: CLIProxyProfileName;
model?: string;
account?: string;
target?: TargetType;
force?: boolean;
yes?: boolean;
composite?: boolean;
errors: string[];
}
function parseTargetValue(rawValue: string): TargetType | null {
const normalized = rawValue.trim().toLowerCase();
if (normalized === 'claude' || normalized === 'droid') {
return normalized;
}
return null;
}
function parseProfileArgs(args: string[]): CliproxyProfileArgs {
const result: CliproxyProfileArgs = {};
const result: CliproxyProfileArgs = { errors: [] };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--provider' && args[i + 1]) {
@@ -47,6 +58,27 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs {
result.model = args[++i];
} else if (arg === '--account' && args[i + 1]) {
result.account = args[++i];
} else if (arg === '--target') {
const rawValue = args[i + 1];
if (!rawValue || rawValue.startsWith('-')) {
result.errors.push('Missing value for --target');
} else {
i += 1;
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
} else {
result.target = parsedTarget;
}
}
} else if (arg.startsWith('--target=')) {
const rawValue = arg.slice('--target='.length);
const parsedTarget = parseTargetValue(rawValue);
if (!parsedTarget) {
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
} else {
result.target = parsedTarget;
}
} else if (arg === '--force') {
result.force = true;
} else if (arg === '--yes' || arg === '-y') {
@@ -145,6 +177,11 @@ export async function handleCreate(
): Promise<void> {
await initUI();
const parsedArgs = parseProfileArgs(args);
if (parsedArgs.errors.length > 0) {
parsedArgs.errors.forEach((errorMessage) => console.log(fail(errorMessage)));
process.exitCode = 1;
return;
}
console.log(header(`Create ${getBackendLabel(backend)} Variant`));
console.log('');
@@ -168,6 +205,17 @@ export async function handleCreate(
process.exit(1);
}
let resolvedTarget: TargetType = parsedArgs.target || 'claude';
if (!parsedArgs.target && !parsedArgs.yes) {
const useDroidByDefault = await InteractivePrompt.confirm(
'Set default target to Factory Droid for this variant?',
{ default: false }
);
if (useDroidByDefault) {
resolvedTarget = 'droid';
}
}
// Composite mode: select provider+model per tier
if (parsedArgs.composite) {
console.log(info('Composite variant — select provider and model for each tier'));
@@ -203,6 +251,7 @@ export async function handleCreate(
const result = createCompositeVariant({
name,
defaultTier,
target: resolvedTarget,
tiers: { opus, sonnet, haiku },
});
@@ -217,7 +266,8 @@ export async function handleCreate(
? `Opus: ${tiers.opus.provider} / ${tiers.opus.model}\n` +
`Sonnet: ${tiers.sonnet.provider} / ${tiers.sonnet.model}\n` +
`Haiku: ${tiers.haiku.provider} / ${tiers.haiku.model}\n` +
`Default: ${defaultTier}`
`Default: ${defaultTier}\n` +
`Target: ${resolvedTarget}`
: '';
const portInfo = result.variant?.port ? `\nPort: ${result.variant.port}` : '';
console.log(
@@ -228,7 +278,24 @@ export async function handleCreate(
);
console.log('');
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('');
return;
}
@@ -350,7 +417,7 @@ export async function handleCreate(
// Create variant
console.log('');
console.log(info(`Creating ${getBackendLabel(backend)} variant...`));
const result = createVariant(name, provider, model, account);
const result = createVariant(name, provider, model, account, resolvedTarget);
if (!result.success) {
console.log(fail(`Failed to create variant: ${result.error}`));
@@ -367,13 +434,30 @@ export async function handleCreate(
const portInfo = result.variant?.port ? `Port: ${result.variant.port}\n` : '';
console.log(
infoBox(
`Variant: ${name}\nProvider: ${provider}\nModel: ${model}\n${portInfo}${account ? `Account: ${account}\n` : ''}${isUnifiedMode() ? 'Config' : 'Settings'}: ${settingsDisplay}`,
`Variant: ${name}\nProvider: ${provider}\nModel: ${model}\nTarget: ${resolvedTarget}\n${portInfo}${account ? `Account: ${account}\n` : ''}${isUnifiedMode() ? 'Config' : 'Settings'}: ${settingsDisplay}`,
configType
)
);
console.log('');
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(dim('To change model later:'));
console.log(` ${color(`ccs ${name} --config`, 'command')}`);
@@ -383,6 +467,11 @@ export async function handleCreate(
export async function handleRemove(args: string[]): Promise<void> {
await initUI();
const parsedArgs = parseProfileArgs(args);
if (parsedArgs.errors.length > 0) {
parsedArgs.errors.forEach((errorMessage) => console.log(fail(errorMessage)));
process.exitCode = 1;
return;
}
const variants = listVariants();
const variantNames = Object.keys(variants);
@@ -435,6 +524,7 @@ export async function handleRemove(args: string[]): Promise<void> {
if (variant.port) {
console.log(` Port: ${variant.port}`);
}
console.log(` Target: ${variant.target || 'claude'}`);
console.log(` Settings: ${variant.settings || '-'}`);
console.log('');
@@ -461,6 +551,11 @@ export async function handleEdit(
): Promise<void> {
await initUI();
const parsedArgs = parseProfileArgs(args);
if (parsedArgs.errors.length > 0) {
parsedArgs.errors.forEach((errorMessage) => console.log(fail(errorMessage)));
process.exitCode = 1;
return;
}
const variants = listVariants();
const variantNames = Object.keys(variants);
@@ -501,12 +596,14 @@ export async function handleEdit(
// If not composite, use existing updateVariant() flow (interactive prompts)
if (variant.type !== 'composite') {
const currentTarget: TargetType = variant.target || 'claude';
console.log(header(`Edit Variant: ${name}`));
console.log('');
console.log(`Current provider: ${variant.provider}`);
if (variant.model) {
console.log(`Current model: ${variant.model}`);
}
console.log(`Current target: ${currentTarget}`);
console.log('');
const changeProvider = await InteractivePrompt.confirm('Change provider?', { default: false });
@@ -552,6 +649,22 @@ export async function handleEdit(
}
}
let newTarget: TargetType | undefined = parsedArgs.target;
if (!parsedArgs.target) {
const changeTarget = await InteractivePrompt.confirm('Change default target?', {
default: false,
});
if (changeTarget) {
const targetOptions = [
{ id: 'claude', label: 'Claude Code' },
{ id: 'droid', label: 'Factory Droid' },
];
newTarget = (await InteractivePrompt.selectFromList('Select target:', targetOptions, {
defaultIndex: currentTarget === 'droid' ? 1 : 0,
})) as TargetType;
}
}
console.log('');
console.log(info(`Updating ${getBackendLabel(backend)} variant...`));
// Use existing updateVariant from variant-service for single-provider variants
@@ -559,6 +672,7 @@ export async function handleEdit(
const result = updateVariant(name, {
provider: newProvider,
model: changeModel ? newModel : undefined,
target: newTarget,
});
if (!result.success) {
@@ -566,13 +680,35 @@ export async function handleEdit(
process.exit(1);
}
const resolvedTarget = result.variant?.target || currentTarget;
console.log('');
console.log(ok(`Variant updated: ${name}`));
console.log('');
console.log(header('Usage'));
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('');
return;
}
// Composite variant edit flow
const compositeCurrentTarget: TargetType = variant.target || 'claude';
console.log(header(`Edit Composite Variant: ${name}`));
console.log('');
if (!variant.tiers) {
@@ -585,6 +721,7 @@ export async function handleEdit(
console.log(` Sonnet: ${variant.tiers.sonnet.provider} / ${variant.tiers.sonnet.model}`);
console.log(` Haiku: ${variant.tiers.haiku.provider} / ${variant.tiers.haiku.model}`);
console.log(` Default: ${variant.default_tier}`);
console.log(` Target: ${compositeCurrentTarget}`);
console.log('');
const verbose = args.includes('--verbose');
@@ -634,11 +771,32 @@ export async function handleEdit(
)) as 'opus' | 'sonnet' | 'haiku';
}
let newCompositeTarget: TargetType | undefined = parsedArgs.target;
if (!parsedArgs.target) {
const changeTarget = await InteractivePrompt.confirm('Change default target?', {
default: false,
});
if (changeTarget) {
const targetOptions = [
{ id: 'claude', label: 'Claude Code' },
{ id: 'droid', label: 'Factory Droid' },
];
newCompositeTarget = (await InteractivePrompt.selectFromList(
'Select target:',
targetOptions,
{
defaultIndex: compositeCurrentTarget === 'droid' ? 1 : 0,
}
)) as TargetType;
}
}
console.log('');
console.log(info(`Updating composite ${getBackendLabel(backend)} variant...`));
const result = updateCompositeVariant(name, {
tiers: updatedTiers,
defaultTier: changeDefault ? newDefaultTier : undefined,
target: newCompositeTarget,
});
if (!result.success) {
@@ -653,7 +811,8 @@ export async function handleEdit(
`Opus: ${finalVariant.tiers.opus.provider} / ${finalVariant.tiers.opus.model}\n` +
`Sonnet: ${finalVariant.tiers.sonnet.provider} / ${finalVariant.tiers.sonnet.model}\n` +
`Haiku: ${finalVariant.tiers.haiku.provider} / ${finalVariant.tiers.haiku.model}\n` +
`Default: ${finalVariant.default_tier}`;
`Default: ${finalVariant.default_tier}\n` +
`Target: ${finalVariant.target || compositeCurrentTarget}`;
const portInfo = finalVariant.port ? `\nPort: ${finalVariant.port}` : '';
console.log(
infoBox(