feat(cli): standardize UI output with ui.ts abstraction layer

This commit is contained in:
kaitranntt
2025-12-12 05:11:59 -05:00
parent 0b23ec2c0a
commit 4005f1c01c
25 changed files with 262 additions and 249 deletions
+21 -18
View File
@@ -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;
+7 -4
View File
@@ -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();
}
+14 -15
View File
@@ -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);
});
+3 -2
View File
@@ -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}`));
}
}
+10 -10
View File
@@ -945,21 +945,21 @@ async function showStatus(verbose: boolean): Promise<void> {
async function installVersion(version: string, verbose: boolean): Promise<void> {
// 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<void>
* Install latest version
*/
async function installLatest(verbose: boolean): Promise<void> {
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);
}
}
+10 -6
View File
@@ -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<void> {
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<void> {
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);
}
+21 -20
View File
@@ -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<void> {
await initUI();
// Handle --list-backups
if (args.includes('--list-backups')) {
listBackups();
@@ -31,9 +34,9 @@ export async function handleMigrateCommand(args: string[]): Promise<void> {
const backupPath = args[rollbackIndex + 1];
if (!backupPath) {
console.error('[X] Error: --rollback requires backup path');
console.log('[i] Usage: ccs migrate --rollback <backup-path>');
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 <backup-path>'));
console.log(info('Use --list-backups to see available backups'));
process.exit(1);
}
@@ -43,13 +46,13 @@ export async function handleMigrateCommand(args: string[]): Promise<void> {
// 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<void> {
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<void> {
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<void> {
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<void> {
}
async function handleRollback(backupPath: string): Promise<void> {
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 <backup-path>');
console.log(info('To rollback: ccs migrate --rollback <backup-path>'));
}
/**
+10 -9
View File
@@ -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<void> {
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<void
});
if (result.alreadyInstalled && !force) {
console.log(colored('[OK] Shell completion already installed', 'green'));
console.log(` Use ${colored('--force', 'yellow')} to reinstall`);
console.log(ok('Shell completion already installed'));
console.log(` Use ${color('--force', 'warning')} to reinstall`);
console.log('');
return;
}
console.log(colored('[OK] Shell completion installed successfully!', 'green'));
console.log(ok('Shell completion installed successfully!'));
console.log('');
console.log(result.message);
console.log('');
console.log(colored('To activate:', 'cyan'));
console.log(color('To activate:', 'info'));
console.log(` ${result.reload}`);
console.log('');
console.log(colored('Then test:', 'cyan'));
console.log(color('Then test:', 'info'));
console.log(' ccs <TAB> # See available profiles');
console.log(' ccs auth <TAB> # 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');
+5 -4
View File
@@ -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<void> {
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<void> {
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);
+42 -51
View File
@@ -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<void> {
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<void> {
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<void> {
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<void> {
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);
+20 -17
View File
@@ -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<void> {
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);
}
+4 -7
View File
@@ -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<void> {
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('');
+6 -5
View File
@@ -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 <profile> -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');
+17 -15
View File
@@ -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));
}
+2 -2
View File
@@ -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'));
}
}
+3 -2
View File
@@ -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');
}
+19 -18
View File
@@ -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}`));
}
/**
+3 -3
View File
@@ -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}`);
+3 -3
View File
@@ -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);
}
+3 -3
View File
@@ -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}`);
}
+5 -7
View File
@@ -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('');
}
+4 -3
View File
@@ -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);
};
+7 -6
View File
@@ -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}`);
}
}
+16 -13
View File
@@ -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<T>(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;
}
}
+7 -6
View File
@@ -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,