diff --git a/src/ccs.ts b/src/ccs.ts index d5957b6c..5ed864e8 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -284,7 +284,7 @@ async function main(): Promise { // Detect Claude CLI first (needed for all paths) const claudeCli = detectClaudeCli(); if (!claudeCli) { - ErrorManager.showClaudeNotFound(); + await ErrorManager.showClaudeNotFound(); process.exit(1); } @@ -341,7 +341,7 @@ async function main(): Promise { // Check if this is a profile not found error with suggestions if (err.profileName && err.availableProfiles !== undefined) { const allProfiles = err.availableProfiles.split('\n'); - ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions); + await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions); } else { console.error(`[X] ${err.message}`); } diff --git a/src/management/doctor.ts b/src/management/doctor.ts index 17bb0366..ae9a98d4 100644 --- a/src/management/doctor.ts +++ b/src/management/doctor.ts @@ -6,8 +6,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { spawn } from 'child_process'; -import { colored } from '../utils/helpers'; import { detectClaudeCli } from '../utils/claude-detector'; +import { initUI, header, box, table, color, ok, fail, warn, info } from '../utils/ui'; import packageJson from '../../package.json'; import { isCLIProxyInstalled, @@ -50,9 +50,6 @@ try { }; } -// Import cli-table3 -const Table = require('cli-table3'); - interface HealthCheckDetails { status: 'OK' | 'ERROR' | 'WARN'; info: string; @@ -133,40 +130,43 @@ class Doctor { * Run all health checks */ async runAllChecks(): Promise { - console.log(colored('Running CCS Health Check...', 'cyan')); + await initUI(); + + // Hero header box + console.log(box(`CCS Health Check v${this.ccsVersion}`, { borderStyle: 'round', padding: 0 })); console.log(''); // Store CCS version in details this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` }; // Group 1: System - console.log(colored('System:', 'bold')); + console.log(header('SYSTEM')); await this.checkClaudeCli(); this.checkCcsDirectory(); console.log(''); // Group 2: Configuration - console.log(colored('Configuration:', 'bold')); + console.log(header('CONFIGURATION')); this.checkConfigFiles(); this.checkClaudeSettings(); console.log(''); // Group 3: Profiles & Delegation - console.log(colored('Profiles & Delegation:', 'bold')); + console.log(header('PROFILES & DELEGATION')); this.checkProfiles(); this.checkInstances(); this.checkDelegation(); console.log(''); // Group 4: System Health - console.log(colored('System Health:', 'bold')); + console.log(header('SYSTEM HEALTH')); this.checkPermissions(); this.checkCcsSymlinks(); this.checkSettingsSymlinks(); console.log(''); // Group 5: CLIProxy (OAuth profiles) - console.log(colored('CLIProxy (OAuth Profiles):', 'bold')); + console.log(header('CLIPROXY (OAUTH PROFILES)')); await this.checkCLIProxy(); console.log(''); @@ -183,7 +183,8 @@ class Doctor { const claudeCli = detectClaudeCli(); if (!claudeCli) { - spinner.fail(` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found in PATH`); + spinner.fail(); + console.log(` ${fail('Claude CLI'.padEnd(22))} Not found in PATH`); this.results.addCheck( 'Claude CLI', 'error', @@ -218,17 +219,15 @@ class Doctor { const versionMatch = result.match(/(\d+\.\d+\.\d+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; - spinner.succeed( - ` ${'Claude CLI'.padEnd(26)}${colored('[OK]', 'green')} ${claudeCli} (v${version})` - ); + spinner.succeed(); + console.log(` ${ok('Claude CLI'.padEnd(22))} v${version} (${claudeCli})`); this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, undefined, { status: 'OK', info: `v${version} (${claudeCli})`, }); } catch (_err) { - spinner.fail( - ` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found or not working` - ); + spinner.fail(); + console.log(` ${fail('Claude CLI'.padEnd(22))} Not found or not working`); this.results.addCheck( 'Claude CLI', 'error', @@ -246,13 +245,15 @@ class Doctor { const spinner = ora('Checking ~/.ccs/ directory').start(); if (fs.existsSync(this.ccsDir)) { - spinner.succeed(` ${'CCS Directory'.padEnd(26)}${colored('[OK]', 'green')} ~/.ccs/`); + spinner.succeed(); + console.log(` ${ok('CCS Directory'.padEnd(22))} ~/.ccs/`); this.results.addCheck('CCS Directory', 'success', undefined, undefined, { status: 'OK', info: '~/.ccs/', }); } else { - spinner.fail(` ${'CCS Directory'.padEnd(26)}${colored('[X]', 'red')} Not found`); + spinner.fail(); + console.log(` ${fail('CCS Directory'.padEnd(22))} Not found`); this.results.addCheck( 'CCS Directory', 'error', @@ -289,7 +290,8 @@ class Doctor { const spinner = ora(`Checking ${file.name}`).start(); if (!fs.existsSync(file.path)) { - spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Not found`); + spinner.fail(); + console.log(` ${fail(file.name.padEnd(22))} Not found`); this.results.addCheck( file.name, 'error', @@ -306,7 +308,7 @@ class Doctor { JSON.parse(content); // Extract useful info based on file type - let info = 'Valid'; + let fileInfo = 'Valid'; let status: 'OK' | 'WARN' = 'OK'; if (file.profile) { @@ -314,23 +316,23 @@ class Doctor { const validation = DelegationValidator.validate(file.profile); if (validation.valid) { - info = 'Key configured'; + fileInfo = 'Key configured'; status = 'OK'; } else if (validation.error && validation.error.includes('placeholder')) { - info = 'Placeholder key (not configured)'; + fileInfo = 'Placeholder key'; status = 'WARN'; } else { - info = 'Valid JSON'; + fileInfo = 'Valid JSON'; status = 'OK'; } } - const statusIcon = status === 'OK' ? colored('[OK]', 'green') : colored('[!]', 'yellow'); - if (status === 'WARN') { - spinner.warn(` ${file.name.padEnd(26)}${statusIcon} ${info}`); + spinner.warn(); + console.log(` ${warn(file.name.padEnd(22))} ${fileInfo}`); } else { - spinner.succeed(` ${file.name.padEnd(26)}${statusIcon} ${info}`); + spinner.succeed(); + console.log(` ${ok(file.name.padEnd(22))} ${fileInfo}`); } this.results.addCheck( @@ -340,11 +342,12 @@ class Doctor { undefined, { status: status, - info: info, + info: fileInfo, } ); } catch (e) { - spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Invalid JSON`); + spinner.fail(); + console.log(` ${fail(file.name.padEnd(22))} Invalid JSON`); this.results.addCheck( file.name, 'error', @@ -362,11 +365,11 @@ class Doctor { private checkClaudeSettings(): void { const spinner = ora('Checking ~/.claude/settings.json').start(); const settingsPath = path.join(this.claudeDir, 'settings.json'); + const settingsName = '~/.claude/settings.json'; if (!fs.existsSync(settingsPath)) { - spinner.warn( - ` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Not found` - ); + spinner.warn(); + console.log(` ${warn(settingsName.padEnd(22))} Not found`); this.results.addCheck( 'Claude Settings', 'warning', @@ -380,12 +383,12 @@ class Doctor { try { const content = fs.readFileSync(settingsPath, 'utf8'); JSON.parse(content); - spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`); + spinner.succeed(); + console.log(` ${ok(settingsName.padEnd(22))} Valid`); this.results.addCheck('Claude Settings', 'success'); } catch (e) { - spinner.warn( - ` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Invalid JSON` - ); + spinner.warn(); + console.log(` ${warn(settingsName.padEnd(22))} Invalid JSON`); this.results.addCheck( 'Claude Settings', 'warning', @@ -403,7 +406,8 @@ class Doctor { const configPath = path.join(this.ccsDir, 'config.json'); if (!fs.existsSync(configPath)) { - spinner.info(` ${'Profiles'.padEnd(26)}${colored('[SKIP]', 'cyan')} config.json not found`); + spinner.info(); + console.log(` ${info('Profiles'.padEnd(22))} config.json not found`); return; } @@ -411,7 +415,8 @@ class Doctor { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (!config.profiles || typeof config.profiles !== 'object') { - spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} Missing profiles object`); + spinner.fail(); + console.log(` ${fail('Profiles'.padEnd(22))} Missing profiles object`); this.results.addCheck( 'Profiles', 'error', @@ -425,9 +430,8 @@ class Doctor { const profileCount = Object.keys(config.profiles).length; const profileNames = Object.keys(config.profiles).join(', '); - spinner.succeed( - ` ${'Profiles'.padEnd(26)}${colored('[OK]', 'green')} ${profileCount} configured (${profileNames})` - ); + spinner.succeed(); + console.log(` ${ok('Profiles'.padEnd(22))} ${profileCount} configured (${profileNames})`); this.results.addCheck( 'Profiles', 'success', @@ -439,7 +443,8 @@ class Doctor { } ); } catch (e) { - spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} ${(e as Error).message}`); + spinner.fail(); + console.log(` ${fail('Profiles'.padEnd(22))} ${(e as Error).message}`); this.results.addCheck('Profiles', 'error', (e as Error).message, undefined, { status: 'ERROR', info: (e as Error).message, @@ -455,7 +460,8 @@ class Doctor { const instancesDir = path.join(this.ccsDir, 'instances'); if (!fs.existsSync(instancesDir)) { - spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`); + spinner.info(); + console.log(` ${info('Instances'.padEnd(22))} No account profiles`); this.results.addCheck('Instances', 'success', 'No account profiles configured'); return; } @@ -465,14 +471,14 @@ class Doctor { }); if (instances.length === 0) { - spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`); + spinner.info(); + console.log(` ${info('Instances'.padEnd(22))} No account profiles`); this.results.addCheck('Instances', 'success', 'No account profiles'); return; } - spinner.succeed( - ` ${'Instances'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} account profiles` - ); + spinner.succeed(); + console.log(` ${ok('Instances'.padEnd(22))} ${instances.length} account profiles`); this.results.addCheck('Instances', 'success', `${instances.length} account profiles`); } @@ -488,7 +494,8 @@ class Doctor { const hasContinueCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'ccs', 'continue.md')); if (!hasCcsCommand || !hasContinueCommand) { - spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} Not installed`); + spinner.warn(); + console.log(` ${warn('Delegation'.padEnd(22))} Not installed`); this.results.addCheck( 'Delegation', 'warning', @@ -511,7 +518,8 @@ class Doctor { } if (readyProfiles.length === 0) { - spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} No profiles ready`); + spinner.warn(); + console.log(` ${warn('Delegation'.padEnd(22))} No profiles ready`); this.results.addCheck( 'Delegation', 'warning', @@ -522,8 +530,9 @@ class Doctor { return; } - spinner.succeed( - ` ${'Delegation'.padEnd(26)}${colored('[OK]', 'green')} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})` + spinner.succeed(); + console.log( + ` ${ok('Delegation'.padEnd(22))} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})` ); this.results.addCheck( 'Delegation', @@ -544,17 +553,15 @@ class Doctor { try { fs.writeFileSync(testFile, 'test', 'utf8'); fs.unlinkSync(testFile); - spinner.succeed( - ` ${'Permissions'.padEnd(26)}${colored('[OK]', 'green')} Write access verified` - ); + spinner.succeed(); + console.log(` ${ok('Permissions'.padEnd(22))} Write access verified`); this.results.addCheck('Permissions', 'success', undefined, undefined, { status: 'OK', info: 'Write access verified', }); } catch (_e) { - spinner.fail( - ` ${'Permissions'.padEnd(26)}${colored('[X]', 'red')} Cannot write to ~/.ccs/` - ); + spinner.fail(); + console.log(` ${fail('Permissions'.padEnd(22))} Cannot write to ~/.ccs/`); this.results.addCheck( 'Permissions', 'error', @@ -578,9 +585,8 @@ class Doctor { if (health.healthy) { const itemCount = manager.ccsItems.length; - spinner.succeed( - ` ${'CCS Symlinks'.padEnd(26)}${colored('[OK]', 'green')} ${itemCount}/${itemCount} items linked` - ); + spinner.succeed(); + console.log(` ${ok('CCS Symlinks'.padEnd(22))} ${itemCount}/${itemCount} items linked`); this.results.addCheck( 'CCS Symlinks', 'success', @@ -592,9 +598,8 @@ class Doctor { } ); } else { - spinner.warn( - ` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} ${health.issues.length} issues found` - ); + spinner.warn(); + console.log(` ${warn('CCS Symlinks'.padEnd(22))} ${health.issues.length} issues found`); this.results.addCheck( 'CCS Symlinks', 'warning', @@ -604,7 +609,8 @@ class Doctor { ); } } catch (e) { - spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} Could not check`); + spinner.warn(); + console.log(` ${warn('CCS Symlinks'.padEnd(22))} Could not check`); this.results.addCheck( 'CCS Symlinks', 'warning', @@ -620,6 +626,7 @@ class Doctor { */ private checkSettingsSymlinks(): void { const spinner = ora('Checking settings.json symlinks').start(); + const settingsLabel = 'settings.json'; try { const sharedDir = path.join(this.homedir, '.ccs', 'shared'); @@ -628,9 +635,8 @@ class Doctor { // Check shared settings exists and points to ~/.claude/ if (!fs.existsSync(sharedSettings)) { - spinner.warn( - ` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not found` - ); + spinner.warn(); + console.log(` ${warn(settingsLabel.padEnd(22))} Not found (shared)`); this.results.addCheck( 'Settings Symlinks', 'warning', @@ -642,9 +648,8 @@ class Doctor { const sharedStats = fs.lstatSync(sharedSettings); if (!sharedStats.isSymbolicLink()) { - spinner.warn( - ` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Not a symlink` - ); + spinner.warn(); + console.log(` ${warn(settingsLabel.padEnd(22))} Not a symlink (shared)`); this.results.addCheck( 'Settings Symlinks', 'warning', @@ -658,9 +663,8 @@ class Doctor { const resolvedShared = path.resolve(path.dirname(sharedSettings), sharedTarget); if (resolvedShared !== claudeSettings) { - spinner.warn( - ` ${'settings.json (shared)'.padEnd(26)}${colored('[!]', 'yellow')} Wrong target` - ); + spinner.warn(); + console.log(` ${warn(settingsLabel.padEnd(22))} Wrong target (shared)`); this.results.addCheck( 'Settings Symlinks', 'warning', @@ -673,9 +677,8 @@ class Doctor { // Check each instance const instancesDir = path.join(this.ccsDir, 'instances'); if (!fs.existsSync(instancesDir)) { - spinner.succeed( - ` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} Shared symlink valid` - ); + spinner.succeed(); + console.log(` ${ok(settingsLabel.padEnd(22))} Shared symlink valid`); this.results.addCheck('Settings Symlinks', 'success', 'Shared symlink valid', undefined, { status: 'OK', info: 'Shared symlink valid', @@ -716,9 +719,8 @@ class Doctor { } if (broken > 0) { - spinner.warn( - ` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} ${broken} broken instance(s)` - ); + spinner.warn(); + console.log(` ${warn(settingsLabel.padEnd(22))} ${broken} broken instance(s)`); this.results.addCheck( 'Settings Symlinks', 'warning', @@ -727,9 +729,8 @@ class Doctor { { status: 'WARN', info: `${broken} broken instance(s)` } ); } else { - spinner.succeed( - ` ${'settings.json'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} instance(s) valid` - ); + spinner.succeed(); + console.log(` ${ok(settingsLabel.padEnd(22))} ${instances.length} instance(s) valid`); this.results.addCheck( 'Settings Symlinks', 'success', @@ -742,7 +743,8 @@ class Doctor { ); } } catch (err) { - spinner.warn(` ${'settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Check failed`); + spinner.warn(); + console.log(` ${warn(settingsLabel.padEnd(22))} Check failed`); this.results.addCheck( 'Settings Symlinks', 'warning', @@ -762,16 +764,16 @@ class Doctor { if (isCLIProxyInstalled()) { const binaryPath = getCLIProxyPath(); - binarySpinner.succeed( - ` ${'CLIProxy Binary'.padEnd(26)}${colored('[OK]', 'green')} v${CLIPROXY_VERSION}` - ); + binarySpinner.succeed(); + console.log(` ${ok('CLIProxy Binary'.padEnd(22))} v${CLIPROXY_VERSION}`); this.results.addCheck('CLIProxy Binary', 'success', undefined, undefined, { status: 'OK', info: `v${CLIPROXY_VERSION} (${binaryPath})`, }); } else { - binarySpinner.info( - ` ${'CLIProxy Binary'.padEnd(26)}${colored('[i]', 'cyan')} Not installed (downloads on first use)` + binarySpinner.info(); + console.log( + ` ${info('CLIProxy Binary'.padEnd(22))} Not installed (downloads on first use)` ); this.results.addCheck( 'CLIProxy Binary', @@ -787,17 +789,15 @@ class Doctor { const configPath = getConfigPath(); if (fs.existsSync(configPath)) { - configSpinner.succeed( - ` ${'CLIProxy Config'.padEnd(26)}${colored('[OK]', 'green')} cliproxy/config.yaml` - ); + configSpinner.succeed(); + console.log(` ${ok('CLIProxy Config'.padEnd(22))} cliproxy/config.yaml`); this.results.addCheck('CLIProxy Config', 'success', undefined, undefined, { status: 'OK', info: 'cliproxy/config.yaml', }); } else { - configSpinner.info( - ` ${'CLIProxy Config'.padEnd(26)}${colored('[i]', 'cyan')} Not created (generated on first use)` - ); + configSpinner.info(); + console.log(` ${info('CLIProxy Config'.padEnd(22))} Not created (on first use)`); this.results.addCheck('CLIProxy Config', 'success', 'Not created yet', undefined, { status: 'OK', info: 'Generated on first use', @@ -812,17 +812,15 @@ class Doctor { if (status.authenticated) { const lastAuth = status.lastAuth ? ` (${status.lastAuth.toLocaleDateString()})` : ''; - authSpinner.succeed( - ` ${`${providerName} Auth`.padEnd(26)}${colored('[OK]', 'green')} Authenticated${lastAuth}` - ); + authSpinner.succeed(); + console.log(` ${ok(`${providerName} Auth`.padEnd(22))} Authenticated${lastAuth}`); this.results.addCheck(`${providerName} Auth`, 'success', undefined, undefined, { status: 'OK', info: `Authenticated${lastAuth}`, }); } else { - authSpinner.info( - ` ${`${providerName} Auth`.padEnd(26)}${colored('[i]', 'cyan')} Not authenticated` - ); + authSpinner.info(); + console.log(` ${info(`${providerName} Auth`.padEnd(22))} Not authenticated`); this.results.addCheck( `${providerName} Auth`, 'success', @@ -838,17 +836,15 @@ class Doctor { const portAvailable = await isPortAvailable(CLIPROXY_DEFAULT_PORT); if (portAvailable) { - portSpinner.succeed( - ` ${'CLIProxy Port'.padEnd(26)}${colored('[OK]', 'green')} ${CLIPROXY_DEFAULT_PORT} available` - ); + portSpinner.succeed(); + console.log(` ${ok('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} available`); this.results.addCheck('CLIProxy Port', 'success', undefined, undefined, { status: 'OK', info: `Port ${CLIPROXY_DEFAULT_PORT} available`, }); } else { - portSpinner.warn( - ` ${'CLIProxy Port'.padEnd(26)}${colored('[!]', 'yellow')} ${CLIPROXY_DEFAULT_PORT} in use` - ); + portSpinner.warn(); + console.log(` ${warn('CLIProxy Port'.padEnd(22))} ${CLIPROXY_DEFAULT_PORT} in use`); this.results.addCheck( 'CLIProxy Port', 'warning', @@ -864,68 +860,48 @@ class Doctor { */ private showReport(): void { console.log(''); - - // Calculate exact table width to match header bars - // colWidths: [20, 10, 35] = 65 + borders (4) = 69 total - const tableWidth = 69; - const headerBar = '═'.repeat(tableWidth); - - console.log(colored(headerBar, 'cyan')); - console.log(colored(' Health Check Summary', 'bold')); - console.log(colored(headerBar, 'cyan')); - - // Create summary table with detailed information - const table = new Table({ - head: [colored('Component', 'cyan'), colored('Status', 'cyan'), colored('Details', 'cyan')], - colWidths: [20, 10, 35], - wordWrap: true, - chars: { - top: '═', - 'top-mid': '╤', - 'top-left': '╔', - 'top-right': '╗', - bottom: '═', - 'bottom-mid': '╧', - 'bottom-left': '╚', - 'bottom-right': '╝', - left: '║', - 'left-mid': '╟', - mid: '─', - 'mid-mid': '┼', - right: '║', - 'right-mid': '╢', - middle: '│', - }, - }); - - // Populate table with collected details - for (const [component, detail] of Object.entries(this.results.details)) { - const statusColor = - detail.status === 'OK' ? 'green' : detail.status === 'ERROR' ? 'red' : 'yellow'; - table.push([component, colored(detail.status, statusColor), detail.info || '']); - } - - console.log(table.toString()); + console.log(header('HEALTH CHECK SUMMARY')); console.log(''); - // Show errors and warnings if present + // Build summary table rows + const rows: string[][] = Object.entries(this.results.details).map(([component, detail]) => { + const statusIndicator = + detail.status === 'OK' + ? color('[OK]', 'success') + : detail.status === 'ERROR' + ? color('[X]', 'error') + : color('[!]', 'warning'); + + return [component, statusIndicator, detail.info || '']; + }); + + console.log( + table(rows, { + head: ['Component', 'Status', 'Details'], + colWidths: [20, 12, 35], + }) + ); + console.log(''); + + // Show errors if present if (this.results.hasErrors()) { - console.log(colored('Errors:', 'red')); + console.log(header('ERRORS')); this.results.errors.forEach((err) => { - console.log(` [X] ${err.name}: ${err.message}`); + console.log(` ${fail(err.name)}: ${err.message}`); if (err.fix) { - console.log(` Fix: ${err.fix}`); + console.log(` Fix: ${color(err.fix, 'command')}`); } }); console.log(''); } + // Show warnings if present if (this.results.hasWarnings()) { - console.log(colored('Warnings:', 'yellow')); - this.results.warnings.forEach((warn) => { - console.log(` [!] ${warn.name}: ${warn.message}`); - if (warn.fix) { - console.log(` Fix: ${warn.fix}`); + console.log(header('WARNINGS')); + this.results.warnings.forEach((w) => { + console.log(` ${warn(w.name)}: ${w.message}`); + if (w.fix) { + console.log(` Fix: ${color(w.fix, 'command')}`); } }); console.log(''); @@ -933,12 +909,15 @@ class Doctor { // Final status if (this.results.isHealthy() && !this.results.hasWarnings()) { - console.log(colored('[OK] All checks passed! Your CCS installation is healthy.', 'green')); + console.log(ok('All checks passed! Installation is healthy.')); } else if (this.results.hasErrors()) { - console.log(colored('[X] Status: Installation has errors', 'red')); - console.log('Run suggested fixes above to resolve issues.'); + console.log(fail('Installation has errors. Run suggested fixes above.')); } else { - console.log(colored('[OK] Status: Installation healthy (warnings only)', 'green')); + console.log( + ok( + `Installation healthy (${this.results.warnings.length} warning${this.results.warnings.length !== 1 ? 's' : ''})` + ) + ); } console.log(''); diff --git a/src/utils/error-manager.ts b/src/utils/error-manager.ts index d4a7fbe2..33380dd7 100644 --- a/src/utils/error-manager.ts +++ b/src/utils/error-manager.ts @@ -1,4 +1,4 @@ -import { colored } from './helpers'; +import { initUI, header, color, dim, errorBox } from './ui'; import { ERROR_CODES, getErrorDocUrl, ErrorCode } from './error-codes'; /** @@ -23,60 +23,70 @@ export class ErrorManager { * Show error code and documentation URL */ static showErrorCode(errorCode: ErrorCode): void { - console.error(colored(`Error: ${errorCode}`, 'yellow')); - console.error(colored(getErrorDocUrl(errorCode), 'yellow')); + console.error(dim(`Error: ${errorCode}`)); + console.error(dim(getErrorDocUrl(errorCode))); console.error(''); } /** * Show Claude CLI not found error */ - static showClaudeNotFound(): void { + static async showClaudeNotFound(): Promise { + await initUI(); + console.error(''); - console.error(colored('[X] Claude CLI not found', 'red')); + console.error( + errorBox( + 'Claude CLI not found\n\n' + + 'CCS requires Claude CLI to be installed\n' + + 'and available in PATH.', + 'ERROR' + ) + ); console.error(''); - console.error('CCS requires Claude CLI to be installed and available in PATH.'); + + console.error(header('SOLUTIONS')); console.error(''); - console.error(colored('Solutions:', 'yellow')); - console.error(' 1. Install Claude CLI:'); - console.error(' https://docs.claude.com/en/docs/claude-code/installation'); + console.error(' 1. Install Claude CLI'); + console.error(` ${color('https://docs.claude.com/install', 'path')}`); console.error(''); - console.error(' 2. Verify installation:'); - console.error(' command -v claude (Unix)'); - console.error(' Get-Command claude (Windows)'); + console.error(' 2. Verify installation'); + console.error(` ${color('command -v claude', 'command')} (Unix)`); + console.error(` ${color('Get-Command claude', 'command')} (Windows)`); console.error(''); - console.error(' 3. Custom path (if installed elsewhere):'); - console.error(' export CCS_CLAUDE_PATH="/path/to/claude"'); + console.error(' 3. Custom path (if installed elsewhere)'); + console.error(` ${color('export CCS_CLAUDE_PATH="/path/to/claude"', 'command')}`); console.error(''); + this.showErrorCode(ERROR_CODES.CLAUDE_NOT_FOUND); } /** * Show settings file not found error */ - static showSettingsNotFound(settingsPath: string): void { + static async showSettingsNotFound(settingsPath: string): Promise { + await initUI(); + const isClaudeSettings = settingsPath.includes('.claude') && settingsPath.endsWith('settings.json'); console.error(''); - console.error(colored('[X] Settings file not found', 'red')); - console.error(''); - console.error(`File: ${settingsPath}`); + console.error(errorBox('Settings file not found\n\n' + `File: ${settingsPath}`, 'ERROR')); console.error(''); if (isClaudeSettings) { console.error('This file is auto-created when you login to Claude CLI.'); console.error(''); - console.error(colored('Solutions:', 'yellow')); - console.error(` echo '{}' > ${settingsPath}`); - console.error(' claude /login'); + console.error(header('SOLUTIONS')); + console.error(` ${color(`echo '{}' > ${settingsPath}`, 'command')}`); + console.error(` ${color('claude /login', 'command')}`); console.error(''); - console.error('Why: Newer Claude CLI versions require explicit login.'); + console.error(dim('Why: Newer Claude CLI versions require explicit login.')); } else { - console.error(colored('Solutions:', 'yellow')); - console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(header('SOLUTIONS')); + console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`); console.error(''); - console.error('This will recreate missing profile settings.'); + console.error(dim('This will recreate missing profile settings.')); } console.error(''); @@ -86,120 +96,139 @@ export class ErrorManager { /** * Show invalid configuration error */ - static showInvalidConfig(configPath: string, errorDetail: string): void { + static async showInvalidConfig(configPath: string, errorDetail: string): Promise { + await initUI(); + console.error(''); - console.error(colored('[X] Configuration invalid', 'red')); + console.error( + errorBox( + 'Configuration invalid\n\n' + `File: ${configPath}\n` + `Issue: ${errorDetail}`, + 'ERROR' + ) + ); console.error(''); - console.error(`File: ${configPath}`); - console.error(`Issue: ${errorDetail}`); + + console.error(header('SOLUTIONS')); console.error(''); - console.error(colored('Solutions:', 'yellow')); - console.error(' # Backup corrupted file'); - console.error(` mv ${configPath} ${configPath}.backup`); + console.error(` ${dim('# Backup corrupted file')}`); + console.error(` ${color(`mv ${configPath} ${configPath}.backup`, 'command')}`); console.error(''); - console.error(' # Reinstall CCS'); - console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(` ${dim('# Reinstall CCS')}`); + console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`); console.error(''); - console.error('Your profile settings will be preserved.'); + console.error(dim('Your profile settings will be preserved.')); console.error(''); + this.showErrorCode(ERROR_CODES.CONFIG_INVALID_JSON); } /** * Show profile not found error */ - static showProfileNotFound( + static async showProfileNotFound( profileName: string, availableProfiles: string[], suggestions: string[] = [] - ): void { + ): Promise { + await initUI(); + console.error(''); - console.error(colored(`[X] Profile '${profileName}' not found`, 'red')); + console.error(errorBox(`Profile '${profileName}' not found`, 'ERROR')); console.error(''); if (suggestions && suggestions.length > 0) { - console.error(colored('Did you mean:', 'yellow')); - suggestions.forEach((s) => console.error(` ${s}`)); + console.error(header('DID YOU MEAN')); + suggestions.forEach((s) => console.error(` ${color(s, 'command')}`)); console.error(''); } - console.error(colored('Available profiles:', 'cyan')); - availableProfiles.forEach((line) => console.error(` ${line}`)); + console.error(header('AVAILABLE PROFILES')); + availableProfiles.forEach((line) => console.error(` ${color(line, 'info')}`)); console.error(''); - console.error(colored('Solutions:', 'yellow')); - console.error(' # Use existing profile'); - console.error(' ccs "your prompt"'); + + console.error(header('SOLUTIONS')); console.error(''); - console.error(' # Create new account profile'); - console.error(' ccs auth create '); + console.error(' Use an existing profile:'); + console.error(` ${color('ccs "your prompt"', 'command')}`); console.error(''); + console.error(' Create a new account profile:'); + console.error(` ${color('ccs auth create ', 'command')}`); + console.error(''); + this.showErrorCode(ERROR_CODES.PROFILE_NOT_FOUND); } /** * Show permission denied error */ - static showPermissionDenied(path: string): void { + static async showPermissionDenied(filePath: string): Promise { + await initUI(); + console.error(''); - console.error(colored('[X] Permission denied', 'red')); + console.error(errorBox('Permission denied\n\n' + `Cannot write to: ${filePath}`, 'ERROR')); console.error(''); - console.error(`Cannot write to: ${path}`); + + console.error(header('SOLUTIONS')); console.error(''); - console.error(colored('Solutions:', 'yellow')); - console.error(' # Fix ownership'); - console.error(' sudo chown -R $USER ~/.ccs ~/.claude'); + console.error(` ${dim('# Fix ownership')}`); + console.error(` ${color('sudo chown -R $USER ~/.ccs ~/.claude', 'command')}`); console.error(''); - console.error(' # Fix permissions'); - console.error(' chmod 755 ~/.ccs ~/.claude'); + console.error(` ${dim('# Fix permissions')}`); + console.error(` ${color('chmod 755 ~/.ccs ~/.claude', 'command')}`); console.error(''); - console.error(' # Retry installation'); - console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(` ${dim('# Retry installation')}`); + console.error(` ${color('npm install -g @kaitranntt/ccs --force', 'command')}`); console.error(''); + this.showErrorCode(ERROR_CODES.FS_CANNOT_WRITE_FILE); } /** * Show CLIProxy OAuth timeout error */ - static showOAuthTimeout(provider: string): void { + static async showOAuthTimeout(provider: string): Promise { + await initUI(); + console.error(''); - console.error(colored('+---------------------------------------------------------+', 'red')); - console.error(colored('| OAuth Timeout |', 'red')); - console.error(colored('+---------------------------------------------------------+', 'red')); + console.error( + errorBox('OAuth Timeout\n\n' + 'Authentication did not complete within 2 minutes.', 'ERROR') + ); console.error(''); - console.error('Authentication did not complete within 2 minutes.'); - console.error(''); - console.error(colored('Troubleshooting:', 'yellow')); + + console.error(header('TROUBLESHOOTING')); console.error(' 1. Check if browser opened (popup blocker?)'); console.error(' 2. Complete login in browser, then return here'); console.error(' 3. Try different browser'); console.error(' 4. Disable browser extensions temporarily'); console.error(''); - console.error(colored('For headless/SSH environments:', 'cyan')); - console.error(` ccs ${provider} --auth --headless`); + + console.error(header('FOR HEADLESS/SSH ENVIRONMENTS')); + console.error(` ${color(`ccs ${provider} --auth --headless`, 'command')}`); console.error(''); - console.error('This displays manual authentication steps.'); + console.error(dim('This displays manual authentication steps.')); console.error(''); } /** * Show CLIProxy port conflict error */ - static showPortConflict(port: number): void { + static async showPortConflict(port: number): Promise { + await initUI(); + console.error(''); - console.error(colored('+---------------------------------------------------------+', 'red')); - console.error(colored('| Port Conflict |', 'red')); - console.error(colored('+---------------------------------------------------------+', 'red')); + console.error( + errorBox('Port Conflict\n\n' + `CLIProxy port ${port} is already in use.`, 'ERROR') + ); console.error(''); - console.error(`CLIProxy port ${port} is already in use.`); + + console.error(header('SOLUTIONS')); console.error(''); - console.error(colored('Solutions:', 'yellow')); console.error(' 1. Find process using port:'); - console.error(` lsof -i :${port} (macOS/Linux)`); - console.error(` netstat -ano | findstr ${port} (Windows)`); + console.error(` ${color(`lsof -i :${port}`, 'command')} (macOS/Linux)`); + console.error(` ${color(`netstat -ano | findstr ${port}`, 'command')} (Windows)`); console.error(''); console.error(' 2. Kill the process:'); - console.error(` lsof -ti:${port} | xargs kill -9`); + console.error(` ${color(`lsof -ti:${port} | xargs kill -9`, 'command')}`); console.error(''); console.error(' 3. Wait and retry (process may exit on its own)'); console.error(''); @@ -208,38 +237,43 @@ export class ErrorManager { /** * Show CLIProxy binary download failure error */ - static showBinaryDownloadFailed(url: string, error: string): void { + static async showBinaryDownloadFailed(url: string, error: string): Promise { + await initUI(); + console.error(''); - console.error(colored('+---------------------------------------------------------+', 'red')); - console.error(colored('| Binary Download Failed |', 'red')); - console.error(colored('+---------------------------------------------------------+', 'red')); + console.error(errorBox('Binary Download Failed\n\n' + `Error: ${error}`, 'ERROR')); console.error(''); - console.error(`Error: ${error}`); - console.error(''); - console.error(colored('Troubleshooting:', 'yellow')); + + console.error(header('TROUBLESHOOTING')); console.error(' 1. Check internet connection'); console.error(' 2. Check firewall/proxy settings'); console.error(' 3. Try again in a few minutes'); console.error(''); - console.error(colored('Manual download:', 'cyan')); - console.error(` URL: ${url}`); - console.error(' Save to: ~/.ccs/bin/cliproxyapi'); - console.error(' chmod +x ~/.ccs/bin/cliproxyapi'); + + console.error(header('MANUAL DOWNLOAD')); + console.error(` URL: ${color(url, 'path')}`); + console.error(` Save to: ${color('~/.ccs/bin/cliproxyapi', 'path')}`); + console.error(` ${color('chmod +x ~/.ccs/bin/cliproxyapi', 'command')}`); console.error(''); } /** * Show CLIProxy authentication required error */ - static showAuthRequired(provider: string): void { + static async showAuthRequired(provider: string): Promise { + await initUI(); + + const displayName = provider.charAt(0).toUpperCase() + provider.slice(1); + console.error(''); - console.error(colored(`[X] ${provider} authentication required`, 'red')); + console.error(errorBox(`${displayName} authentication required`, 'ERROR')); console.error(''); - console.error(colored('To authenticate:', 'yellow')); - console.error(` ccs ${provider} --auth`); + + console.error(header('TO AUTHENTICATE')); + console.error(` ${color(`ccs ${provider} --auth`, 'command')}`); console.error(''); - console.error('This will open a browser for OAuth login.'); - console.error('After login, you can use the profile normally.'); + console.error(dim('This will open a browser for OAuth login.')); + console.error(dim('After login, you can use the profile normally.')); console.error(''); } } diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index cde7cd7a..c0af201b 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -52,8 +52,8 @@ export function execClaude( else process.exit(code || 0); }); - child.on('error', () => { - ErrorManager.showClaudeNotFound(); + child.on('error', async () => { + await ErrorManager.showClaudeNotFound(); process.exit(1); }); }