Files
ccs/src/commands/cliproxy/variant-subcommand.ts
T
Tam Nhu Tran d2d1d599cd 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
2026-02-25 15:52:29 +07:00

828 lines
28 KiB
TypeScript

/**
* CLIProxy Variant Management
*
* Handles:
* - ccs cliproxy create [name]
* - ccs cliproxy remove <name>
*/
import * as path from 'path';
import { getProviderAccounts } from '../../cliproxy/account-manager';
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';
import {
validateProfileName,
variantExists,
listVariants,
createVariant,
createCompositeVariant,
updateCompositeVariant,
removeVariant,
} from '../../cliproxy/services';
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
import { CompositeTierConfig } from '../../config/unified-config-types';
interface CliproxyProfileArgs {
name?: string;
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 = { errors: [] };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--provider' && args[i + 1]) {
result.provider = args[++i] as CLIProxyProfileName;
} else if (arg === '--model' && args[i + 1]) {
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') {
result.yes = true;
} else if (arg === '--composite') {
result.composite = true;
} else if (!arg.startsWith('-') && !result.name) {
result.name = arg;
}
}
return result;
}
function formatModelOption(model: ModelEntry): string {
const tierBadge =
model.tier === 'ultra'
? color(' [Ultra]', 'warning')
: model.tier === 'pro'
? color(' [Pro]', 'warning')
: '';
return `${model.name}${tierBadge}`;
}
function getBackendLabel(backend: CLIProxyBackend): string {
return backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
}
/**
* Interactive prompt to select provider + model for a single tier.
* Returns a CompositeTierConfig, or null if user cancelled auth.
*/
async function selectTierConfig(
tierName: string,
verbose: boolean
): Promise<CompositeTierConfig | null> {
console.log(header(`${tierName.charAt(0).toUpperCase() + tierName.slice(1)} Tier`));
// Select provider
const providerOptions = CLIPROXY_PROFILES.map((p) => ({
id: p,
label: p.charAt(0).toUpperCase() + p.slice(1),
}));
const provider = (await InteractivePrompt.selectFromList(
`Provider for ${tierName}:`,
providerOptions
)) as CLIProxyProfileName;
// Check auth
const providerAccounts = getProviderAccounts(provider as CLIProxyProvider);
if (providerAccounts.length === 0) {
console.log('');
console.log(warn(`No accounts authenticated for ${provider}`));
const shouldAuth = await InteractivePrompt.confirm(`Authenticate with ${provider} now?`, {
default: true,
});
if (!shouldAuth) {
console.log(info(`Skipping auth. Run: ${color(`ccs ${provider} --auth`, 'command')}`));
return null;
}
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
add: true,
verbose,
});
if (!newAccount) {
console.log(fail('Authentication failed'));
process.exit(1);
}
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
}
// Select model
let model: string | undefined;
if (supportsModelConfig(provider as CLIProxyProvider)) {
const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) {
const modelOptions = catalog.models.map((m) => ({ id: m.id, label: formatModelOption(m) }));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
model = await InteractivePrompt.selectFromList(`Model for ${tierName}:`, modelOptions, {
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
});
}
}
if (!model) {
model = await InteractivePrompt.input(`Model name for ${tierName}`, {
validate: (val) => (val ? null : 'Model is required'),
});
}
console.log('');
return { provider, model };
}
export async function handleCreate(
args: string[],
backend: CLIProxyBackend = DEFAULT_BACKEND
): 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('');
// Step 1: Profile name
let name = parsedArgs.name;
if (!name) {
name = await InteractivePrompt.input('Variant name (e.g., g3, flash, pro)', {
validate: validateProfileName,
});
} else {
const error = validateProfileName(name);
if (error) {
console.log(fail(error));
process.exit(1);
}
}
if (variantExists(name) && !parsedArgs.force) {
console.log(fail(`Variant '${name}' already exists`));
console.log(` Use ${color('--force', 'command')} to overwrite`);
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'));
console.log('');
const verbose = args.includes('--verbose');
const opus = await selectTierConfig('opus', verbose);
if (!opus) {
return; // User cancelled auth
}
const sonnet = await selectTierConfig('sonnet', verbose);
if (!sonnet) {
return; // User cancelled auth
}
const haiku = await selectTierConfig('haiku', verbose);
if (!haiku) {
return; // User cancelled auth
}
// Select default tier
const tierOptions = [
{ id: 'opus' as const, label: `Opus (${opus.provider}: ${opus.model})` },
{ id: 'sonnet' as const, label: `Sonnet (${sonnet.provider}: ${sonnet.model})` },
{ id: 'haiku' as const, label: `Haiku (${haiku.provider}: ${haiku.model})` },
];
const defaultTier = (await InteractivePrompt.selectFromList(
'Default tier (ANTHROPIC_MODEL):',
tierOptions
)) as 'opus' | 'sonnet' | 'haiku';
console.log('');
console.log(info(`Creating composite ${getBackendLabel(backend)} variant...`));
const result = createCompositeVariant({
name,
defaultTier,
target: resolvedTarget,
tiers: { opus, sonnet, haiku },
});
if (!result.success) {
console.log(fail(`Failed to create composite variant: ${result.error}`));
process.exit(1);
}
console.log('');
const tiers = result.variant?.tiers;
const tierSummary = tiers
? `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}\n` +
`Target: ${resolvedTarget}`
: '';
const portInfo = result.variant?.port ? `\nPort: ${result.variant.port}` : '';
console.log(
infoBox(
`Variant: ${name} (composite)\n${tierSummary}${portInfo}\nConfig: ~/.ccs/config.yaml`,
'Composite Variant Created'
)
);
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;
}
// Step 2: Provider selection
let provider = parsedArgs.provider;
if (!provider) {
const providerOptions = CLIPROXY_PROFILES.map((p) => ({
id: p,
label: p.charAt(0).toUpperCase() + p.slice(1),
}));
provider = (await InteractivePrompt.selectFromList(
'Select provider:',
providerOptions
)) as CLIProxyProfileName;
} else if (!CLIPROXY_PROFILES.includes(provider)) {
console.log(fail(`Invalid provider: ${provider}`));
console.log(` Available: ${CLIPROXY_PROFILES.join(', ')}`);
process.exit(1);
}
// Step 2.5: Account selection
let account = parsedArgs.account;
const providerAccounts = getProviderAccounts(provider as CLIProxyProvider);
if (!account) {
if (providerAccounts.length === 0) {
console.log('');
console.log(warn(`No accounts authenticated for ${provider}`));
console.log('');
const shouldAuth = await InteractivePrompt.confirm(`Authenticate with ${provider} now?`, {
default: true,
});
if (!shouldAuth) {
console.log('');
console.log(info('Run authentication first:'));
console.log(` ${color(`ccs ${provider} --auth`, 'command')}`);
process.exit(0);
}
console.log('');
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
add: true,
verbose: args.includes('--verbose'),
});
if (!newAccount) {
console.log(fail('Authentication failed'));
process.exit(1);
}
account = newAccount.id;
console.log('');
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
} else if (providerAccounts.length === 1) {
account = providerAccounts[0].id;
} else {
const ADD_NEW_ID = '__add_new__';
const accountOptions = [
...providerAccounts.map((acc) => ({
id: acc.id,
label: `${acc.email || acc.id}${acc.isDefault ? ' (default)' : ''}`,
})),
{ id: ADD_NEW_ID, label: color('[+ Add new account...]', 'info') },
];
const defaultIdx = providerAccounts.findIndex((a) => a.isDefault);
const selectedAccount = await InteractivePrompt.selectFromList(
'Select account:',
accountOptions,
{ defaultIndex: defaultIdx >= 0 ? defaultIdx : 0 }
);
if (selectedAccount === ADD_NEW_ID) {
console.log('');
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
add: true,
verbose: args.includes('--verbose'),
});
if (!newAccount) {
console.log(fail('Authentication failed'));
process.exit(1);
}
account = newAccount.id;
console.log('');
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
} else {
account = selectedAccount;
}
}
} else {
const exists = providerAccounts.find((a) => a.id === account);
if (!exists) {
console.log(fail(`Account '${account}' not found for ${provider}`));
console.log('');
console.log('Available accounts:');
providerAccounts.forEach((a) =>
console.log(` - ${a.email || a.id}${a.isDefault ? ' (default)' : ''}`)
);
process.exit(1);
}
}
// Step 3: Model selection
let model = parsedArgs.model;
if (!model) {
if (supportsModelConfig(provider as CLIProxyProvider)) {
const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) {
const modelOptions = catalog.models.map((m) => ({ id: m.id, label: formatModelOption(m) }));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
model = await InteractivePrompt.selectFromList('Select model:', modelOptions, {
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
});
}
}
if (!model) {
model = await InteractivePrompt.input('Model name', {
validate: (val) => (val ? null : 'Model is required'),
});
}
}
// Create variant
console.log('');
console.log(info(`Creating ${getBackendLabel(backend)} variant...`));
const result = createVariant(name, provider, model, account, resolvedTarget);
if (!result.success) {
console.log(fail(`Failed to create variant: ${result.error}`));
process.exit(1);
}
console.log('');
const configType = isUnifiedMode()
? 'CLIProxy Variant Created (Unified Config)'
: 'CLIProxy Variant Created';
const settingsDisplay = isUnifiedMode()
? '~/.ccs/config.yaml'
: `~/.ccs/${path.basename(result.settingsPath || '')}`;
const portInfo = result.variant?.port ? `Port: ${result.variant.port}\n` : '';
console.log(
infoBox(
`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'));
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')}`);
console.log('');
}
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);
if (variantNames.length === 0) {
console.log(warn('No CLIProxy variants to remove'));
process.exit(0);
}
let name = parsedArgs.name;
if (!name) {
console.log(header('Remove CLIProxy Variant'));
console.log('');
console.log('Available variants:');
variantNames.forEach((n, i) => {
const v = variants[n];
const label = v.type === 'composite' ? 'composite' : v.provider;
console.log(` ${i + 1}. ${n} (${label})`);
});
console.log('');
name = await InteractivePrompt.input('Variant name to remove', {
validate: (val) => {
if (!val) return 'Variant name is required';
if (!variantNames.includes(val)) return `Variant '${val}' not found`;
return null;
},
});
}
if (!variantNames.includes(name)) {
console.log(fail(`Variant '${name}' not found`));
console.log('');
console.log('Available variants:');
variantNames.forEach((n) => console.log(` - ${n}`));
process.exit(1);
}
const variant = variants[name];
console.log('');
console.log(`Variant '${color(name, 'command')}' will be removed.`);
if (variant.type === 'composite') {
console.log(` Type: composite`);
if (variant.tiers) {
console.log(` Opus: ${variant.tiers.opus.provider} / ${variant.tiers.opus.model}`);
console.log(` Sonnet: ${variant.tiers.sonnet.provider} / ${variant.tiers.sonnet.model}`);
console.log(` Haiku: ${variant.tiers.haiku.provider} / ${variant.tiers.haiku.model}`);
}
} else {
console.log(` Provider: ${variant.provider}`);
}
if (variant.port) {
console.log(` Port: ${variant.port}`);
}
console.log(` Target: ${variant.target || 'claude'}`);
console.log(` Settings: ${variant.settings || '-'}`);
console.log('');
const confirmed =
parsedArgs.yes || (await InteractivePrompt.confirm('Delete this variant?', { default: false }));
if (!confirmed) {
console.log(info('Cancelled'));
process.exit(0);
}
const result = removeVariant(name);
if (!result.success) {
console.log(fail(`Failed to remove variant: ${result.error}`));
process.exit(1);
}
console.log(ok(`Variant removed: ${name}`));
console.log('');
}
export async function handleEdit(
args: string[],
backend: CLIProxyBackend = DEFAULT_BACKEND
): 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);
if (variantNames.length === 0) {
console.log(warn('No CLIProxy variants to edit'));
process.exit(0);
}
let name = parsedArgs.name;
if (!name) {
console.log(header(`Edit ${getBackendLabel(backend)} Variant`));
console.log('');
console.log('Available variants:');
variantNames.forEach((n, i) => {
const v = variants[n];
const label = v.type === 'composite' ? 'composite' : v.provider;
console.log(` ${i + 1}. ${n} (${label})`);
});
console.log('');
name = await InteractivePrompt.input('Variant name to edit', {
validate: (val) => {
if (!val) return 'Variant name is required';
if (!variantNames.includes(val)) return `Variant '${val}' not found`;
return null;
},
});
}
if (!variantNames.includes(name)) {
console.log(fail(`Variant '${name}' not found`));
console.log('');
console.log('Available variants:');
variantNames.forEach((n) => console.log(` - ${n}`));
process.exit(1);
}
const variant = variants[name];
// 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 });
let newProvider: CLIProxyProfileName | undefined = undefined;
if (changeProvider) {
const providerOptions = CLIPROXY_PROFILES.map((p) => ({
id: p,
label: p.charAt(0).toUpperCase() + p.slice(1),
}));
newProvider = (await InteractivePrompt.selectFromList(
'Select new provider:',
providerOptions
)) as CLIProxyProfileName;
}
const providerChanged = !!(newProvider && newProvider !== variant.provider);
if (providerChanged) {
console.log(info('Provider changed. Model selection is required.'));
}
const changeModel = providerChanged
? true
: await InteractivePrompt.confirm('Change model?', { default: false });
let newModel = variant.model || '';
if (changeModel) {
const providerForModel = newProvider || (variant.provider as CLIProxyProfileName);
if (supportsModelConfig(providerForModel as CLIProxyProvider)) {
const catalog = getProviderCatalog(providerForModel as CLIProxyProvider);
if (catalog) {
const modelOptions = catalog.models.map((m) => ({
id: m.id,
label: formatModelOption(m),
}));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
newModel = await InteractivePrompt.selectFromList('Select new model:', modelOptions, {
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
});
}
} else {
newModel = await InteractivePrompt.input('New model name', {
validate: (val) => (val ? null : 'Model is required'),
});
}
}
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
const { updateVariant } = await import('../../cliproxy/services/variant-service');
const result = updateVariant(name, {
provider: newProvider,
model: changeModel ? newModel : undefined,
target: newTarget,
});
if (!result.success) {
console.log(fail(`Failed to update variant: ${result.error}`));
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) {
console.log(fail('Invalid composite variant: missing tier configuration'));
process.exit(1);
}
console.log(info('Current tier configuration:'));
console.log(` Opus: ${variant.tiers.opus.provider} / ${variant.tiers.opus.model}`);
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');
const updatedTiers: Partial<Record<'opus' | 'sonnet' | 'haiku', CompositeTierConfig>> = {};
// Ask per-tier edits
for (const tierName of ['opus', 'sonnet', 'haiku'] as const) {
const shouldEdit = await InteractivePrompt.confirm(`Edit ${tierName} tier?`, {
default: false,
});
if (shouldEdit) {
const newConfig = await selectTierConfig(tierName, verbose);
if (!newConfig) {
console.log(fail('Edit cancelled'));
process.exit(0);
}
updatedTiers[tierName] = newConfig;
}
}
// Ask for default tier change
let newDefaultTier = variant.default_tier;
const changeDefault = await InteractivePrompt.confirm('Change default tier?', { default: false });
if (changeDefault) {
const finalTiers = {
opus: updatedTiers.opus ?? variant.tiers.opus,
sonnet: updatedTiers.sonnet ?? variant.tiers.sonnet,
haiku: updatedTiers.haiku ?? variant.tiers.haiku,
};
const tierOptions = [
{
id: 'opus' as const,
label: `Opus (${finalTiers.opus.provider}: ${finalTiers.opus.model})`,
},
{
id: 'sonnet' as const,
label: `Sonnet (${finalTiers.sonnet.provider}: ${finalTiers.sonnet.model})`,
},
{
id: 'haiku' as const,
label: `Haiku (${finalTiers.haiku.provider}: ${finalTiers.haiku.model})`,
},
];
newDefaultTier = (await InteractivePrompt.selectFromList(
'Default tier (ANTHROPIC_MODEL):',
tierOptions
)) 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) {
console.log(fail(`Failed to update composite variant: ${result.error}`));
process.exit(1);
}
console.log('');
const finalVariant = result.variant;
if (finalVariant && finalVariant.tiers) {
const tierSummary =
`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}\n` +
`Target: ${finalVariant.target || compositeCurrentTarget}`;
const portInfo = finalVariant.port ? `\nPort: ${finalVariant.port}` : '';
console.log(
infoBox(
`Variant: ${name} (composite)\n${tierSummary}${portInfo}\nConfig: ~/.ccs/config.yaml`,
'Composite Variant Updated'
)
);
} else {
console.log(ok(`Composite variant updated: ${name}`));
}
console.log('');
}