From cec616d530d9cf61a3a45032465b01e9a4037558 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 23 Dec 2025 21:28:38 -0500 Subject: [PATCH] feat(setup): add first-time setup wizard for config initialization Addresses Issue #142 - remote CLIProxyAPI configuration for Docker/server deployments. Changes: - Add `ccs setup` command with interactive wizard - Support local vs remote CLIProxy mode selection - Guide users through remote proxy config (host, port, auth token) - Auto-detect first-time install and suggest setup wizard - Update help command to include setup Scenarios covered: 1. New users - wizard guides through local/remote/skip choice 2. Remote proxy users - configure host/port/protocol/auth_token 3. Local users - default auto-start behavior 4. Skip CLIProxy - only API profiles or Claude accounts --- src/ccs.ts | 17 ++ src/commands/help-command.ts | 1 + src/commands/setup-command.ts | 355 ++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 src/commands/setup-command.ts diff --git a/src/ccs.ts b/src/ccs.ts index f347819f..25dd7171 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -404,6 +404,13 @@ async function main(): Promise { return; } + // Special case: setup command (first-time wizard) + if (firstArg === 'setup' || firstArg === '--setup') { + const { handleSetupCommand } = await import('./commands/setup-command'); + await handleSetupCommand(args.slice(1)); + return; + } + // Special case: copilot command (GitHub Copilot integration) // Only route to command handler for known subcommands, otherwise treat as profile const COPILOT_SUBCOMMANDS = [ @@ -441,6 +448,16 @@ async function main(): Promise { if (recovered) { recovery.showRecoveryHints(); + + // First-time install: offer setup wizard for interactive users + // Skip if headless, CI, or non-TTY environment + const { isFirstTimeInstall } = await import('./commands/setup-command'); + if (process.stdout.isTTY && !process.env['CI'] && isFirstTimeInstall()) { + console.log(''); + console.log(info('First-time install detected. Run `ccs setup` for guided configuration.')); + console.log(' Or use `ccs config` for the web dashboard.'); + console.log(''); + } } // Detect profile diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 6261dc2a..224230ae 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -213,6 +213,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); // Diagnostics printSubSection('Diagnostics', [ + ['ccs setup', 'First-time setup wizard'], ['ccs doctor', 'Run health check and diagnostics'], ['ccs cleanup', 'Remove old CLIProxy logs'], ['ccs config', 'Open web configuration dashboard'], diff --git a/src/commands/setup-command.ts b/src/commands/setup-command.ts new file mode 100644 index 00000000..ec990812 --- /dev/null +++ b/src/commands/setup-command.ts @@ -0,0 +1,355 @@ +/** + * Setup Command Handler + * + * Interactive first-time setup wizard for CCS. + * Guides users through initial configuration including: + * - Local vs Remote CLIProxy mode selection + * - Remote proxy configuration (host, port, auth token) + * - Default profile selection + * + * Usage: ccs setup + * + * Related: Issue #142 - remote CLIProxyAPI configuration + */ + +import * as readline from 'readline'; +import { initUI, header, ok, info, warn } from '../utils/ui'; +import { + loadOrCreateUnifiedConfig, + saveUnifiedConfig, + hasUnifiedConfig, +} from '../config/unified-config-loader'; +import { DEFAULT_CLIPROXY_SERVER_CONFIG } from '../config/unified-config-types'; + +/** + * Create readline interface for interactive prompts + */ +function createReadline(): readline.Interface { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); +} + +/** + * Prompt user for input with optional default value + */ +async function prompt( + rl: readline.Interface, + question: string, + defaultValue?: string +): Promise { + return new Promise((resolve) => { + const displayQuestion = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `; + rl.question(displayQuestion, (answer) => { + resolve(answer.trim() || defaultValue || ''); + }); + }); +} + +/** + * Prompt user for yes/no confirmation + */ +async function confirm( + rl: readline.Interface, + question: string, + defaultYes: boolean = true +): Promise { + const hint = defaultYes ? '[Y/n]' : '[y/N]'; + const answer = await prompt(rl, `${question} ${hint}`); + + if (answer === '') return defaultYes; + return answer.toLowerCase().startsWith('y'); +} + +/** + * Prompt user to select from numbered options + */ +async function selectOption( + rl: readline.Interface, + question: string, + options: { label: string; value: string; description?: string }[] +): Promise { + console.log(''); + console.log(question); + console.log(''); + + options.forEach((opt, idx) => { + const desc = opt.description ? ` - ${opt.description}` : ''; + console.log(` ${idx + 1}) ${opt.label}${desc}`); + }); + + console.log(''); + const answer = await prompt(rl, 'Enter choice (number)', '1'); + const idx = parseInt(answer, 10) - 1; + + if (idx >= 0 && idx < options.length) { + return options[idx].value; + } + + // Invalid selection, default to first + console.log(warn(`Invalid selection, using default: ${options[0].label}`)); + return options[0].value; +} + +/** + * Check if this is a first-time install (no config exists) + */ +export function isFirstTimeInstall(): boolean { + return !hasUnifiedConfig(); +} + +/** + * Configure remote CLIProxy settings interactively + */ +async function configureRemoteProxy(rl: readline.Interface): Promise<{ + host: string; + port?: number; + protocol: 'http' | 'https'; + authToken: string; +}> { + console.log(''); + console.log(info('Configure Remote CLIProxyAPI Connection')); + console.log(''); + console.log(' Enter the details for your remote CLIProxyAPI server.'); + console.log(' Example: your-server.example.com'); + console.log(''); + + // Host + const host = await prompt(rl, 'Remote host (hostname or IP)'); + if (!host) { + throw new Error('Host is required for remote proxy mode'); + } + + // Protocol + const protocol = (await selectOption(rl, 'Protocol:', [ + { label: 'HTTPS', value: 'https', description: 'Secure connection (recommended)' }, + { label: 'HTTP', value: 'http', description: 'Unencrypted connection' }, + ])) as 'http' | 'https'; + + // Port (optional) + const defaultPort = protocol === 'https' ? '443' : '80'; + const portStr = await prompt(rl, `Port (leave empty for default ${defaultPort})`); + const port = portStr ? parseInt(portStr, 10) : undefined; + + // Auth token + console.log(''); + console.log(info('Authentication')); + console.log(' The auth token is configured in your CLIProxyAPI config.yaml'); + console.log(' under api-keys section. Example: "ccs-internal-managed"'); + console.log(''); + + const authToken = await prompt(rl, 'Auth token', 'ccs-internal-managed'); + + return { host, port, protocol, authToken }; +} + +/** + * Main setup wizard + */ +async function runSetupWizard(force: boolean = false): Promise { + const rl = createReadline(); + + try { + console.log(''); + console.log(header('CCS First-Time Setup')); + console.log(''); + + // Check if already configured + if (!force && !isFirstTimeInstall()) { + console.log(info('CCS is already configured.')); + console.log(' Use --force to reconfigure, or run `ccs config` for the dashboard.'); + console.log(''); + rl.close(); + return; + } + + console.log('Welcome to CCS (Claude Code Switch)!'); + console.log('This wizard will help you configure CCS for first-time use.'); + console.log(''); + + // Step 1: Local vs Remote mode + const proxyMode = await selectOption( + rl, + 'How do you want to use CLIProxy providers (gemini, codex, agy)?', + [ + { + label: 'Local (Recommended)', + value: 'local', + description: 'CCS auto-starts CLIProxyAPI binary on your machine', + }, + { + label: 'Remote Server', + value: 'remote', + description: 'Connect to a remote CLIProxyAPI instance (Issue #142)', + }, + { + label: 'Skip CLIProxy', + value: 'skip', + description: 'Only use API profiles (GLM, Kimi) or Claude accounts', + }, + ] + ); + + // Load or create config + const config = loadOrCreateUnifiedConfig(); + + if (proxyMode === 'remote') { + // Configure remote proxy + const remoteConfig = await configureRemoteProxy(rl); + + config.cliproxy_server = { + remote: { + enabled: true, + host: remoteConfig.host, + port: remoteConfig.port, + protocol: remoteConfig.protocol, + auth_token: remoteConfig.authToken, + }, + fallback: { + enabled: true, + auto_start: false, + }, + local: { + port: 8317, + auto_start: false, // Disable local auto-start when using remote + }, + }; + + console.log(''); + console.log(ok('Remote proxy configured successfully!')); + console.log(''); + console.log( + ` URL: ${remoteConfig.protocol}://${remoteConfig.host}${remoteConfig.port ? `:${remoteConfig.port}` : ''}` + ); + console.log(` Auth: ${remoteConfig.authToken ? '[configured]' : '[none]'}`); + } else if (proxyMode === 'local') { + // Ensure local mode is configured + config.cliproxy_server = { + ...DEFAULT_CLIPROXY_SERVER_CONFIG, + remote: { + enabled: false, + host: '', + protocol: 'http', + auth_token: '', + }, + local: { + port: 8317, + auto_start: true, + }, + }; + + console.log(''); + console.log(ok('Local proxy mode configured!')); + console.log(' CLIProxyAPI will auto-start when you use gemini/codex/agy profiles.'); + } else { + // Skip CLIProxy - just use local config + console.log(''); + console.log(ok('CLIProxy skipped.')); + console.log(' You can still use API profiles (GLM, Kimi) or Claude accounts.'); + } + + // Step 2: Ask about API profiles + console.log(''); + const wantsApiProfile = await confirm( + rl, + 'Do you want to set up an API profile (GLM, Kimi, custom)?', + false + ); + + if (wantsApiProfile) { + console.log(''); + console.log(info('Creating API profiles...')); + console.log(' Use the following commands to create profiles:'); + console.log(''); + console.log(' ccs api create glm --preset glm'); + console.log(' ccs api create kimi --preset kimi'); + console.log(' ccs api create custom --prompt'); + console.log(''); + console.log(' After creating, edit the settings file to add your API key.'); + } + + // Save config + saveUnifiedConfig(config); + + // Final summary + console.log(''); + console.log(header('Setup Complete!')); + console.log(''); + console.log('Quick start commands:'); + console.log(''); + + if (proxyMode !== 'skip') { + console.log(' ccs gemini # Use Gemini via CLIProxy (OAuth)'); + console.log(' ccs codex # Use Codex via CLIProxy (OAuth)'); + console.log(' ccs agy # Use Antigravity via CLIProxy (OAuth)'); + } + + console.log(' ccs # Use default Claude CLI'); + console.log(' ccs config # Open web dashboard'); + console.log(' ccs doctor # Check configuration health'); + console.log(''); + + if (proxyMode === 'remote') { + console.log(info('Remote proxy tip:')); + console.log(' If connection fails, CCS will offer to start local proxy as fallback.'); + console.log(' Edit ~/.ccs/config.yaml to adjust remote settings.'); + console.log(''); + } + + console.log(info('Configuration saved to: ~/.ccs/config.yaml')); + console.log(''); + } finally { + rl.close(); + } +} + +/** + * Parse command line arguments + */ +function parseArgs(args: string[]): { force: boolean; help: boolean } { + return { + force: args.includes('--force') || args.includes('-f'), + help: args.includes('--help') || args.includes('-h'), + }; +} + +/** + * Show help message + */ +function showHelp(): void { + console.log(''); + console.log('Usage: ccs setup [options]'); + console.log(''); + console.log('Interactive first-time setup wizard for CCS.'); + console.log(''); + console.log('Options:'); + console.log(' --force, -f Force setup even if already configured'); + console.log(' --help, -h Show this help message'); + console.log(''); + console.log('This wizard helps you configure:'); + console.log(' - Local vs Remote CLIProxy mode'); + console.log(' - Remote proxy connection (host, port, auth token)'); + console.log(' - API profile creation'); + console.log(''); + console.log('Examples:'); + console.log(' ccs setup Run setup wizard'); + console.log(' ccs setup --force Force reconfiguration'); + console.log(''); +} + +/** + * Handle setup command + */ +export async function handleSetupCommand(args: string[]): Promise { + await initUI(); + + const options = parseArgs(args); + + if (options.help) { + showHelp(); + return; + } + + await runSetupWizard(options.force); +}