mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-23 18:24:54 +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;
|
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)
|
// Special case: copilot command (GitHub Copilot integration)
|
||||||
// Only route to command handler for known subcommands, otherwise treat as profile
|
// Only route to command handler for known subcommands, otherwise treat as profile
|
||||||
const COPILOT_SUBCOMMANDS = [
|
const COPILOT_SUBCOMMANDS = [
|
||||||
@@ -441,6 +448,16 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
if (recovered) {
|
if (recovered) {
|
||||||
recovery.showRecoveryHints();
|
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
|
// Detect profile
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
|
|
||||||
// Diagnostics
|
// Diagnostics
|
||||||
printSubSection('Diagnostics', [
|
printSubSection('Diagnostics', [
|
||||||
|
['ccs setup', 'First-time setup wizard'],
|
||||||
['ccs doctor', 'Run health check and diagnostics'],
|
['ccs doctor', 'Run health check and diagnostics'],
|
||||||
['ccs cleanup', 'Remove old CLIProxy logs'],
|
['ccs cleanup', 'Remove old CLIProxy logs'],
|
||||||
['ccs config', 'Open web configuration dashboard'],
|
['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