Skip to content

Commit d868dc4

Browse files
committed
feat(cliproxy): add multi-account support phases 02-03
Phase 02 - CLI Account Selector in Variant Wizard: - Add --account flag to cliproxy create command - Implement inline OAuth flow when no accounts exist - Add account selector for multiple accounts - Auto-select single account Phase 03 - Dashboard Quick Setup Wizard: - Create quick-setup-wizard.tsx component - Add step-by-step wizard: Provider → Auth → Account → Variant → Success - Add Quick Setup button to CLIProxy page
1 parent 493492f commit d868dc4

File tree

3 files changed

+490
-11
lines changed

3 files changed

+490
-11
lines changed

src/commands/cliproxy-command.ts

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
isCLIProxyInstalled,
2424
getCLIProxyPath,
2525
} from '../cliproxy';
26-
import { getAllAuthStatus, getOAuthConfig } from '../cliproxy/auth-handler';
26+
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
27+
import { getProviderAccounts } from '../cliproxy/account-manager';
2728
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
2829
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
2930
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
@@ -53,6 +54,7 @@ interface CliproxyProfileArgs {
5354
name?: string;
5455
provider?: CLIProxyProfileName;
5556
model?: string;
57+
account?: string;
5658
force?: boolean;
5759
yes?: boolean;
5860
}
@@ -70,6 +72,8 @@ function parseProfileArgs(args: string[]): CliproxyProfileArgs {
7072
result.provider = args[++i] as CLIProxyProfileName;
7173
} else if (arg === '--model' && args[i + 1]) {
7274
result.model = args[++i];
75+
} else if (arg === '--account' && args[i + 1]) {
76+
result.account = args[++i];
7377
} else if (arg === '--force') {
7478
result.force = true;
7579
} else if (arg === '--yes' || arg === '-y') {
@@ -136,7 +140,8 @@ function cliproxyVariantExists(name: string): boolean {
136140
function createCliproxySettingsFile(
137141
name: string,
138142
provider: CLIProxyProfileName,
139-
model: string
143+
model: string,
144+
_account?: string
140145
): string {
141146
const ccsDir = getCcsDir();
142147
const settingsPath = path.join(ccsDir, `${provider}-${name}.settings.json`);
@@ -170,7 +175,8 @@ function createCliproxySettingsFile(
170175
function addCliproxyVariant(
171176
name: string,
172177
provider: CLIProxyProfileName,
173-
settingsPath: string
178+
settingsPath: string,
179+
account?: string
174180
): void {
175181
const configPath = getConfigPath();
176182

@@ -189,10 +195,16 @@ function addCliproxyVariant(
189195

190196
// Use relative path with ~ for portability
191197
const relativePath = `~/.ccs/${path.basename(settingsPath)}`;
192-
config.cliproxy[name] = {
198+
199+
// Build variant config with optional account
200+
const variantConfig: { provider: string; settings: string; account?: string } = {
193201
provider,
194202
settings: relativePath,
195203
};
204+
if (account) {
205+
variantConfig.account = account;
206+
}
207+
config.cliproxy[name] = variantConfig;
196208

197209
// Write config atomically
198210
const tempPath = configPath + '.tmp';
@@ -290,6 +302,103 @@ async function handleCreate(args: string[]): Promise<void> {
290302
process.exit(1);
291303
}
292304

305+
// Step 2.5: Account selection
306+
let account = parsedArgs.account;
307+
const providerAccounts = getProviderAccounts(provider as CLIProxyProvider);
308+
309+
if (!account) {
310+
if (providerAccounts.length === 0) {
311+
// No accounts - prompt to authenticate first
312+
console.log('');
313+
console.log(warn(`No accounts authenticated for ${provider}`));
314+
console.log('');
315+
316+
const shouldAuth = await InteractivePrompt.confirm(`Authenticate with ${provider} now?`, {
317+
default: true,
318+
});
319+
320+
if (!shouldAuth) {
321+
console.log('');
322+
console.log(info('Run authentication first:'));
323+
console.log(` ${color(`ccs ${provider} --auth`, 'command')}`);
324+
process.exit(0);
325+
}
326+
327+
// Trigger OAuth inline
328+
console.log('');
329+
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
330+
add: true,
331+
verbose: args.includes('--verbose'),
332+
});
333+
334+
if (!newAccount) {
335+
console.log(fail('Authentication failed'));
336+
process.exit(1);
337+
}
338+
339+
account = newAccount.id;
340+
console.log('');
341+
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
342+
} else if (providerAccounts.length === 1) {
343+
// Single account - auto-select
344+
account = providerAccounts[0].id;
345+
} else {
346+
// Multiple accounts - show selector with "Add new" option
347+
const ADD_NEW_ID = '__add_new__';
348+
349+
const accountOptions = [
350+
...providerAccounts.map((acc) => ({
351+
id: acc.id,
352+
label: `${acc.email || acc.id}${acc.isDefault ? ' (default)' : ''}`,
353+
})),
354+
{
355+
id: ADD_NEW_ID,
356+
label: color('[+ Add new account...]', 'info'),
357+
},
358+
];
359+
360+
const defaultIdx = providerAccounts.findIndex((a) => a.isDefault);
361+
362+
const selectedAccount = await InteractivePrompt.selectFromList(
363+
'Select account:',
364+
accountOptions,
365+
{ defaultIndex: defaultIdx >= 0 ? defaultIdx : 0 }
366+
);
367+
368+
if (selectedAccount === ADD_NEW_ID) {
369+
// Add new account inline
370+
console.log('');
371+
const newAccount = await triggerOAuth(provider as CLIProxyProvider, {
372+
add: true,
373+
verbose: args.includes('--verbose'),
374+
});
375+
376+
if (!newAccount) {
377+
console.log(fail('Authentication failed'));
378+
process.exit(1);
379+
}
380+
381+
account = newAccount.id;
382+
console.log('');
383+
console.log(ok(`Authenticated as ${newAccount.email || newAccount.id}`));
384+
} else {
385+
account = selectedAccount;
386+
}
387+
}
388+
} else {
389+
// Validate provided account exists
390+
const exists = providerAccounts.find((a) => a.id === account);
391+
if (!exists) {
392+
console.log(fail(`Account '${account}' not found for ${provider}`));
393+
console.log('');
394+
console.log('Available accounts:');
395+
providerAccounts.forEach((a) => {
396+
console.log(` - ${a.email || a.id}${a.isDefault ? ' (default)' : ''}`);
397+
});
398+
process.exit(1);
399+
}
400+
}
401+
293402
// Step 3: Model selection
294403
let model = parsedArgs.model;
295404
if (!model) {
@@ -323,15 +432,16 @@ async function handleCreate(args: string[]): Promise<void> {
323432
console.log(info('Creating CLIProxy variant...'));
324433

325434
try {
326-
const settingsPath = createCliproxySettingsFile(name, provider, model);
327-
addCliproxyVariant(name, provider, settingsPath);
435+
const settingsPath = createCliproxySettingsFile(name, provider, model, account);
436+
addCliproxyVariant(name, provider, settingsPath, account);
328437

329438
console.log('');
330439
console.log(
331440
infoBox(
332441
`Variant: ${name}\n` +
333442
`Provider: ${provider}\n` +
334443
`Model: ${model}\n` +
444+
(account ? `Account: ${account}\n` : '') +
335445
`Settings: ~/.ccs/${path.basename(settingsPath)}`,
336446
'CLIProxy Variant Created'
337447
)
@@ -551,6 +661,7 @@ async function showHelp(): Promise<void> {
551661
const createOpts: [string, string][] = [
552662
['--provider <name>', 'Provider (gemini, codex, agy, qwen)'],
553663
['--model <model>', 'Model name'],
664+
['--account <id>', 'Account ID (email or default)'],
554665
['--force', 'Overwrite existing variant'],
555666
['--yes, -y', 'Skip confirmation prompts'],
556667
];

0 commit comments

Comments
 (0)