mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
feat(cli): standardize UI output with ui.ts abstraction layer
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user