diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index 1f8b057f..32567662 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -16,6 +16,7 @@ import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { ProgressIndicator } from '../utils/progress-indicator'; +import { ok, fail, info, warn } from '../utils/ui'; import { ensureCLIProxyBinary } from './binary-manager'; import { generateConfig, getProviderAuthDir } from './config-generator'; import { CLIProxyProvider } from './types'; @@ -460,7 +461,9 @@ export async function triggerOAuth( if (existingAccounts.length > 0 && !add) { console.log(''); console.log( - `[i] ${existingAccounts.length} account(s) already authenticated for ${oauthConfig.displayName}` + info( + `${existingAccounts.length} account(s) already authenticated for ${oauthConfig.displayName}` + ) ); // Import readline for confirm prompt @@ -478,7 +481,7 @@ export async function triggerOAuth( }); if (!confirmed) { - console.log('[i] Cancelled'); + console.log(info('Cancelled')); return null; } } @@ -487,12 +490,12 @@ export async function triggerOAuth( const preflight = await preflightOAuthCheck(provider); if (!preflight.ready) { console.log(''); - console.log('[!] OAuth pre-flight check failed:'); + console.log(warn('OAuth pre-flight check failed:')); for (const issue of preflight.issues) { console.log(` ${issue}`); } console.log(''); - console.log('[i] Resolve the port conflict and try again.'); + console.log(info('Resolve the port conflict and try again.')); return null; } @@ -501,7 +504,7 @@ export async function triggerOAuth( try { binaryPath = await ensureCLIProxyBinary(verbose); } catch (error) { - console.error('[X] Failed to prepare CLIProxy binary'); + console.error(fail('Failed to prepare CLIProxy binary')); throw error; } @@ -532,12 +535,12 @@ export async function triggerOAuth( // Show appropriate message console.log(''); if (headless) { - console.log(`[i] Headless mode detected - manual authentication required`); - console.log(`[i] ${oauthConfig.displayName} will display an OAuth URL below`); + console.log(info('Headless mode detected - manual authentication required')); + console.log(info(`${oauthConfig.displayName} will display an OAuth URL below`)); console.log(''); } else { - console.log(`[i] Opening browser for ${oauthConfig.displayName} authentication...`); - console.log('[i] Complete the login in your browser.'); + console.log(info(`Opening browser for ${oauthConfig.displayName} authentication...`)); + console.log(info('Complete the login in your browser.')); console.log(''); } @@ -572,7 +575,7 @@ export async function triggerOAuth( if (urlMatch && !urlDisplayed) { console.log(` ${urlMatch[0]}`); console.log(''); - console.log('[i] Waiting for authentication... (press Ctrl+C to cancel)'); + console.log(info('Waiting for authentication... (press Ctrl+C to cancel)')); urlDisplayed = true; } } @@ -590,7 +593,7 @@ export async function triggerOAuth( if (urlMatch) { console.log(` ${urlMatch[0]}`); console.log(''); - console.log('[i] Waiting for authentication... (press Ctrl+C to cancel)'); + console.log(info('Waiting for authentication... (press Ctrl+C to cancel)')); urlDisplayed = true; } } @@ -601,7 +604,7 @@ export async function triggerOAuth( const timeout = setTimeout(() => { if (!headless) spinner.fail('Authentication timeout'); authProcess.kill(); - console.error(`[X] OAuth timed out after ${headless ? 5 : 2} minutes`); + console.error(fail(`OAuth timed out after ${headless ? 5 : 2} minutes`)); console.error(''); if (!headless) { console.error('Troubleshooting:'); @@ -618,14 +621,14 @@ export async function triggerOAuth( // Verify token was created BEFORE showing success if (isAuthenticated(provider)) { if (!headless) spinner.succeed(`Authenticated with ${oauthConfig.displayName}`); - console.log('[OK] Authentication successful'); + console.log(ok('Authentication successful')); // Register the account in accounts registry const account = registerAccountFromToken(provider, tokenDir, nickname); resolve(account); } else { if (!headless) spinner.fail('Authentication incomplete'); - console.error('[X] Token not found after authentication'); + console.error(fail('Token not found after authentication')); // Qwen uses Device Code Flow (polling), others use Authorization Code Flow (callback) if (provider === 'qwen') { console.error(' Qwen uses Device Code Flow - ensure you completed auth in browser'); @@ -639,14 +642,14 @@ export async function triggerOAuth( } } else { if (!headless) spinner.fail('Authentication failed'); - console.error(`[X] CLIProxyAPI auth exited with code ${code}`); + console.error(fail(`CLIProxyAPI auth exited with code ${code}`)); if (stderrData && !urlDisplayed) { console.error(` ${stderrData.trim().split('\n')[0]}`); } // Show headless hint if we detected headless environment if (headless && !urlDisplayed) { console.error(''); - console.error('[i] No OAuth URL was displayed. Try with --verbose for details.'); + console.error(info('No OAuth URL was displayed. Try with --verbose for details.')); } resolve(null); } @@ -655,7 +658,7 @@ export async function triggerOAuth( authProcess.on('error', (error) => { clearTimeout(timeout); if (!headless) spinner.fail('Authentication error'); - console.error(`[X] Failed to start auth process: ${error.message}`); + console.error(fail(`Failed to start auth process: ${error.message}`)); resolve(null); }); }); @@ -735,7 +738,7 @@ export async function ensureAuth( // Not authenticated - trigger OAuth const oauthConfig = getOAuthConfig(provider); - console.log(`[i] ${oauthConfig.displayName} authentication required`); + console.log(info(`${oauthConfig.displayName} authentication required`)); const account = await triggerOAuth(provider, options); return account !== null; diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index 87ecbc28..5ebc8948 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -18,6 +18,7 @@ import * as http from 'http'; import * as crypto from 'crypto'; import * as zlib from 'zlib'; import { ProgressIndicator } from '../utils/progress-indicator'; +import { ok, info } from '../utils/ui'; import { getBinDir, getCliproxyDir } from './config-generator'; import { BinaryInfo, @@ -100,9 +101,11 @@ export class BinaryManager { const updateResult = await this.checkForUpdates(); if (updateResult.hasUpdate) { console.log( - `[i] CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}` + info( + `CLIProxyAPI update available: v${updateResult.currentVersion} -> v${updateResult.latestVersion}` + ) ); - console.log(`[i] Updating CLIProxyAPI...`); + console.log(info('Updating CLIProxyAPI...')); // Delete old binary and download new version this.deleteBinary(); @@ -435,7 +438,7 @@ export class BinaryManager { // Save installed version for future update checks this.saveInstalledVersion(this.config.version); - console.log(`[OK] CLIProxyAPI v${this.config.version} installed successfully`); + console.log(ok(`CLIProxyAPI v${this.config.version} installed successfully`)); } catch (error) { spinner.fail('Installation failed'); throw error; @@ -938,7 +941,7 @@ export async function installCliproxyVersion(version: string, verbose = false): if (manager.isBinaryInstalled()) { const currentVersion = getInstalledCliproxyVersion(); if (verbose) { - console.log(`[i] Removing existing CLIProxyAPI v${currentVersion}`); + console.log(info(`Removing existing CLIProxyAPI v${currentVersion}`)); } manager.deleteBinary(); } diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index e21d1a27..744a66a0 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -15,6 +15,7 @@ import { spawn, ChildProcess } from 'child_process'; import * as net from 'net'; import { ProgressIndicator } from '../utils/progress-indicator'; +import { ok, fail, info, warn } from '../utils/ui'; import { escapeShellArg } from '../utils/shell-executor'; import { ensureCLIProxyBinary } from './binary-manager'; import { @@ -152,7 +153,7 @@ export async function execClaudeWithCLIProxy( if (showAccounts) { const accounts = getProviderAccounts(provider); if (accounts.length === 0) { - console.log(`[i] No accounts registered for ${providerConfig.displayName}`); + console.log(info(`No accounts registered for ${providerConfig.displayName}`)); console.log(` Run "ccs ${provider} --auth" to add an account`); } else { console.log(`\n${providerConfig.displayName} Accounts:\n`); @@ -170,7 +171,7 @@ export async function execClaudeWithCLIProxy( if (useAccount) { const account = findAccountByQuery(provider, useAccount); if (!account) { - console.error(`[X] Account not found: "${useAccount}"`); + console.error(fail(`Account not found: "${useAccount}"`)); const accounts = getProviderAccounts(provider); if (accounts.length > 0) { console.error(` Available accounts:`); @@ -183,27 +184,27 @@ export async function execClaudeWithCLIProxy( // Set as default for this and future sessions setDefaultAccount(provider, account.id); touchAccount(provider, account.id); - console.log(`[OK] Switched to account: ${account.nickname || account.email || account.id}`); + console.log(ok(`Switched to account: ${account.nickname || account.email || account.id}`)); } // Handle --nickname: rename account and exit (unless used with --auth --add) if (setNickname && !addAccount) { const defaultAccount = getDefaultAccount(provider); if (!defaultAccount) { - console.error(`[X] No account found for ${providerConfig.displayName}`); + console.error(fail(`No account found for ${providerConfig.displayName}`)); console.error(` Run "ccs ${provider} --auth" to add an account first`); process.exit(1); } try { const success = renameAccount(provider, defaultAccount.id, setNickname); if (success) { - console.log(`[OK] Renamed account to: ${setNickname}`); + console.log(ok(`Renamed account to: ${setNickname}`)); } else { - console.error(`[X] Failed to rename account`); + console.error(fail('Failed to rename account')); process.exit(1); } } catch (err) { - console.error(`[X] ${err instanceof Error ? err.message : 'Failed to rename account'}`); + console.error(fail(err instanceof Error ? err.message : 'Failed to rename account')); process.exit(1); } process.exit(0); @@ -220,9 +221,9 @@ export async function execClaudeWithCLIProxy( if (forceLogout) { const { clearAuth } = await import('./auth-handler'); if (clearAuth(provider)) { - console.log(`[OK] Logged out from ${providerConfig.displayName}`); + console.log(ok(`Logged out from ${providerConfig.displayName}`)); } else { - console.log(`[i] No authentication found for ${providerConfig.displayName}`); + console.log(info(`No authentication found for ${providerConfig.displayName}`)); } process.exit(0); } @@ -265,9 +266,7 @@ export async function execClaudeWithCLIProxy( const modelEntry = findModel(provider, currentModel); const issueUrl = getModelIssueUrl(provider, currentModel); console.error(''); - console.error( - `[!] Warning: ${modelEntry?.name || currentModel} has known issues with Claude Code` - ); + console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`)); console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.'); if (issueUrl) { console.error(` Tracking: ${issueUrl}`); @@ -306,7 +305,7 @@ export async function execClaudeWithCLIProxy( // Handle proxy errors proxy.on('error', (error) => { - console.error(`[X] CLIProxy spawn error: ${error.message}`); + console.error(fail(`CLIProxy spawn error: ${error.message}`)); }); // 5. Wait for proxy readiness via TCP polling @@ -322,7 +321,7 @@ export async function execClaudeWithCLIProxy( const err = error as Error; console.error(''); - console.error('[X] CLIProxy failed to start'); + console.error(fail('CLIProxy failed to start')); console.error(''); console.error('Possible causes:'); console.error(` 1. Port ${cfg.port} already in use`); @@ -398,7 +397,7 @@ export async function execClaudeWithCLIProxy( }); claude.on('error', (error) => { - console.error('[X] Claude CLI error:', error); + console.error(fail(`Claude CLI error: ${error}`)); proxy.kill('SIGTERM'); process.exit(1); }); diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 2bc103db..92e70e1e 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -11,6 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir } from '../utils/config-manager'; +import { warn } from '../utils/ui'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader'; @@ -373,10 +374,10 @@ export function getEffectiveEnvVars( } } catch { // Invalid JSON - fall through to provider defaults - console.warn(`[!] Warning: Invalid settings file: ${customSettingsPath}`); + console.warn(warn(`Invalid settings file: ${customSettingsPath}`)); } } else { - console.warn(`[!] Warning: Settings file not found: ${customSettingsPath}`); + console.warn(warn(`Settings file not found: ${customSettingsPath}`)); } } diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index ff4b65ba..a6fd1715 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -945,21 +945,21 @@ async function showStatus(verbose: boolean): Promise { async function installVersion(version: string, verbose: boolean): Promise { // Validate version format (basic semver check) if (!/^\d+\.\d+\.\d+$/.test(version)) { - console.error('[X] Invalid version format. Expected format: X.Y.Z (e.g., 6.5.53)'); + console.error(fail('Invalid version format. Expected format: X.Y.Z (e.g., 6.5.53)')); process.exit(1); } - console.log(`[i] Installing CLIProxyAPI v${version}...`); + console.log(info(`Installing CLIProxyAPI v${version}...`)); console.log(''); try { await installCliproxyVersion(version, verbose); console.log(''); - console.log(`[OK] CLIProxyAPI v${version} installed successfully`); + console.log(ok(`CLIProxyAPI v${version} installed successfully`)); } catch (error) { const err = error as Error; console.error(''); - console.error(`[X] Failed to install CLIProxyAPI v${version}`); + console.error(fail(`Failed to install CLIProxyAPI v${version}`)); console.error(` ${err.message}`); console.error(''); console.error('Possible causes:'); @@ -977,29 +977,29 @@ async function installVersion(version: string, verbose: boolean): Promise * Install latest version */ async function installLatest(verbose: boolean): Promise { - console.log('[i] Fetching latest CLIProxyAPI version...'); + console.log(info('Fetching latest CLIProxyAPI version...')); try { const latestVersion = await fetchLatestCliproxyVersion(); const currentVersion = getInstalledCliproxyVersion(); if (isCLIProxyInstalled() && latestVersion === currentVersion) { - console.log(`[OK] Already running latest version: v${latestVersion}`); + console.log(ok(`Already running latest version: v${latestVersion}`)); return; } - console.log(`[i] Latest version: v${latestVersion}`); + console.log(info(`Latest version: v${latestVersion}`)); if (isCLIProxyInstalled()) { - console.log(`[i] Current version: v${currentVersion}`); + console.log(info(`Current version: v${currentVersion}`)); } console.log(''); await installCliproxyVersion(latestVersion, verbose); console.log(''); - console.log(`[OK] CLIProxyAPI updated to v${latestVersion}`); + console.log(ok(`CLIProxyAPI updated to v${latestVersion}`)); } catch (error) { const err = error as Error; - console.error(`[X] Failed to install latest version: ${err.message}`); + console.error(fail(`Failed to install latest version: ${err.message}`)); process.exit(1); } } diff --git a/src/commands/install-command.ts b/src/commands/install-command.ts index 30a41136..5bdcfbf2 100644 --- a/src/commands/install-command.ts +++ b/src/commands/install-command.ts @@ -4,17 +4,20 @@ * Handle --install and --uninstall commands for CCS. */ +import { info, color, initUI } from '../utils/ui'; + /** * Handle install command */ -export function handleInstallCommand(): void { +export async function handleInstallCommand(): Promise { + await initUI(); console.log(''); - console.log('Feature not available'); + console.log(info('Feature not available')); console.log(''); console.log('The --install flag is currently under development.'); console.log('.claude/ integration testing is not complete.'); console.log(''); - console.log('For updates: https://github.com/kaitranntt/ccs/issues'); + console.log(`For updates: ${color('https://github.com/kaitranntt/ccs/issues', 'path')}`); console.log(''); process.exit(0); } @@ -22,14 +25,15 @@ export function handleInstallCommand(): void { /** * Handle uninstall command */ -export function handleUninstallCommand(): void { +export async function handleUninstallCommand(): Promise { + await initUI(); console.log(''); - console.log('Feature not available'); + console.log(info('Feature not available')); console.log(''); console.log('The --uninstall flag is currently under development.'); console.log('.claude/ integration testing is not complete.'); console.log(''); - console.log('For updates: https://github.com/kaitranntt/ccs/issues'); + console.log(`For updates: ${color('https://github.com/kaitranntt/ccs/issues', 'path')}`); console.log(''); process.exit(0); } diff --git a/src/commands/migrate-command.ts b/src/commands/migrate-command.ts index aac190fc..3d643689 100644 --- a/src/commands/migrate-command.ts +++ b/src/commands/migrate-command.ts @@ -17,8 +17,11 @@ import { getBackupDirectories, } from '../config/migration-manager'; import { hasUnifiedConfig } from '../config/unified-config-loader'; +import { initUI, ok, fail, info, warn, infoBox, dim } from '../utils/ui'; export async function handleMigrateCommand(args: string[]): Promise { + await initUI(); + // Handle --list-backups if (args.includes('--list-backups')) { listBackups(); @@ -31,9 +34,9 @@ export async function handleMigrateCommand(args: string[]): Promise { const backupPath = args[rollbackIndex + 1]; if (!backupPath) { - console.error('[X] Error: --rollback requires backup path'); - console.log('[i] Usage: ccs migrate --rollback '); - console.log('[i] Use --list-backups to see available backups'); + console.error(fail('Error: --rollback requires backup path')); + console.log(info('Usage: ccs migrate --rollback ')); + console.log(info('Use --list-backups to see available backups')); process.exit(1); } @@ -43,13 +46,13 @@ export async function handleMigrateCommand(args: string[]): Promise { // Check if already migrated if (hasUnifiedConfig() && !needsMigration()) { - console.log('[i] Already using unified config format (config.yaml)'); + console.log(info('Already using unified config format (config.yaml)')); return; } // Check if migration is needed if (!needsMigration()) { - console.log('[i] No migration needed - no legacy config found'); + console.log(info('No migration needed - no legacy config found')); return; } @@ -57,7 +60,7 @@ export async function handleMigrateCommand(args: string[]): Promise { const dryRun = args.includes('--dry-run'); if (dryRun) { - console.log('[i] Dry run - no changes will be made'); + console.log(info('Dry run - no changes will be made')); console.log(''); } @@ -65,13 +68,11 @@ export async function handleMigrateCommand(args: string[]): Promise { if (result.success) { console.log(''); - console.log('╭─────────────────────────────────────────────────────────╮'); if (dryRun) { - console.log('│ [i] Dry run - migration preview (no changes made) │'); + console.log(infoBox('Dry run - migration preview (no changes made)')); } else { - console.log('│ [OK] Migrated to unified config (config.yaml) │'); + console.log(infoBox('Migrated to unified config (config.yaml)', 'SUCCESS')); } - console.log('╰─────────────────────────────────────────────────────────╯'); if (result.backupPath && !dryRun) { console.log(` Backup: ${result.backupPath}`); @@ -80,18 +81,18 @@ export async function handleMigrateCommand(args: string[]): Promise { if (result.warnings.length > 0) { for (const warning of result.warnings) { - console.log(` [!] ${warning}`); + console.log(warn(warning)); } } if (dryRun) { - console.log(' Run without --dry-run to apply changes'); + console.log(dim(' Run without --dry-run to apply changes')); } else { console.log(` Rollback: ccs migrate --rollback ${result.backupPath}`); } console.log(''); } else { - console.error(`[X] Migration failed: ${result.error}`); + console.error(fail(`Migration failed: ${result.error}`)); if (result.migratedFiles.length > 0) { console.log(''); @@ -104,16 +105,16 @@ export async function handleMigrateCommand(args: string[]): Promise { } async function handleRollback(backupPath: string): Promise { - console.log(`[i] Rolling back from: ${backupPath}`); + console.log(info(`Rolling back from: ${backupPath}`)); console.log(''); const success = await rollback(backupPath); if (success) { - console.log('[OK] Rollback complete'); - console.log('[i] Legacy config restored'); + console.log(ok('Rollback complete')); + console.log(info('Legacy config restored')); } else { - console.error('[X] Rollback failed'); + console.error(fail('Rollback failed')); process.exit(1); } } @@ -122,17 +123,17 @@ function listBackups(): void { const backups = getBackupDirectories(); if (backups.length === 0) { - console.log('[i] No backup directories found'); + console.log(info('No backup directories found')); return; } - console.log('[i] Available backups (most recent first):'); + console.log(info('Available backups (most recent first):')); console.log(''); backups.forEach((backup, index) => { console.log(` ${index + 1}. ${backup}`); }); console.log(''); - console.log('[i] To rollback: ccs migrate --rollback '); + console.log(info('To rollback: ccs migrate --rollback ')); } /** diff --git a/src/commands/shell-completion-command.ts b/src/commands/shell-completion-command.ts index 804c7f7f..f01de47c 100644 --- a/src/commands/shell-completion-command.ts +++ b/src/commands/shell-completion-command.ts @@ -4,15 +4,16 @@ * Handle --shell-completion command for CCS. */ -import { colored } from '../utils/helpers'; +import { initUI, header, ok, fail, color } from '../utils/ui'; /** * Handle shell completion command */ export async function handleShellCompletionCommand(args: string[]): Promise { + await initUI(); const { ShellCompletionInstaller } = await import('../utils/shell-completion'); - console.log(colored('Shell Completion Installer', 'bold')); + console.log(header('Shell Completion Installer')); console.log(''); // Parse flags @@ -30,28 +31,28 @@ export async function handleShellCompletionCommand(args: string[]): Promise # See available profiles'); console.log(' ccs auth # See auth subcommands'); console.log(''); } catch (error) { const err = error as Error; - console.error(colored('[X] Error:', 'red'), err.message); + console.error(fail(`Error: ${err.message}`)); console.error(''); - console.error(colored('Usage:', 'yellow')); + console.error(color('Usage:', 'warning')); console.error(' ccs --shell-completion # Auto-detect shell'); console.error(' ccs --shell-completion --bash # Install for bash'); console.error(' ccs --shell-completion --zsh # Install for zsh'); diff --git a/src/commands/sync-command.ts b/src/commands/sync-command.ts index bb2c60c0..2ceb5d16 100644 --- a/src/commands/sync-command.ts +++ b/src/commands/sync-command.ts @@ -4,14 +4,15 @@ * Handle sync command for CCS. */ -import { colored } from '../utils/helpers'; +import { initUI, header, ok } from '../utils/ui'; /** * Handle sync command */ export async function handleSyncCommand(): Promise { + await initUI(); console.log(''); - console.log(colored('Syncing CCS Components...', 'cyan')); + console.log(header('Syncing CCS Components...')); console.log(''); // First, copy .claude/ directory from package to ~/.ccs/.claude/ @@ -38,10 +39,10 @@ export async function handleSyncCommand(): Promise { const SharedManager = (await import('../management/shared-manager')).default; const sharedManager = new SharedManager(); sharedManager.ensureSharedDirectories(); - console.log(colored('[OK]', 'green') + ' Shared symlinks verified'); + console.log(ok('Shared symlinks verified')); console.log(''); - console.log(colored('[OK] Sync complete!', 'green')); + console.log(ok('Sync complete!')); console.log(''); process.exit(0); diff --git a/src/commands/update-command.ts b/src/commands/update-command.ts index c875b8bd..406dd9df 100644 --- a/src/commands/update-command.ts +++ b/src/commands/update-command.ts @@ -6,7 +6,7 @@ */ import { spawn } from 'child_process'; -import { colored } from '../utils/helpers'; +import { initUI, header, ok, fail, warn, info, color } from '../utils/ui'; import { detectInstallationMethod, detectPackageManager } from '../utils/package-manager-detector'; import { compareVersionsWithPrerelease } from '../utils/update-checker'; import { getVersion } from '../utils/version'; @@ -27,11 +27,12 @@ const CCS_VERSION = getVersion(); * Checks for updates and installs the latest version */ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise { + await initUI(); const { force = false, beta = false } = options; const targetTag = beta ? 'dev' : 'latest'; console.log(''); - console.log(colored('Checking for updates...', 'cyan')); + console.log(header('Checking for updates...')); console.log(''); const installMethod = detectInstallationMethod(); @@ -39,7 +40,7 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise< // Force reinstall - skip update check if (force) { - console.log(colored(`[i] Force reinstall from @${targetTag} channel...`, 'cyan')); + console.log(info(`Force reinstall from @${targetTag} channel...`)); console.log(''); if (isNpmInstall) { @@ -70,9 +71,7 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise< } // Update available - console.log( - colored(`[i] Update available: ${updateResult.current} -> ${updateResult.latest}`, 'yellow') - ); + console.log(warn(`Update available: ${updateResult.current} -> ${updateResult.latest}`)); console.log(''); // Check if this is a downgrade (e.g., stable to older dev) @@ -84,23 +83,22 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise< // This happens when stable user requests @dev but @dev base is older if (isDowngrade && beta) { console.log( - colored( - '[!] WARNING: Downgrading from ' + + warn( + 'WARNING: Downgrading from ' + (updateResult.current || 'unknown') + ' to ' + - (updateResult.latest || 'unknown'), - 'yellow' + (updateResult.latest || 'unknown') ) ); - console.log(colored('[!] Dev channel may be behind stable.', 'yellow')); + console.log(warn('Dev channel may be behind stable.')); console.log(''); } // Show beta warning if (beta) { - console.log(colored('[!] Installing from @dev channel (unstable)', 'yellow')); - console.log(colored('[!] Not recommended for production use', 'yellow')); - console.log(colored('[!] Use `ccs update` (without --beta) to return to stable', 'cyan')); + console.log(warn('Installing from @dev channel (unstable)')); + console.log(warn('Not recommended for production use')); + console.log(info('Use `ccs update` (without --beta) to return to stable')); console.log(''); } @@ -119,9 +117,9 @@ function handleCheckFailed( isNpmInstall: boolean, targetTag: string = 'latest' ): void { - console.log(colored(`[X] ${message}`, 'red')); + console.log(fail(message)); console.log(''); - console.log(colored('[i] Possible causes:', 'yellow')); + console.log(warn('Possible causes:')); console.log(' - Network connection issues'); console.log(' - Firewall blocking requests'); console.log(' - GitHub/npm API temporarily unavailable'); @@ -149,13 +147,13 @@ function handleCheckFailed( manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`; } - console.log(colored(` ${manualCommand}`, 'yellow')); + console.log(color(` ${manualCommand}`, 'command')); } else { const isWindows = process.platform === 'win32'; if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); + console.log(color(' irm ccs.kaitran.ca/install | iex', 'command')); } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); + console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command')); } } console.log(''); @@ -173,14 +171,14 @@ function handleNoUpdate(reason: string | undefined): void { switch (reason) { case 'dismissed': message = `Update dismissed. You are on version ${version}`; - console.log(colored(`[i] ${message}`, 'yellow')); + console.log(warn(message)); break; case 'cached': message = `No updates available (cached result). You are on version ${version}`; - console.log(colored(`[i] ${message}`, 'cyan')); + console.log(info(message)); break; default: - console.log(colored(`[OK] ${message}`, 'green')); + console.log(ok(message)); } console.log(''); process.exit(0); @@ -231,9 +229,7 @@ async function performNpmUpdate( cacheArgs = ['cache', 'clean', '--force']; } - console.log( - colored(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`, 'cyan') - ); + console.log(info(`${isReinstall ? 'Reinstalling' : 'Updating'} via ${packageManager}...`)); console.log(''); const performUpdate = (): void => { @@ -244,16 +240,16 @@ async function performNpmUpdate( child.on('exit', (code) => { if (code === 0) { console.log(''); - console.log(colored(`[OK] ${isReinstall ? 'Reinstall' : 'Update'} successful!`, 'green')); + console.log(ok(`${isReinstall ? 'Reinstall' : 'Update'} successful!`)); console.log(''); - console.log(`Run ${colored('ccs --version', 'yellow')} to verify`); + console.log(`Run ${color('ccs --version', 'command')} to verify`); console.log(''); } else { console.log(''); - console.log(colored(`[X] ${isReinstall ? 'Reinstall' : 'Update'} failed`, 'red')); + console.log(fail(`${isReinstall ? 'Reinstall' : 'Update'} failed`)); console.log(''); console.log('Try manually:'); - console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow')); + console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command')); console.log(''); } process.exit(code || 0); @@ -261,35 +257,30 @@ async function performNpmUpdate( child.on('error', () => { console.log(''); - console.log( - colored( - `[X] Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`, - 'red' - ) - ); + console.log(fail(`Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`)); console.log(''); console.log('Try manually:'); - console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow')); + console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command')); console.log(''); process.exit(1); }); }; if (cacheCommand && cacheArgs) { - console.log(colored('Clearing package cache...', 'cyan')); + console.log(info('Clearing package cache...')); const cacheChild = spawn(cacheCommand, cacheArgs, { stdio: 'inherit', }); cacheChild.on('exit', (code) => { if (code !== 0) { - console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow')); + console.log(warn('Cache clearing failed, proceeding anyway...')); } performUpdate(); }); cacheChild.on('error', () => { - console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow')); + console.log(warn('Cache clearing failed, proceeding anyway...')); performUpdate(); }); } else { @@ -301,13 +292,13 @@ async function performNpmUpdate( * Handle direct install beta not supported error */ function handleDirectBetaNotSupported(): void { - console.log(colored('[X] --beta flag requires npm installation', 'red')); + console.log(fail('--beta flag requires npm installation')); console.log(''); console.log('Current installation method: direct installer'); console.log('To use beta releases, install via npm:'); console.log(''); - console.log(colored(' npm install -g @kaitranntt/ccs', 'yellow')); - console.log(colored(' ccs update --beta', 'yellow')); + console.log(color(' npm install -g @kaitranntt/ccs', 'command')); + console.log(color(' ccs update --beta', 'command')); console.log(''); console.log('Or continue using stable releases via direct installer.'); console.log(''); @@ -318,7 +309,7 @@ function handleDirectBetaNotSupported(): void { * Perform update via direct installer (curl/irm) */ async function performDirectUpdate(): Promise { - console.log(colored('Updating via installer...', 'cyan')); + console.log(info('Updating via installer...')); console.log(''); const isWindows = process.platform === 'win32'; @@ -346,19 +337,19 @@ async function performDirectUpdate(): Promise { child.on('exit', (code) => { if (code === 0) { console.log(''); - console.log(colored('[OK] Update successful!', 'green')); + console.log(ok('Update successful!')); console.log(''); - console.log(`Run ${colored('ccs --version', 'yellow')} to verify`); + console.log(`Run ${color('ccs --version', 'command')} to verify`); console.log(''); } else { console.log(''); - console.log(colored('[X] Update failed', 'red')); + console.log(fail('Update failed')); console.log(''); console.log('Try manually:'); if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); + console.log(color(' irm ccs.kaitran.ca/install | iex', 'command')); } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); + console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command')); } console.log(''); } @@ -367,13 +358,13 @@ async function performDirectUpdate(): Promise { child.on('error', () => { console.log(''); - console.log(colored('[X] Failed to run installer', 'red')); + console.log(fail('Failed to run installer')); console.log(''); console.log('Try manually:'); if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); + console.log(color(' irm ccs.kaitran.ca/install | iex', 'command')); } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); + console.log(color(' curl -fsSL ccs.kaitran.ca/install | bash', 'command')); } console.log(''); process.exit(1); diff --git a/src/commands/version-command.ts b/src/commands/version-command.ts index 50395af2..5c4ab2a0 100644 --- a/src/commands/version-command.ts +++ b/src/commands/version-command.ts @@ -7,29 +7,30 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import { colored } from '../utils/helpers'; +import { initUI, header, subheader, color, warn } from '../utils/ui'; import { getConfigPath } from '../utils/config-manager'; import { getVersion } from '../utils/version'; /** * Handle version command */ -export function handleVersionCommand(): void { - console.log(colored(`CCS (Claude Code Switch) v${getVersion()}`, 'bold')); +export async function handleVersionCommand(): Promise { + await initUI(); + console.log(header(`CCS (Claude Code Switch) v${getVersion()}`)); console.log(''); - console.log(colored('Installation:', 'cyan')); + console.log(subheader('Installation:')); const installLocation = process.argv[1] || '(not found)'; - console.log(` ${colored('Location:'.padEnd(17), 'cyan')} ${installLocation}`); + console.log(` ${color('Location:'.padEnd(17), 'info')} ${installLocation}`); const ccsDir = path.join(os.homedir(), '.ccs'); - console.log(` ${colored('CCS Directory:'.padEnd(17), 'cyan')} ${ccsDir}`); + console.log(` ${color('CCS Directory:'.padEnd(17), 'info')} ${ccsDir}`); const configPath = getConfigPath(); - console.log(` ${colored('Config:'.padEnd(17), 'cyan')} ${configPath}`); + console.log(` ${color('Config:'.padEnd(17), 'info')} ${configPath}`); const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json'); - console.log(` ${colored('Profiles:'.padEnd(17), 'cyan')} ${profilesJson}`); + console.log(` ${color('Profiles:'.padEnd(17), 'info')} ${profilesJson}`); // Delegation status const delegationSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json'); @@ -57,29 +58,31 @@ export function handleVersionCommand(): void { const delegationEnabled = delegationConfigured || hasValidApiKeys; if (delegationEnabled) { - console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Enabled`); + console.log(` ${color('Delegation:'.padEnd(17), 'info')} Enabled`); } else { - console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Not configured`); + console.log(` ${color('Delegation:'.padEnd(17), 'info')} Not configured`); } console.log(''); if (readyProfiles.length > 0) { - console.log(colored('Delegation Ready:', 'cyan')); + console.log(subheader('Delegation Ready:')); console.log( - ` ${colored('[OK]', 'yellow')} ${readyProfiles.join(', ')} profiles are ready for delegation` + ` ${color('[OK]', 'warning')} ${readyProfiles.join(', ')} profiles are ready for delegation` ); console.log(''); } else if (delegationEnabled) { - console.log(colored('Delegation Ready:', 'cyan')); - console.log(` ${colored('[!]', 'yellow')} Delegation configured but no valid API keys found`); + console.log(subheader('Delegation Ready:')); + console.log(warn('Delegation configured but no valid API keys found')); console.log(''); } - console.log(`${colored('Documentation:', 'cyan')} https://github.com/kaitranntt/ccs`); - console.log(`${colored('License:', 'cyan')} MIT`); + console.log( + `${subheader('Documentation:')} ${color('https://github.com/kaitranntt/ccs', 'path')}` + ); + console.log(`${subheader('License:')} MIT`); console.log(''); - console.log(colored("Run 'ccs --help' for usage information", 'yellow')); + console.log(color("Run 'ccs --help' for usage information", 'command')); process.exit(0); } diff --git a/src/config/migration-manager.ts b/src/config/migration-manager.ts index 88344006..bd011c79 100644 --- a/src/config/migration-manager.ts +++ b/src/config/migration-manager.ts @@ -18,6 +18,7 @@ import { getCcsDir } from '../utils/config-manager'; import type { ProfileConfig, AccountConfig, CLIProxyVariantConfig } from './unified-config-types'; import { createEmptyUnifiedConfig } from './unified-config-types'; import { saveUnifiedConfig, hasUnifiedConfig } from './unified-config-loader'; +import { infoBox, warn } from '../utils/ui'; const BACKUP_DIR_PREFIX = 'backup-v1-'; @@ -343,23 +344,19 @@ export async function autoMigrate(): Promise { if (result.success) { console.log(''); - console.log('╭─────────────────────────────────────────────────────────╮'); - console.log('│ [OK] Migrated to unified config (config.yaml) │'); - console.log('╰─────────────────────────────────────────────────────────╯'); + console.log(infoBox('Migrated to unified config (config.yaml)', 'SUCCESS')); console.log(` Backup: ${result.backupPath}`); console.log(` Items: ${result.migratedFiles.length} migrated`); if (result.warnings.length > 0) { for (const warning of result.warnings) { - console.log(` [!] ${warning}`); + console.log(warn(warning)); } } console.log(` Rollback: ccs migrate --rollback ${result.backupPath}`); console.log(''); } else { console.log(''); - console.log('╭─────────────────────────────────────────────────────────╮'); - console.log('│ [!] Migration failed - using legacy config │'); - console.log('╰─────────────────────────────────────────────────────────╯'); + console.log(infoBox('Migration failed - using legacy config', 'WARNING')); console.log(` Error: ${result.error}`); console.log(' Retry: ccs migrate'); console.log(''); diff --git a/src/delegation/delegation-handler.ts b/src/delegation/delegation-handler.ts index 868b39cf..bfe4aa61 100644 --- a/src/delegation/delegation-handler.ts +++ b/src/delegation/delegation-handler.ts @@ -5,6 +5,7 @@ import { SessionManager } from './session-manager'; import { ResultFormatter } from './result-formatter'; import { DelegationValidator } from '../utils/delegation-validator'; import { SettingsParser } from './settings-parser'; +import { fail } from '../utils/ui'; interface ParsedArgs { profile: string; @@ -52,7 +53,7 @@ export class DelegationHandler { // 6. Exit with proper code process.exit(result.exitCode || 0); } catch (error) { - console.error(`[X] Delegation error: ${(error as Error).message}`); + console.error(fail(`Delegation error: ${(error as Error).message}`)); if (process.env.CCS_DEBUG) { console.error((error as Error).stack); } @@ -72,7 +73,7 @@ export class DelegationHandler { const lastSession = sessionMgr.getLastSession(baseProfile); if (!lastSession) { - console.error(`[X] No previous session found for ${baseProfile}`); + console.error(fail(`No previous session found for ${baseProfile}`)); console.error(` Start a new session first with: ccs ${baseProfile} -p "task"`); process.exit(1); } @@ -148,7 +149,7 @@ export class DelegationHandler { const index = pIndex !== -1 ? pIndex : promptIndex; if (index === -1 || index === args.length - 1) { - console.error('[X] Missing prompt after -p flag'); + console.error(fail('Missing prompt after -p flag')); console.error(' Usage: ccs glm -p "task description"'); process.exit(1); } @@ -228,7 +229,7 @@ export class DelegationHandler { */ _validateProfile(profile: string): void { if (!profile) { - console.error('[X] No profile specified'); + console.error(fail('No profile specified')); console.error(' Usage: ccs -p "task"'); console.error(' Examples: ccs glm -p "task", ccs kimi -p "task"'); process.exit(1); @@ -237,7 +238,7 @@ export class DelegationHandler { // Use DelegationValidator to check profile const validation = DelegationValidator.validate(profile); if (!validation.valid) { - console.error(`[X] Profile '${profile}' is not configured for delegation`); + console.error(fail(`Profile '${profile}' is not configured for delegation`)); console.error(` ${validation.error}`); console.error(''); console.error(' Run: ccs doctor'); diff --git a/src/delegation/headless-executor.ts b/src/delegation/headless-executor.ts index 0416f470..c0217593 100644 --- a/src/delegation/headless-executor.ts +++ b/src/delegation/headless-executor.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as fs from 'fs'; import { SessionManager } from './session-manager'; import { SettingsParser } from './settings-parser'; -import { ui } from '../utils/ui'; +import { ui, warn, info } from '../utils/ui'; // Type definitions for delegation responses interface ClaudeMessage { @@ -157,9 +157,9 @@ export class HeadlessExecutor { args.push('--dangerously-skip-permissions'); // Warn about dangerous mode if (process.env.CCS_DEBUG) { - console.warn('[!] WARNING: Using --dangerously-skip-permissions mode'); + console.warn(warn('WARNING: Using --dangerously-skip-permissions mode')); console.warn( - '[!] This bypasses ALL permission checks. Use only in trusted environments.' + warn('This bypasses ALL permission checks. Use only in trusted environments.') ); } } else { @@ -179,21 +179,23 @@ export class HeadlessExecutor { ? lastSession.totalCost.toFixed(4) : '0.0000'; console.error( - `[i] Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${cost})` + info( + `Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${cost})` + ) ); } } else if (sessionId) { args.push('--resume', sessionId); if (process.env.CCS_DEBUG) { - console.error(`[i] Resuming specific session: ${sessionId}`); + console.error(info(`Resuming specific session: ${sessionId}`)); } } else { - console.warn('[!] No previous session found, starting new session'); + console.warn(warn('No previous session found, starting new session')); } } else if (sessionId) { args.push('--resume', sessionId); if (process.env.CCS_DEBUG) { - console.error(`[i] Resuming specific session: ${sessionId}`); + console.error(info(`Resuming specific session: ${sessionId}`)); } } @@ -219,7 +221,7 @@ export class HeadlessExecutor { // Debug log args if (process.env.CCS_DEBUG) { - console.error(`[i] Claude CLI args: ${args.join(' ')}`); + console.error(info(`Claude CLI args: ${args.join(' ')}`)); } // Initialize UI before spawning @@ -256,7 +258,7 @@ export class HeadlessExecutor { const cleanupHandler = () => { if (!proc.killed) { if (process.env.CCS_DEBUG) { - console.error('[!] Parent process terminating, killing delegated session...'); + console.error(warn('Parent process terminating, killing delegated session...')); } proc.kill('SIGTERM'); // Force kill if not dead after 2s @@ -420,7 +422,7 @@ export class HeadlessExecutor { // Skip malformed JSON lines (shouldn't happen with stream-json) if (process.env.CCS_DEBUG) { console.error( - `[!] Failed to parse stream-json line: ${(parseError as Error).message}` + warn(`Failed to parse stream-json line: ${(parseError as Error).message}`) ); } } @@ -495,7 +497,7 @@ export class HeadlessExecutor { // Fallback: no result message found (shouldn't happen) result.content = stdout; if (process.env.CCS_DEBUG) { - console.error(`[!] No result message found in stream-json output`); + console.error(warn('No result message found in stream-json output')); } } @@ -547,7 +549,7 @@ export class HeadlessExecutor { if (process.env.CCS_DEBUG) { console.error( - `[!] Timeout reached after ${timeout}ms, sending SIGTERM for graceful shutdown...` + warn(`Timeout reached after ${timeout}ms, sending SIGTERM for graceful shutdown...`) ); } @@ -558,7 +560,7 @@ export class HeadlessExecutor { setTimeout(() => { if (!proc.killed) { if (process.env.CCS_DEBUG) { - console.error(`[!] Process did not terminate gracefully, sending SIGKILL...`); + console.error(warn('Process did not terminate gracefully, sending SIGKILL...')); } proc.kill('SIGKILL'); } @@ -632,7 +634,7 @@ export class HeadlessExecutor { // If not last attempt, retry if (attempt < maxRetries) { - console.error(`[!] Attempt ${attempt + 1} failed, retrying...`); + console.error(warn(`Attempt ${attempt + 1} failed, retrying...`)); await this._sleep(1000 * (attempt + 1)); // Exponential backoff continue; } @@ -644,7 +646,7 @@ export class HeadlessExecutor { if (attempt < maxRetries) { console.error( - `[!] Attempt ${attempt + 1} errored: ${(error as Error).message}, retrying...` + warn(`Attempt ${attempt + 1} errored: ${(error as Error).message}, retrying...`) ); await this._sleep(1000 * (attempt + 1)); } diff --git a/src/management/doctor.ts b/src/management/doctor.ts index 778e0a51..1844f8f3 100644 --- a/src/management/doctor.ts +++ b/src/management/doctor.ts @@ -1153,9 +1153,9 @@ class Doctor { if (fixed > 0) { console.log(''); - console.log(ok(`[OK] Fixed ${fixed} issue(s)`)); + console.log(ok(`Fixed ${fixed} issue(s)`)); } else { - console.log(info('[i] No fixable issues detected')); + console.log(info('No fixable issues detected')); } } diff --git a/src/management/recovery-manager.ts b/src/management/recovery-manager.ts index 9f5fc2f3..b886cf8f 100644 --- a/src/management/recovery-manager.ts +++ b/src/management/recovery-manager.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import { info } from '../utils/ui'; /** * Recovery Manager Class @@ -123,13 +124,13 @@ class RecoveryManager { if (this.recovered.length === 0) return; console.log(''); - console.log('[i] Auto-recovery completed:'); + console.log(info('Auto-recovery completed:')); this.recovered.forEach((msg) => console.log(` - ${msg}`)); // Show login hint if created Claude settings if (this.recovered.some((msg) => msg.includes('settings.json'))) { console.log(''); - console.log('[i] Next step: Login to Claude CLI'); + console.log(info('Next step: Login to Claude CLI')); console.log(' Run: claude /login'); } diff --git a/src/management/shared-manager.ts b/src/management/shared-manager.ts index 40640fe8..7edfcddc 100644 --- a/src/management/shared-manager.ts +++ b/src/management/shared-manager.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import { ok, info, warn } from '../utils/ui'; interface SharedItem { name: string; @@ -61,7 +62,7 @@ class SharedManager { // Check if target points back to our shared dir or link path const sharedDir = path.join(this.homeDir, '.ccs', 'shared'); if (resolvedTarget.startsWith(sharedDir) || resolvedTarget === linkPath) { - console.log(`[!] Circular symlink detected: ${target} → ${resolvedTarget}`); + console.log(warn(`Circular symlink detected: ${target} → ${resolvedTarget}`)); return true; } } catch (_err) { @@ -79,7 +80,7 @@ class SharedManager { ensureSharedDirectories(): void { // Create ~/.claude/ if missing if (!fs.existsSync(this.claudeDir)) { - console.log('[i] Creating ~/.claude/ directory structure'); + console.log(info('Creating ~/.claude/ directory structure')); fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o700 }); } @@ -105,7 +106,7 @@ class SharedManager { // Check for circular symlink if (this.detectCircularSymlink(claudePath, sharedPath)) { - console.log(`[!] Skipping ${item.name}: circular symlink detected`); + console.log(warn(`Skipping ${item.name}: circular symlink detected`)); continue; } @@ -145,7 +146,7 @@ class SharedManager { fs.copyFileSync(claudePath, sharedPath); } console.log( - `[!] Symlink failed for ${item.name}, copied instead (enable Developer Mode)` + warn(`Symlink failed for ${item.name}, copied instead (enable Developer Mode)`) ); } else { throw _err; @@ -186,7 +187,7 @@ class SharedManager { fs.copyFileSync(targetPath, linkPath); } console.log( - `[!] Symlink failed for ${item.name}, copied instead (enable Developer Mode)` + warn(`Symlink failed for ${item.name}, copied instead (enable Developer Mode)`) ); } else { throw _err; @@ -212,7 +213,7 @@ class SharedManager { } } - console.log('[i] Migrating from v3.1.1 to v3.2.0...'); + console.log(info('Migrating from v3.1.1 to v3.2.0...')); // Ensure ~/.claude/ exists if (!fs.existsSync(this.claudeDir)) { @@ -256,7 +257,7 @@ class SharedManager { } if (copied > 0) { - console.log(`[OK] Migrated ${copied} ${item.name} to ~/.claude/${item.name}`); + console.log(ok(`Migrated ${copied} ${item.name} to ~/.claude/${item.name}`)); } } @@ -265,11 +266,11 @@ class SharedManager { // Only copy if ~/.claude/ version doesn't exist if (!fs.existsSync(claudePath)) { fs.copyFileSync(sharedPath, claudePath); - console.log(`[OK] Migrated ${item.name} to ~/.claude/${item.name}`); + console.log(ok(`Migrated ${item.name} to ~/.claude/${item.name}`)); } } } catch (_err) { - console.log(`[!] Failed to migrate ${item.name}: ${(_err as Error).message}`); + console.log(warn(`Failed to migrate ${item.name}: ${(_err as Error).message}`)); } } @@ -288,7 +289,7 @@ class SharedManager { this.linkSharedDirectories(instancePath); } } catch (_err) { - console.log(`[!] Failed to update instance ${instance}: ${(_err as Error).message}`); + console.log(warn(`Failed to update instance ${instance}: ${(_err as Error).message}`)); } } } catch (_err) { @@ -296,7 +297,7 @@ class SharedManager { } } - console.log('[OK] Migration to v3.2.0 complete'); + console.log(ok('Migration to v3.2.0 complete')); } /** @@ -304,14 +305,14 @@ class SharedManager { * Runs once on upgrade */ migrateToSharedSettings(): void { - console.log('[i] Migrating instances to shared settings.json...'); + console.log(info('Migrating instances to shared settings.json...')); // Ensure ~/.claude/settings.json exists (authoritative source) const claudeSettings = path.join(this.claudeDir, 'settings.json'); if (!fs.existsSync(claudeSettings)) { // Create empty settings if missing fs.writeFileSync(claudeSettings, JSON.stringify({}, null, 2), 'utf8'); - console.log('[i] Created ~/.claude/settings.json'); + console.log(info('Created ~/.claude/settings.json')); } // Ensure shared settings.json symlink exists @@ -319,7 +320,7 @@ class SharedManager { // Migrate each instance if (!fs.existsSync(this.instancesDir)) { - console.log('[i] No instances to migrate'); + console.log(info('No instances to migrate')); return; } @@ -348,7 +349,7 @@ class SharedManager { const backup = instanceSettings + '.pre-shared-migration'; if (!fs.existsSync(backup)) { fs.copyFileSync(instanceSettings, backup); - console.log(`[i] Backed up ${instance}/settings.json`); + console.log(info(`Backed up ${instance}/settings.json`)); } // Remove old settings.json @@ -365,18 +366,18 @@ class SharedManager { // Windows fallback if (process.platform === 'win32') { fs.copyFileSync(sharedSettings, instanceSettings); - console.log(`[!] Symlink failed for ${instance}, copied instead`); + console.log(warn(`Symlink failed for ${instance}, copied instead`)); migrated++; } else { throw _err; } } } catch (_err) { - console.log(`[!] Failed to migrate ${instance}: ${(_err as Error).message}`); + console.log(warn(`Failed to migrate ${instance}: ${(_err as Error).message}`)); } } - console.log(`[OK] Migrated ${migrated} instance(s), skipped ${skipped}`); + console.log(ok(`Migrated ${migrated} instance(s), skipped ${skipped}`)); } /** diff --git a/src/utils/claude-dir-installer.ts b/src/utils/claude-dir-installer.ts index 58d2cb54..9664f241 100644 --- a/src/utils/claude-dir-installer.ts +++ b/src/utils/claude-dir-installer.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { colored } from './helpers'; +import { ok, warn } from './ui'; // Ora fallback type for when ora is not available interface OraSpinner { @@ -117,7 +117,7 @@ export class ClaudeDirInstaller { const msg = `Copied .claude/ items (${itemCount.files} files, ${itemCount.dirs} directories)`; if (spinner) { - spinner.succeed(colored('[OK]', 'green') + ` ${msg}`); + spinner.succeed(ok(msg)); } else { console.log(`[OK] ${msg}`); } @@ -126,7 +126,7 @@ export class ClaudeDirInstaller { const error = err as Error; const msg = `Failed to copy .claude/ directory: ${error.message}`; if (spinner) { - spinner.fail(colored('[!]', 'yellow') + ` ${msg}`); + spinner.fail(warn(msg)); console.warn(' CCS items may not be available'); } else { console.warn(`[!] ${msg}`); diff --git a/src/utils/claude-symlink-manager.ts b/src/utils/claude-symlink-manager.ts index 29c98260..dc252610 100644 --- a/src/utils/claude-symlink-manager.ts +++ b/src/utils/claude-symlink-manager.ts @@ -15,7 +15,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { colored } from './helpers'; +import { ok, color } from './ui'; // Ora fallback type for when ora is not available interface OraSpinner { @@ -121,7 +121,7 @@ export class ClaudeSymlinkManager { const msg = `${installed}/${this.ccsItems.length} items installed to ~/.claude/`; if (spinner) { - spinner.succeed(colored('[OK]', 'green') + ` ${msg}`); + spinner.succeed(ok(msg)); } else { console.log(`[OK] ${msg}`); } @@ -395,7 +395,7 @@ export class ClaudeSymlinkManager { */ sync(): void { console.log(''); - console.log(colored('Syncing CCS Components...', 'cyan')); + console.log(color('Syncing CCS Components...', 'info')); console.log(''); this.install(false); } diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 5049e587..23504e9a 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -2,8 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { Config, isConfig, Settings, isSettings } from '../types'; -import { expandPath } from './helpers'; -import { error } from './helpers'; +import { expandPath, error } from './helpers'; +import { info } from './ui'; // TODO: Replace with proper imports after converting these files // const { ErrorManager } = require('./error-manager'); @@ -107,7 +107,7 @@ export function getSettingsPath(profile: string): string { // const recovery = new RecoveryManager(); // recovery.ensureClaudeSettings(); - console.log('[i] Auto-created missing settings file'); + console.log(info('Auto-created missing settings file')); } else { error(`Settings file not found: ${expandedPath}`); } diff --git a/src/utils/update-checker.ts b/src/utils/update-checker.ts index 95d1076b..5a594813 100644 --- a/src/utils/update-checker.ts +++ b/src/utils/update-checker.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as https from 'https'; -import { colored } from './helpers'; +import { warn, color } from './ui'; const CACHE_DIR = path.join(os.homedir(), '.ccs', 'cache'); const UPDATE_CHECK_FILE = path.join(CACHE_DIR, 'update-check.json'); @@ -305,13 +305,11 @@ export async function checkForUpdates( */ export function showUpdateNotification(updateInfo: { current: string; latest: string }): void { console.log(''); - console.log(colored('═══════════════════════════════════════════════════════', 'cyan')); - console.log( - colored(` Update available: ${updateInfo.current} → ${updateInfo.latest}`, 'yellow') - ); - console.log(colored('═══════════════════════════════════════════════════════', 'cyan')); + console.log(color('═══════════════════════════════════════════════════════', 'info')); + console.log(warn(`Update available: ${updateInfo.current} → ${updateInfo.latest}`)); + console.log(color('═══════════════════════════════════════════════════════', 'info')); console.log(''); - console.log(` Run ${colored('ccs update', 'yellow')} to update`); + console.log(` Run ${color('ccs update', 'command')} to update`); console.log(''); } diff --git a/src/web-server/shutdown.ts b/src/web-server/shutdown.ts index 37b78899..5420fe5a 100644 --- a/src/web-server/shutdown.ts +++ b/src/web-server/shutdown.ts @@ -7,6 +7,7 @@ import { Server as HTTPServer } from 'http'; import { WebSocketServer } from 'ws'; +import { ok, info, warn } from '../utils/ui'; const SHUTDOWN_TIMEOUT = 10_000; // 10 seconds @@ -19,7 +20,7 @@ export function setupGracefulShutdown( cleanup?: () => void ): void { const shutdown = () => { - console.log('\n[i] Shutting down gracefully...'); + console.log('\n' + info('Shutting down gracefully...')); // Run cleanup first (closes file watchers + WebSocket clients) if (cleanup) { @@ -28,13 +29,13 @@ export function setupGracefulShutdown( // Close HTTP server server.close(() => { - console.log('[OK] Server closed'); + console.log(ok('Server closed')); process.exit(0); }); // Force shutdown if graceful shutdown takes too long setTimeout(() => { - console.log('[!] Force shutdown (timeout exceeded)'); + console.log(warn('Force shutdown (timeout exceeded)')); process.exit(1); }, SHUTDOWN_TIMEOUT); }; diff --git a/src/web-server/usage-disk-cache.ts b/src/web-server/usage-disk-cache.ts index c1ba8ee6..8a2a947d 100644 --- a/src/web-server/usage-disk-cache.ts +++ b/src/web-server/usage-disk-cache.ts @@ -12,6 +12,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import type { DailyUsage, MonthlyUsage, SessionUsage } from './usage-types'; +import { ok, info, warn } from '../utils/ui'; // Cache configuration const CCS_DIR = path.join(os.homedir(), '.ccs'); @@ -58,7 +59,7 @@ export function readDiskCache(): UsageDiskCache | null { // Version check - invalidate if schema changed if (cache.version !== CACHE_VERSION) { - console.log('[i] Cache version mismatch, will refresh'); + console.log(info('Cache version mismatch, will refresh')); return null; } @@ -66,7 +67,7 @@ export function readDiskCache(): UsageDiskCache | null { return cache; } catch (err) { // Cache corrupted or unreadable - treat as miss - console.log('[i] Cache read failed, will refresh:', (err as Error).message); + console.log(info('Cache read failed, will refresh:') + ` ${(err as Error).message}`); return null; } } @@ -113,10 +114,10 @@ export function writeDiskCache( fs.writeFileSync(tempFile, JSON.stringify(cache), 'utf-8'); fs.renameSync(tempFile, CACHE_FILE); - console.log('[OK] Disk cache updated'); + console.log(ok('Disk cache updated')); } catch (err) { // Non-fatal - we can still serve from memory - console.log('[!] Failed to write disk cache:', (err as Error).message); + console.log(warn('Failed to write disk cache:') + ` ${(err as Error).message}`); } } @@ -143,9 +144,9 @@ export function clearDiskCache(): void { try { if (fs.existsSync(CACHE_FILE)) { fs.unlinkSync(CACHE_FILE); - console.log('[OK] Disk cache cleared'); + console.log(ok('Disk cache cleared')); } } catch (err) { - console.log('[!] Failed to clear disk cache:', (err as Error).message); + console.log(warn('Failed to clear disk cache:') + ` ${(err as Error).message}`); } } diff --git a/src/web-server/usage-routes.ts b/src/web-server/usage-routes.ts index fa98e9b7..803ba9e0 100644 --- a/src/web-server/usage-routes.ts +++ b/src/web-server/usage-routes.ts @@ -38,6 +38,7 @@ import { clearDiskCache, getCacheAge, } from './usage-disk-cache'; +import { ok, info, fail } from '../utils/ui'; // ============================================================================ // Multi-Instance Support - Aggregate usage from CCS profiles @@ -66,7 +67,7 @@ function getInstancePaths(): string[] { return fs.existsSync(projectsPath); }); } catch { - console.error('[!] Failed to read CCS instances directory'); + console.error(fail('Failed to read CCS instances directory')); return []; } } @@ -87,7 +88,7 @@ async function loadInstanceData(instancePath: string): Promise<{ } catch (_err) { // Instance may have no usage data - that's OK const instanceName = path.basename(instancePath); - console.log(`[i] No usage data in instance: ${instanceName}`); + console.log(info(`No usage data in instance: ${instanceName}`)); return { daily: [], monthly: [], session: [] }; } } @@ -283,7 +284,7 @@ async function getCachedData(key: string, ttl: number, loader: () => Promise< persistCacheIfComplete(); }) .catch((err) => { - console.error(`[!] Background refresh failed for ${key}:`, err); + console.error(fail(`Background refresh failed for ${key}: ${err}`)); }) .finally(() => { pendingRequests.delete(key); @@ -376,7 +377,7 @@ async function refreshFromSource(): Promise<{ instanceDataResults.push(data); } catch (err) { const instanceName = path.basename(instancePath); - console.error(`[!] Failed to load instance ${instanceName}:`, err); + console.error(fail(`Failed to load instance ${instanceName}: ${err}`)); } } @@ -392,7 +393,7 @@ async function refreshFromSource(): Promise<{ } if (instanceDataResults.length > 0) { - console.log(`[i] Aggregated usage data from ${instanceDataResults.length} CCS instance(s)`); + console.log(info(`Aggregated usage data from ${instanceDataResults.length} CCS instance(s)`)); } // Merge all data sources @@ -453,7 +454,7 @@ export async function prewarmUsageCache(): Promise<{ source: string; }> { const start = Date.now(); - console.log('[i] Pre-warming usage cache...'); + console.log(info('Pre-warming usage cache...')); try { const diskCache = readDiskCache(); @@ -468,7 +469,7 @@ export async function prewarmUsageCache(): Promise<{ const elapsed = Date.now() - start; console.log( - `[OK] Usage cache ready from disk (${elapsed}ms, cached ${getCacheAge(diskCache)})` + ok(`Usage cache ready from disk (${elapsed}ms, cached ${getCacheAge(diskCache)})`) ); return { timestamp: now, elapsed, source: 'disk-fresh' }; } @@ -483,15 +484,17 @@ export async function prewarmUsageCache(): Promise<{ const elapsed = Date.now() - start; console.log( - `[OK] Usage cache ready from disk (${elapsed}ms, stale ${getCacheAge(diskCache)}, refreshing...)` + ok( + `Usage cache ready from disk (${elapsed}ms, stale ${getCacheAge(diskCache)}, refreshing...)` + ) ); // Background refresh if (!isRefreshing) { isRefreshing = true; refreshFromSource() - .then(() => console.log('[OK] Background refresh complete')) - .catch((err) => console.error('[!] Background refresh failed:', err)) + .then(() => console.log(ok('Background refresh complete'))) + .catch((err) => console.error(fail(`Background refresh failed: ${err}`))) .finally(() => { isRefreshing = false; }); @@ -501,14 +504,14 @@ export async function prewarmUsageCache(): Promise<{ } // No usable disk cache - refresh from source (blocking for first startup only) - console.log('[i] No disk cache, loading from source...'); + console.log(info('No disk cache, loading from source...')); await refreshFromSource(); const elapsed = Date.now() - start; - console.log(`[OK] Usage cache ready (${elapsed}ms)`); + console.log(ok(`Usage cache ready (${elapsed}ms)`)); return { timestamp: Date.now(), elapsed, source: 'fresh' }; } catch (err) { - console.error('[!] Failed to prewarm usage cache:', err); + console.error(fail(`Failed to prewarm usage cache: ${err}`)); throw err; } } diff --git a/src/web-server/websocket.ts b/src/web-server/websocket.ts index 05c39a9c..9ecba5e8 100644 --- a/src/web-server/websocket.ts +++ b/src/web-server/websocket.ts @@ -6,6 +6,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import { createFileWatcher, FileChangeEvent } from './file-watcher'; +import { info, warn } from '../utils/ui'; export interface WSMessage { type: string; @@ -29,7 +30,7 @@ export function setupWebSocket(wss: WebSocketServer): { cleanup: () => void } { // Handle new connections wss.on('connection', (ws) => { clients.add(ws); - console.log(`[WS] Client connected (${clients.size} total)`); + console.log(info(`[WS] Client connected (${clients.size} total)`)); // Send welcome message ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() })); @@ -40,18 +41,18 @@ export function setupWebSocket(wss: WebSocketServer): { cleanup: () => void } { const message = JSON.parse(data.toString()); handleClientMessage(ws, message); } catch { - console.log('[WS] Invalid message format'); + console.log(warn('[WS] Invalid message format')); } }); // Handle disconnect ws.on('close', () => { clients.delete(ws); - console.log(`[WS] Client disconnected (${clients.size} remaining)`); + console.log(info(`[WS] Client disconnected (${clients.size} remaining)`)); }); ws.on('error', (err) => { - console.log(`[WS] Error: ${err.message}`); + console.log(warn(`[WS] Error: ${err.message}`)); clients.delete(ws); }); }); @@ -66,13 +67,13 @@ export function setupWebSocket(wss: WebSocketServer): { cleanup: () => void } { // Future: selective subscriptions break; default: - console.log(`[WS] Unknown message type: ${message.type}`); + console.log(warn(`[WS] Unknown message type: ${message.type}`)); } } // Setup file watcher const watcher = createFileWatcher((event: FileChangeEvent) => { - console.log(`[FS] ${event.type}: ${event.path}`); + console.log(info(`[FS] ${event.type}: ${event.path}`)); broadcast({ type: event.type, path: event.path,