diff --git a/src/commands/cliproxy/auth-subcommand.ts b/src/commands/cliproxy/auth-subcommand.ts index 76ab6730..b856f278 100644 --- a/src/commands/cliproxy/auth-subcommand.ts +++ b/src/commands/cliproxy/auth-subcommand.ts @@ -45,10 +45,13 @@ export async function handleList(): Promise { 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)`)); diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts index 01d5bd13..cc3e60a2 100644 --- a/src/commands/cliproxy/help-subcommand.ts +++ b/src/commands/cliproxy/help-subcommand.ts @@ -81,6 +81,7 @@ export async function showHelp(): Promise { 'Options:', [ ['--backend ', 'Use specific backend: original | plus (default: from config)'], + ['--target ', 'Default target for created/edited variants: claude | droid'], ['--verbose, -v', 'Show detailed quota fetch diagnostics'], ], ], diff --git a/src/commands/cliproxy/variant-subcommand.ts b/src/commands/cliproxy/variant-subcommand.ts index fa395445..53171b20 100644 --- a/src/commands/cliproxy/variant-subcommand.ts +++ b/src/commands/cliproxy/variant-subcommand.ts @@ -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 { 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 { 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 { 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 { 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(