diff --git a/src/ccs.ts b/src/ccs.ts index 5d228202..d5957b6c 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -204,7 +204,7 @@ async function main(): Promise { // Special case: help command if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') { - handleHelpCommand(); + await handleHelpCommand(); return; } diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 370e0aa6..9f375f14 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -1,142 +1,146 @@ -import { colored } from '../utils/helpers'; +import { initUI, box, header, color, dim } from '../utils/ui'; + +// Version is read from VERSION file during build +const VERSION = '5.3.0'; + +/** + * Print a section with header and items + */ +function printSection(title: string, subtitle: string, items: [string, string][]): void { + // Header with optional subtitle + const headerText = subtitle ? `${title} ${dim(subtitle)}` : title; + console.log(header(headerText)); + + // Calculate max command length for alignment + const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length)); + + for (const [cmd, desc] of items) { + const paddedCmd = cmd.padEnd(maxCmdLen + 2); + console.log(` ${color(paddedCmd, 'command')} ${desc}`); + } + + console.log(''); +} /** * Display comprehensive help information for CCS (Claude Code Switch) */ -export function handleHelpCommand(): void { - console.log(colored('CCS (Claude Code Switch) - Profile switching for Claude CLI', 'bold')); - console.log(''); +export async function handleHelpCommand(): Promise { + // Initialize UI (if not already) + await initUI(); - console.log(colored('Usage:', 'cyan')); - console.log(` ${colored('ccs', 'yellow')} [profile] [claude-args...]`); - console.log(` ${colored('ccs', 'yellow')} [flags]`); - console.log(''); - - // ═══════════════════════════════════════════════════════════════════════════ - // SECTION 1: API KEY MODELS + PROFILE MANAGEMENT - // ═══════════════════════════════════════════════════════════════════════════ - console.log(colored('═══ API Key Profiles ═══', 'cyanBold')); - console.log(' Configure in ~/.ccs/*.settings.json'); - console.log(''); - console.log(` ${colored('ccs', 'yellow')} Use default Claude account`); - console.log(` ${colored('ccs glm', 'yellow')} GLM 4.6 (API key required)`); - console.log(` ${colored('ccs glmt', 'yellow')} GLM with thinking mode`); - console.log(` ${colored('ccs kimi', 'yellow')} Kimi for Coding (API key)`); - console.log(` ${colored('ccs profile create', 'yellow')} Create custom API profile`); - console.log(` ${colored('ccs profile remove', 'yellow')} Remove a profile`); - console.log(` ${colored('ccs profile list', 'yellow')} List all profiles`); - console.log(''); - - // ═══════════════════════════════════════════════════════════════════════════ - // SECTION 2: ACCOUNT MANAGEMENT - // ═══════════════════════════════════════════════════════════════════════════ - console.log(colored('═══ Account Management ═══', 'cyanBold')); - console.log(' Run multiple Claude accounts concurrently'); - console.log(''); + // Hero box with title console.log( - ` ${colored('ccs auth --help', 'yellow')} Show account management commands` - ); - console.log(` ${colored('ccs auth create ', 'yellow')} Create new account profile`); - console.log(` ${colored('ccs auth list', 'yellow')} List all account profiles`); - console.log(''); - - // ═══════════════════════════════════════════════════════════════════════════ - // SECTION 3: CLI PROXY (OAUTH PROVIDERS) - // ═══════════════════════════════════════════════════════════════════════════ - console.log(colored('═══ CLI Proxy (OAuth Providers) ═══', 'cyanBold')); - console.log(' Zero-config OAuth authentication via CLIProxyAPI'); - console.log(' First run: Browser opens for authentication'); - console.log(' Settings: ~/.ccs/{provider}.settings.json (created after auth)'); - console.log(''); - console.log( - ` ${colored('ccs gemini', 'yellow')} Google Gemini (gemini-2.5-pro)` - ); - console.log( - ` ${colored('ccs codex', 'yellow')} OpenAI Codex (gpt-5.1-codex-max)` - ); - console.log( - ` ${colored('ccs agy', 'yellow')} Antigravity (gemini-3-pro-preview)` - ); - console.log(` ${colored('ccs qwen', 'yellow')} Qwen Code (qwen3-coder)`); - console.log(''); - console.log(` ${colored('ccs --auth', 'yellow')} Authenticate only`); - console.log(` ${colored('ccs --logout', 'yellow')} Clear authentication`); - console.log(` ${colored('ccs --headless', 'yellow')} Headless auth (for SSH)`); - console.log(` ${colored('ccs codex "explain code"', 'yellow')} Use with prompt`); - console.log(''); - - // ═══════════════════════════════════════════════════════════════════════════ - // DELEGATION - // ═══════════════════════════════════════════════════════════════════════════ - console.log(colored('Delegation (inside Claude Code CLI):', 'cyan')); - console.log( - ` ${colored('/ccs "task"', 'yellow')} Delegate task (auto-selects profile)` - ); - console.log( - ` ${colored('/ccs --glm "task"', 'yellow')} Force GLM-4.6 for simple tasks` - ); - console.log(` ${colored('/ccs --kimi "task"', 'yellow')} Force Kimi for long context`); - console.log( - ` ${colored('/ccs:continue "follow-up"', 'yellow')} Continue last delegation session` + box(`CCS v${VERSION}\nClaude Code Profile & Model Switcher`, { + padding: 1, + borderStyle: 'round', + titleAlignment: 'center', + }) ); console.log(''); - console.log(colored('Diagnostics:', 'cyan')); - console.log( - ` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics` - ); - console.log( - ` ${colored('ccs sync', 'yellow')} Sync delegation commands and skills` - ); - console.log(` ${colored('ccs update', 'yellow')} Update CCS to latest version`); + // Usage section + console.log(header('USAGE')); + console.log(` $ ${color('ccs', 'command')} [flags] [-- claude-args...]`); + console.log(` $ ${color('ccs', 'command')} [flags]`); console.log(''); - console.log(colored('Flags:', 'cyan')); - console.log(` ${colored('-h, --help', 'yellow')} Show this help message`); - console.log( - ` ${colored('-v, --version', 'yellow')} Show version and installation info` - ); - console.log( - ` ${colored('-sc, --shell-completion', 'yellow')} Install shell auto-completion` - ); + // API Key Profiles section + printSection('API KEY PROFILES', 'Configure: ~/.ccs/*.settings.json', [ + ['ccs', 'Use default Claude account'], + ['ccs glm', 'GLM-4.6 via Zhipu AI'], + ['ccs glmt', 'GLM-4.6 (Turbo mode)'], + ['ccs kimi', 'Kimi via Moonshot AI'], + ]); + + // Profile management section + printSection('PROFILE MANAGEMENT', '', [ + ['ccs profile create', 'Create custom API profile'], + ['ccs profile list', 'List all profiles'], + ['ccs profile remove', 'Remove a profile'], + ]); + + // Account management section + printSection('ACCOUNT MANAGEMENT', 'Multiple Claude accounts', [ + ['ccs auth create ', 'Create new account'], + ['ccs auth list', 'List all accounts'], + ['ccs auth default ', 'Set default account'], + ]); + + // OAuth section + printSection('OAUTH PROVIDERS', 'Zero config, browser auth', [ + ['ccs gemini', 'Google Gemini (gemini-2.5-pro)'], + ['ccs codex', 'OpenAI Codex (gpt-5.1-codex-max)'], + ['ccs agy', 'Antigravity (gemini-3-pro-preview)'], + ['ccs qwen', 'Qwen Code (qwen3-coder)'], + ]); + + // OAuth flags + console.log(header('OAUTH FLAGS')); + console.log(` ${color('ccs --auth', 'command')} Authenticate only`); + console.log(` ${color('ccs --logout', 'command')} Clear authentication`); + console.log(` ${color('ccs --headless', 'command')} Headless auth (for SSH)`); console.log(''); - console.log(colored('Configuration:', 'cyan')); - console.log(' Config File: ~/.ccs/config.json'); - console.log(' Profiles: ~/.ccs/profiles.json'); - console.log(' Instances: ~/.ccs/instances/'); - console.log(' Settings: ~/.ccs/*.settings.json'); + // Delegation section + printSection('DELEGATION', 'Inside Claude Code CLI', [ + ['/ccs "task"', 'Delegate (auto-select profile)'], + ['/ccs --glm "task"', 'Force GLM-4.6'], + ['/ccs --kimi "task"', 'Force Kimi'], + ['/ccs:continue', 'Continue last delegation'], + ]); + + // Diagnostics section + printSection('DIAGNOSTICS', '', [ + ['ccs doctor', 'Run health check'], + ['ccs sync', 'Sync delegation commands'], + ['ccs update', 'Update to latest version'], + ]); + + // Flags section + console.log(header('FLAGS')); + console.log(` ${color('-h, --help', 'command')} Show this help`); + console.log(` ${color('-v, --version', 'command')} Show version`); + console.log(` ${color('-sc, --shell-completion', 'command')} Install shell completion`); console.log(''); - console.log(colored('CLI Proxy:', 'cyan')); - console.log(' Binary: ~/.ccs/cliproxy/bin/cli-proxy-api'); - console.log(' Config: ~/.ccs/cliproxy/config.yaml'); - console.log(' Auth: ~/.ccs/cliproxy/auth/'); - console.log(' Port: 8317 (default)'); + // Configuration paths + console.log(header('CONFIGURATION')); + console.log(` Config: ${color('~/.ccs/config.json', 'path')}`); + console.log(` Profiles: ${color('~/.ccs/profiles.json', 'path')}`); + console.log(` Settings: ${color('~/.ccs/*.settings.json', 'path')}`); + console.log(` CLIProxy: ${color('~/.ccs/cliproxy/', 'path')}`); console.log(''); - console.log(colored('Shared Data:', 'cyan')); - console.log(' Commands: ~/.ccs/shared/commands/'); - console.log(' Skills: ~/.ccs/shared/skills/'); - console.log(' Agents: ~/.ccs/shared/agents/'); - console.log(' Note: Symlinked across all profiles'); + // Shared Data + console.log(header('SHARED DATA')); + console.log(` Commands: ${color('~/.ccs/shared/commands/', 'path')}`); + console.log(` Skills: ${color('~/.ccs/shared/skills/', 'path')}`); + console.log(` Agents: ${color('~/.ccs/shared/agents/', 'path')}`); + console.log(` ${dim('Note: Symlinked across all profiles')}`); console.log(''); - console.log(colored('Examples:', 'cyan')); - console.log(` ${colored('$ ccs', 'yellow')} # Use default account`); - console.log( - ` ${colored('$ ccs gemini', 'yellow')} # OAuth (browser opens first time)` - ); - console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # API key model`); + // Examples + console.log(header('EXAMPLES')); + console.log(` ${dim('# Use default account')}`); + console.log(` $ ${color('ccs', 'command')}`); console.log(''); - console.log(` Docs: ${colored('https://github.com/kaitranntt/ccs', 'cyan')}`); + console.log(` ${dim('# OAuth provider (browser auth first time)')}`); + console.log(` $ ${color('ccs gemini', 'command')}`); + console.log(''); + console.log(` ${dim('# API key model with prompt')}`); + console.log(` $ ${color('ccs glm "implement the API"', 'command')}`); console.log(''); - console.log(colored('Uninstall:', 'yellow')); + // Footer + console.log(dim('Docs: https://github.com/kaitranntt/ccs')); + console.log(dim('License: MIT')); + console.log(''); + + // Uninstall + console.log(color('Uninstall:', 'warning')); console.log(' npm uninstall -g @kaitranntt/ccs'); console.log(''); - console.log(`${colored('License:', 'cyan')} MIT`); - process.exit(0); } diff --git a/src/commands/profile-command.ts b/src/commands/profile-command.ts index 0f09c50f..f2bf9962 100644 --- a/src/commands/profile-command.ts +++ b/src/commands/profile-command.ts @@ -7,7 +7,19 @@ import * as fs from 'fs'; import * as path from 'path'; -import { colored } from '../utils/helpers'; +import { + initUI, + header, + subheader, + color, + dim, + ok, + fail, + warn, + info, + table, + infoBox, +} from '../utils/ui'; import { InteractivePrompt } from '../utils/prompt'; import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager'; @@ -148,9 +160,10 @@ function updateConfig(name: string, _settingsPath: string): void { * Handle 'ccs profile create' command */ async function handleCreate(args: string[]): Promise { + await initUI(); const parsedArgs = parseArgs(args); - console.log(colored('Create API Profile', 'bold')); + console.log(header('Create API Profile')); console.log(''); // Step 1: Profile name @@ -162,15 +175,15 @@ async function handleCreate(args: string[]): Promise { } else { const error = validateProfileName(name); if (error) { - console.error(`[X] ${error}`); + console.log(fail(error)); process.exit(1); } } // Check if exists if (profileExists(name) && !parsedArgs.force) { - console.error(`[X] Profile '${name}' already exists`); - console.log(` Use ${colored('--force', 'yellow')} to overwrite`); + console.log(fail(`Profile '${name}' already exists`)); + console.log(` Use ${color('--force', 'command')} to overwrite`); process.exit(1); } @@ -183,7 +196,7 @@ async function handleCreate(args: string[]): Promise { } else { const error = validateUrl(baseUrl); if (error) { - console.error(`[X] ${error}`); + console.log(fail(error)); process.exit(1); } } @@ -193,7 +206,7 @@ async function handleCreate(args: string[]): Promise { if (!apiKey) { apiKey = await InteractivePrompt.password('API Key'); if (!apiKey) { - console.error('[X] API key is required'); + console.log(fail('API key is required')); process.exit(1); } } @@ -210,33 +223,57 @@ async function handleCreate(args: string[]): Promise { // Create files console.log(''); - console.log('[i] Creating profile...'); + console.log(info('Creating profile...')); try { const settingsPath = createSettingsFile(name, baseUrl, apiKey, model); updateConfig(name, settingsPath); - console.log(colored('[OK] Profile created successfully', 'green')); console.log(''); - console.log(` Profile: ${name}`); - console.log(` Settings: ~/.ccs/${name}.settings.json`); - console.log(` Base URL: ${baseUrl}`); - console.log(` Model: ${model}`); + console.log( + infoBox( + `Profile: ${name}\n` + + `Settings: ~/.ccs/${name}.settings.json\n` + + `Base URL: ${baseUrl}\n` + + `Model: ${model}`, + 'Profile Created' + ) + ); console.log(''); - console.log(colored('Usage:', 'cyan')); - console.log(` ${colored(`ccs ${name} "your prompt"`, 'yellow')}`); + console.log(header('Usage')); + console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`); console.log(''); } catch (error) { - console.error(`[X] Failed to create profile: ${(error as Error).message}`); + console.log(fail(`Failed to create profile: ${(error as Error).message}`)); process.exit(1); } } +/** + * Check if profile has real API key (not placeholder) + */ +function isProfileConfigured(profileName: string): boolean { + try { + const ccsDir = getCcsDir(); + const settingsPath = path.join(ccsDir, `${profileName}.settings.json`); + if (!fs.existsSync(settingsPath)) return false; + + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || ''; + // Check if it's a placeholder or empty + return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-'); + } catch { + return false; + } +} + /** * Handle 'ccs profile list' command */ async function handleList(): Promise { - console.log(colored('CCS Profiles', 'bold')); + await initUI(); + + console.log(header('CCS Profiles')); console.log(''); try { @@ -244,38 +281,52 @@ async function handleList(): Promise { const profiles = Object.keys(config.profiles); if (profiles.length === 0) { - console.log(colored('No profiles configured', 'yellow')); + console.log(warn('No profiles configured')); console.log(''); console.log('To create a profile:'); - console.log(` ${colored('ccs profile create', 'yellow')}`); + console.log(` ${color('ccs profile create', 'command')}`); console.log(''); return; } - console.log(colored('Settings-based Profiles:', 'cyan')); - // Calculate max name length for alignment - const maxNameLen = Math.max(...profiles.map((n) => n.length)); - profiles.forEach((name) => { + // Build table data with status indicators + const rows: string[][] = profiles.map((name) => { const settingsPath = config.profiles[name]; - const paddedName = name.padEnd(maxNameLen); - console.log(` ${colored(paddedName, 'yellow')} ${settingsPath}`); + const status = isProfileConfigured(name) ? color('[OK]', 'success') : color('[!]', 'warning'); + + return [name, settingsPath, status]; }); + + // Print table + console.log( + table(rows, { + head: ['Profile', 'Settings File', 'Status'], + colWidths: [15, 35, 10], + }) + ); console.log(''); - // Also show CLIProxy profiles if any + // Show CLIProxy variants if any if (config.cliproxy && Object.keys(config.cliproxy).length > 0) { - console.log(colored('CLIProxy Variants:', 'cyan')); - Object.entries(config.cliproxy).forEach(([name, variant]) => { - const v = variant as { provider: string; settings: string }; - console.log(` ${colored(name, 'yellow')} → ${v.provider} (${v.settings})`); + console.log(subheader('CLIProxy Variants')); + const cliproxyRows = Object.entries(config.cliproxy).map(([name, v]) => { + const variant = v as { provider: string; settings: string }; + return [name, variant.provider, variant.settings]; }); + + console.log( + table(cliproxyRows, { + head: ['Variant', 'Provider', 'Settings'], + colWidths: [15, 15, 30], + }) + ); console.log(''); } - console.log(`Total: ${profiles.length} profile(s)`); + console.log(dim(`Total: ${profiles.length} profile(s)`)); console.log(''); } catch (error) { - console.error(`[X] Failed to list profiles: ${(error as Error).message}`); + console.log(fail(`Failed to list profiles: ${(error as Error).message}`)); process.exit(1); } } @@ -284,6 +335,7 @@ async function handleList(): Promise { * Handle 'ccs profile remove' command */ async function handleRemove(args: string[]): Promise { + await initUI(); const parsedArgs = parseArgs(args); // Load config first to get available profiles @@ -291,20 +343,20 @@ async function handleRemove(args: string[]): Promise { try { config = loadConfig(); } catch { - console.error('[X] Failed to load config'); + console.log(fail('Failed to load config')); process.exit(1); } const profiles = Object.keys(config.profiles); if (profiles.length === 0) { - console.log(colored('No profiles to remove', 'yellow')); + console.log(warn('No profiles to remove')); process.exit(0); } // Interactive profile selection if not provided let name = parsedArgs.name; if (!name) { - console.log(colored('Remove Profile', 'bold')); + console.log(header('Remove Profile')); console.log(''); console.log('Available profiles:'); profiles.forEach((p, i) => console.log(` ${i + 1}. ${p}`)); @@ -320,7 +372,7 @@ async function handleRemove(args: string[]): Promise { } if (!(name in config.profiles)) { - console.error(`[X] Profile '${name}' not found`); + console.log(fail(`Profile '${name}' not found`)); console.log(''); console.log('Available profiles:'); profiles.forEach((p) => console.log(` - ${p}`)); @@ -332,7 +384,7 @@ async function handleRemove(args: string[]): Promise { // Confirm deletion console.log(''); - console.log(`Profile '${colored(name, 'yellow')}' will be removed.`); + console.log(`Profile '${color(name, 'command')}' will be removed.`); console.log(` Settings: ${settingsPath}`); console.log(''); @@ -340,7 +392,7 @@ async function handleRemove(args: string[]): Promise { parsedArgs.yes || (await InteractivePrompt.confirm('Delete this profile?', { default: false })); if (!confirmed) { - console.log('[i] Cancelled'); + console.log(info('Cancelled')); process.exit(0); } @@ -356,37 +408,45 @@ async function handleRemove(args: string[]): Promise { fs.unlinkSync(expandedPath); } - console.log(colored('[OK] Profile removed', 'green')); - console.log(` Profile: ${name}`); + console.log(ok(`Profile removed: ${name}`)); console.log(''); } /** * Show help for profile commands */ -function showHelp(): void { - console.log(colored('CCS Profile Management', 'bold')); +async function showHelp(): Promise { + await initUI(); + + console.log(header('CCS Profile Management')); console.log(''); - console.log(colored('Usage:', 'cyan')); - console.log(` ${colored('ccs profile', 'yellow')} [options]`); + console.log(subheader('Usage')); + console.log(` ${color('ccs profile', 'command')} [options]`); console.log(''); - console.log(colored('Commands:', 'cyan')); - console.log(` ${colored('create [name]', 'yellow')} Create new API profile (interactive)`); - console.log(` ${colored('list', 'yellow')} List all profiles`); - console.log(` ${colored('remove ', 'yellow')} Remove a profile`); + console.log(subheader('Commands')); + console.log(` ${color('create [name]', 'command')} Create new API profile (interactive)`); + console.log(` ${color('list', 'command')} List all profiles`); + console.log(` ${color('remove ', 'command')} Remove a profile`); console.log(''); - console.log(colored('Options:', 'cyan')); - console.log(` ${colored('--base-url ', 'yellow')} API base URL (create)`); - console.log(` ${colored('--api-key ', 'yellow')} API key (create)`); - console.log(` ${colored('--model ', 'yellow')} Default model (create)`); - console.log(` ${colored('--force', 'yellow')} Overwrite existing (create)`); - console.log(` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts`); + console.log(subheader('Options')); + console.log(` ${color('--base-url ', 'command')} API base URL (create)`); + console.log(` ${color('--api-key ', 'command')} API key (create)`); + console.log(` ${color('--model ', 'command')} Default model (create)`); + console.log(` ${color('--force', 'command')} Overwrite existing (create)`); + console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts`); console.log(''); - console.log(colored('Examples:', 'cyan')); - console.log(` ${colored('ccs profile create', 'yellow')} # Interactive wizard`); - console.log(` ${colored('ccs profile create myapi', 'yellow')} # Create with name`); - console.log(` ${colored('ccs profile remove myapi', 'yellow')} # Remove profile`); - console.log(` ${colored('ccs profile list', 'yellow')} # Show all profiles`); + console.log(subheader('Examples')); + console.log(` ${dim('# Interactive wizard')}`); + console.log(` ${color('ccs profile create', 'command')}`); + console.log(''); + console.log(` ${dim('# Create with name')}`); + console.log(` ${color('ccs profile create myapi', 'command')}`); + console.log(''); + console.log(` ${dim('# Remove profile')}`); + console.log(` ${color('ccs profile remove myapi', 'command')}`); + console.log(''); + console.log(` ${dim('# Show all profiles')}`); + console.log(` ${color('ccs profile list', 'command')}`); console.log(''); } @@ -397,7 +457,7 @@ export async function handleProfileCommand(args: string[]): Promise { const command = args[0]; if (!command || command === '--help' || command === '-h' || command === 'help') { - showHelp(); + await showHelp(); return; } @@ -414,10 +474,11 @@ export async function handleProfileCommand(args: string[]): Promise { await handleRemove(args.slice(1)); break; default: - console.error(`[X] Unknown command: ${command}`); + await initUI(); + console.log(fail(`Unknown command: ${command}`)); console.log(''); console.log('Run for help:'); - console.log(` ${colored('ccs profile --help', 'yellow')}`); + console.log(` ${color('ccs profile --help', 'command')}`); process.exit(1); } }