mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
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:
+17
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user