feat(cliproxy): implement --nickname flag for account management

- Add renameAccount function to account-manager.ts with validation
- Support --nickname <name> to rename current default account
- Support --nickname with --auth --add to set nickname during OAuth
- Add getDefaultAccount import for nickname operations
- Filter --nickname flag from Claude CLI args
This commit is contained in:
kaitranntt
2025-12-10 18:13:17 -05:00
parent 1dbd7229a5
commit 0d70708658
3 changed files with 93 additions and 9 deletions
+32
View File
@@ -304,6 +304,38 @@ export function removeAccount(provider: CLIProxyProvider, accountId: string): bo
return true;
}
/**
* Rename an account's nickname
*/
export function renameAccount(
provider: CLIProxyProvider,
accountId: string,
newNickname: string
): boolean {
const validationError = validateNickname(newNickname);
if (validationError) {
throw new Error(validationError);
}
const registry = loadAccountsRegistry();
const providerAccounts = registry.providers[provider];
if (!providerAccounts?.accounts[accountId]) {
return false;
}
// Check if nickname is already used by another account
for (const [id, account] of Object.entries(providerAccounts.accounts)) {
if (id !== accountId && account.nickname?.toLowerCase() === newNickname.toLowerCase()) {
throw new Error(`Nickname "${newNickname}" is already used by another account`);
}
}
providerAccounts.accounts[accountId].nickname = newNickname;
saveAccountsRegistry(registry);
return true;
}
/**
* Update last used timestamp for an account
*/
+16 -6
View File
@@ -435,10 +435,16 @@ export function clearAuth(provider: CLIProxyProvider): boolean {
*/
export async function triggerOAuth(
provider: CLIProxyProvider,
options: { verbose?: boolean; headless?: boolean; account?: string; add?: boolean } = {}
options: {
verbose?: boolean;
headless?: boolean;
account?: string;
add?: boolean;
nickname?: string;
} = {}
): Promise<AccountInfo | null> {
const oauthConfig = getOAuthConfig(provider);
const { verbose = false, add = false } = options;
const { verbose = false, add = false, nickname } = options;
// Auto-detect headless if not explicitly set
const headless = options.headless ?? isHeadlessEnvironment();
@@ -615,7 +621,7 @@ export async function triggerOAuth(
console.log('[OK] Authentication successful');
// Register the account in accounts registry
const account = registerAccountFromToken(provider, tokenDir);
const account = registerAccountFromToken(provider, tokenDir, nickname);
resolve(account);
} else {
if (!headless) spinner.fail('Authentication incomplete');
@@ -658,10 +664,14 @@ export async function triggerOAuth(
/**
* Register account from newly created token file
* Scans auth directory for new token and extracts email
* @param provider - The CLIProxy provider
* @param tokenDir - Directory containing token files
* @param nickname - Optional nickname (uses auto-generated from email if not provided)
*/
function registerAccountFromToken(
provider: CLIProxyProvider,
tokenDir: string
tokenDir: string,
nickname?: string
): AccountInfo | null {
try {
const files = fs.readdirSync(tokenDir);
@@ -692,8 +702,8 @@ function registerAccountFromToken(
const data = JSON.parse(content);
const email = data.email || undefined;
// Register the account with auto-generated nickname
return registerAccount(provider, newestFile, email, generateNickname(email));
// Register the account (use provided nickname or auto-generate from email)
return registerAccount(provider, newestFile, email, nickname || generateNickname(email));
} catch {
return null;
}
+45 -3
View File
@@ -33,6 +33,8 @@ import {
getProviderAccounts,
setDefaultAccount,
touchAccount,
renameAccount,
getDefaultAccount,
} from './account-manager';
/** Default executor configuration */
@@ -139,6 +141,13 @@ export async function execClaudeWithCLIProxy(
useAccount = args[useIdx + 1];
}
// Parse --nickname <name> flag
let setNickname: string | undefined;
const nicknameIdx = args.indexOf('--nickname');
if (nicknameIdx !== -1 && args[nicknameIdx + 1] && !args[nicknameIdx + 1].startsWith('-')) {
setNickname = args[nicknameIdx + 1];
}
// Handle --accounts: list accounts and exit
if (showAccounts) {
const accounts = getProviderAccounts(provider);
@@ -177,6 +186,29 @@ export async function execClaudeWithCLIProxy(
console.log(`[OK] Switched to account: ${account.nickname || account.email || account.id}`);
}
// Handle --nickname: rename account and exit (unless used with --auth --add)
if (setNickname && !addAccount) {
const defaultAccount = getDefaultAccount(provider);
if (!defaultAccount) {
console.error(`[X] No account found for ${providerConfig.displayName}`);
console.error(` Run "ccs ${provider} --auth" to add an account first`);
process.exit(1);
}
try {
const success = renameAccount(provider, defaultAccount.id, setNickname);
if (success) {
console.log(`[OK] Renamed account to: ${setNickname}`);
} else {
console.error(`[X] Failed to rename account`);
process.exit(1);
}
} catch (err) {
console.error(`[X] ${err instanceof Error ? err.message : 'Failed to rename account'}`);
process.exit(1);
}
process.exit(0);
}
// Handle --config: configure model selection and exit
// Pass customSettingsPath for CLIProxy variants to save to correct file
if (forceConfig && supportsModelConfig(provider)) {
@@ -206,6 +238,7 @@ export async function execClaudeWithCLIProxy(
verbose,
add: addAccount,
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
});
if (!authSuccess) {
throw new Error(`Authentication required for ${providerConfig.displayName}`);
@@ -314,12 +347,21 @@ export async function execClaudeWithCLIProxy(
log(`Claude env: ANTHROPIC_MODEL=${envVars.ANTHROPIC_MODEL}`);
// Filter out CCS-specific flags before passing to Claude CLI
const ccsFlags = ['--auth', '--headless', '--logout', '--config', '--add', '--accounts', '--use'];
const ccsFlags = [
'--auth',
'--headless',
'--logout',
'--config',
'--add',
'--accounts',
'--use',
'--nickname',
];
const claudeArgs = args.filter((arg, idx) => {
// Filter out CCS flags
if (ccsFlags.includes(arg)) return false;
// Filter out value after --use
if (args[idx - 1] === '--use') return false;
// Filter out value after --use or --nickname
if (args[idx - 1] === '--use' || args[idx - 1] === '--nickname') return false;
return true;
});