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
This commit is contained in:
kaitranntt
2025-12-23 21:28:38 -05:00
parent 9d8268b0f6
commit cec616d530
3 changed files with 373 additions and 0 deletions
+17
View File
@@ -404,6 +404,13 @@ async function main(): Promise<void> {
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<void> {
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
+1
View File
@@ -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'],
+355
View File
@@ -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<string> {
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<boolean> {
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<string> {
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<void> {
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<void> {
await initUI();
const options = parseArgs(args);
if (options.help) {
showHelp();
return;
}
await runSetupWizard(options.force);
}