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

This commit is contained in:
kaitranntt committed 2025-12-12 05:11:59 -05:00
1 parent 0b23ec2c0a
commit 4005f1c01c
25 files changed
+262 -249

No files matched your search

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