fix(ui): use UI color system for consistent CLI indicators

This commit is contained in:
kaitranntt
2025-12-22 19:41:05 -05:00
parent dbc128948e
commit 91cd9ffc16
10 changed files with 104 additions and 87 deletions
+7 -6
View File
@@ -12,6 +12,7 @@ import {
getWebSearchHookEnv, getWebSearchHookEnv,
} from './utils/websearch-manager'; } from './utils/websearch-manager';
import { getGlobalEnvConfig } from './config/unified-config-loader'; import { getGlobalEnvConfig } from './config/unified-config-loader';
import { fail, info } from './utils/ui';
// Import centralized error handling // Import centralized error handling
import { handleError, runCleanup } from './errors'; import { handleError, runCleanup } from './errors';
@@ -75,7 +76,7 @@ async function execClaudeWithProxy(
const apiKey = envData['ANTHROPIC_AUTH_TOKEN']; const apiKey = envData['ANTHROPIC_AUTH_TOKEN'];
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') { if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
console.error('[X] GLMT profile requires Z.AI API key'); console.error(fail('GLMT profile requires Z.AI API key'));
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN'); console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
process.exit(1); process.exit(1);
} }
@@ -134,7 +135,7 @@ async function execClaudeWithProxy(
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
spinner.fail('Failed to start GLMT proxy'); spinner.fail('Failed to start GLMT proxy');
console.error('[X] Error:', err.message); console.error(fail(`Error: ${err.message}`));
console.error(''); console.error('');
console.error('Possible causes:'); console.error('Possible causes:');
console.error(' 1. Port conflict (unlikely with random port)'); console.error(' 1. Port conflict (unlikely with random port)');
@@ -192,7 +193,7 @@ async function execClaudeWithProxy(
}); });
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);
}); });
@@ -475,7 +476,7 @@ async function main(): Promise<void> {
const { executeCopilotProfile } = await import('./copilot'); const { executeCopilotProfile } = await import('./copilot');
const copilotConfig = profileInfo.copilotConfig; const copilotConfig = profileInfo.copilotConfig;
if (!copilotConfig) { if (!copilotConfig) {
console.error('[X] Copilot configuration not found'); console.error(fail('Copilot configuration not found'));
process.exit(1); process.exit(1);
} }
const exitCode = await executeCopilotProfile(copilotConfig, remainingArgs); const exitCode = await executeCopilotProfile(copilotConfig, remainingArgs);
@@ -505,7 +506,7 @@ async function main(): Promise<void> {
// Log global env injection for visibility (debug mode only) // Log global env injection for visibility (debug mode only)
if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) { if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) {
const envNames = Object.keys(globalEnv).join(', '); const envNames = Object.keys(globalEnv).join(', ');
console.error(`[i] Global env: ${envNames}`); console.error(info(`Global env: ${envNames}`));
} }
// CRITICAL: Load settings and explicitly set ANTHROPIC_* env vars // CRITICAL: Load settings and explicitly set ANTHROPIC_* env vars
@@ -563,7 +564,7 @@ async function main(): Promise<void> {
const allProfiles = err.availableProfiles.split('\n'); const allProfiles = err.availableProfiles.split('\n');
await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions); await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
} else { } else {
console.error(`[X] ${err.message}`); console.error(fail(err.message));
} }
process.exit(1); process.exit(1);
} }
+1 -1
View File
@@ -616,7 +616,7 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
if (installIdx !== -1) { if (installIdx !== -1) {
const version = args[installIdx + 1]; const version = args[installIdx + 1];
if (!version || version.startsWith('-')) { if (!version || version.startsWith('-')) {
console.error('[X] Missing version argument for --install'); console.error(fail('Missing version argument for --install'));
console.error(' Usage: ccs cliproxy --install <version>'); console.error(' Usage: ccs cliproxy --install <version>');
console.error(' Example: ccs cliproxy --install 6.5.53'); console.error(' Example: ccs cliproxy --install 6.5.53');
process.exit(1); process.exit(1);
+3 -3
View File
@@ -12,7 +12,7 @@ import { startServer } from '../web-server';
import { setupGracefulShutdown } from '../web-server/shutdown'; import { setupGracefulShutdown } from '../web-server/shutdown';
import { ensureCliproxyService } from '../cliproxy/service-manager'; import { ensureCliproxyService } from '../cliproxy/service-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator'; import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config-generator';
import { initUI, header, ok, info, warn } from '../utils/ui'; import { initUI, header, ok, info, warn, fail } from '../utils/ui';
interface ConfigOptions { interface ConfigOptions {
port?: number; port?: number;
@@ -33,7 +33,7 @@ function parseArgs(args: string[]): ConfigOptions {
if (!isNaN(port) && port > 0 && port < 65536) { if (!isNaN(port) && port > 0 && port < 65536) {
result.port = port; result.port = port;
} else { } else {
console.error('[X] Invalid port number'); console.error(fail('Invalid port number'));
process.exit(1); process.exit(1);
} }
} else if (arg === '--dev') { } else if (arg === '--dev') {
@@ -137,7 +137,7 @@ export async function handleConfigCommand(args: string[]): Promise<void> {
console.log(''); console.log('');
console.log(info('Press Ctrl+C to stop')); console.log(info('Press Ctrl+C to stop'));
} catch (error) { } catch (error) {
console.error('[X] Failed to start server:', (error as Error).message); console.error(fail(`Failed to start server: ${(error as Error).message}`));
process.exit(1); process.exit(1);
} }
} }
+16 -15
View File
@@ -14,6 +14,7 @@ import {
} from '../copilot'; } from '../copilot';
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader'; import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader';
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types'; import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
import { ok, fail, info, color } from '../utils/ui';
/** /**
* Handle copilot subcommand. * Handle copilot subcommand.
@@ -42,7 +43,7 @@ export async function handleCopilotCommand(args: string[]): Promise<number> {
case '-h': case '-h':
return handleHelp(); return handleHelp();
default: default:
console.error(`[X] Unknown subcommand: ${subcommand}`); console.error(fail(`Unknown subcommand: ${subcommand}`));
console.error(''); console.error('');
return handleHelp(); return handleHelp();
} }
@@ -81,7 +82,7 @@ function handleHelp(): number {
*/ */
async function handleAuth(): Promise<number> { async function handleAuth(): Promise<number> {
if (!isCopilotApiInstalled()) { if (!isCopilotApiInstalled()) {
console.error('[X] copilot-api is not installed.'); console.error(fail('copilot-api is not installed.'));
console.error(''); console.error('');
console.error('Install with: npm install -g copilot-api'); console.error('Install with: npm install -g copilot-api');
return 1; return 1;
@@ -91,7 +92,7 @@ async function handleAuth(): Promise<number> {
if (result.success) { if (result.success) {
console.log(''); console.log('');
console.log('[OK] Authentication successful!'); console.log(ok('Authentication successful!'));
console.log(''); console.log('');
console.log('Next steps:'); console.log('Next steps:');
console.log(' 1. Enable copilot: ccs copilot enable'); console.log(' 1. Enable copilot: ccs copilot enable');
@@ -100,7 +101,7 @@ async function handleAuth(): Promise<number> {
return 0; return 0;
} else { } else {
console.error(''); console.error('');
console.error(`[X] ${result.error}`); console.error(fail(result.error || 'Authentication failed'));
return 1; return 1;
} }
} }
@@ -119,17 +120,17 @@ async function handleStatus(): Promise<number> {
console.log(''); console.log('');
// Enabled status // Enabled status
const enabledIcon = copilotConfig.enabled ? '[OK]' : '[X]'; const enabledIcon = copilotConfig.enabled ? color('[OK]', 'success') : color('[X]', 'error');
const enabledText = copilotConfig.enabled ? 'Enabled' : 'Disabled'; const enabledText = copilotConfig.enabled ? 'Enabled' : 'Disabled';
console.log(`Integration: ${enabledIcon} ${enabledText}`); console.log(`Integration: ${enabledIcon} ${enabledText}`);
// Auth status // Auth status
const authIcon = status.auth.authenticated ? '[OK]' : '[X]'; const authIcon = status.auth.authenticated ? color('[OK]', 'success') : color('[X]', 'error');
const authText = status.auth.authenticated ? 'Authenticated' : 'Not authenticated'; const authText = status.auth.authenticated ? 'Authenticated' : 'Not authenticated';
console.log(`Authentication: ${authIcon} ${authText}`); console.log(`Authentication: ${authIcon} ${authText}`);
// Daemon status // Daemon status
const daemonIcon = status.daemon.running ? '[OK]' : '[X]'; const daemonIcon = status.daemon.running ? color('[OK]', 'success') : color('[X]', 'error');
const daemonText = status.daemon.running ? 'Running' : 'Not running'; const daemonText = status.daemon.running ? 'Running' : 'Not running';
console.log(`Daemon: ${daemonIcon} ${daemonText}`); console.log(`Daemon: ${daemonIcon} ${daemonText}`);
@@ -196,15 +197,15 @@ async function handleStart(): Promise<number> {
const config = loadOrCreateUnifiedConfig(); const config = loadOrCreateUnifiedConfig();
const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG; const copilotConfig = config.copilot ?? DEFAULT_COPILOT_CONFIG;
console.log(`[i] Starting copilot-api daemon on port ${copilotConfig.port}...`); console.log(info(`Starting copilot-api daemon on port ${copilotConfig.port}...`));
const result = await startDaemon(copilotConfig); const result = await startDaemon(copilotConfig);
if (result.success) { if (result.success) {
console.log(`[OK] Daemon started (PID: ${result.pid})`); console.log(ok(`Daemon started (PID: ${result.pid})`));
return 0; return 0;
} else { } else {
console.error(`[X] ${result.error}`); console.error(fail(result.error || 'Failed to start daemon'));
return 1; return 1;
} }
} }
@@ -213,15 +214,15 @@ async function handleStart(): Promise<number> {
* Handle stop subcommand. * Handle stop subcommand.
*/ */
async function handleStop(): Promise<number> { async function handleStop(): Promise<number> {
console.log('[i] Stopping copilot-api daemon...'); console.log(info('Stopping copilot-api daemon...'));
const result = await stopDaemon(); const result = await stopDaemon();
if (result.success) { if (result.success) {
console.log('[OK] Daemon stopped'); console.log(ok('Daemon stopped'));
return 0; return 0;
} else { } else {
console.error(`[X] ${result.error}`); console.error(fail(result.error || 'Failed to stop daemon'));
return 1; return 1;
} }
} }
@@ -239,7 +240,7 @@ async function handleEnable(): Promise<number> {
config.copilot.enabled = true; config.copilot.enabled = true;
saveUnifiedConfig(config); saveUnifiedConfig(config);
console.log('[OK] Copilot integration enabled'); console.log(ok('Copilot integration enabled'));
console.log(''); console.log('');
console.log('Next steps:'); console.log('Next steps:');
console.log(' 1. Authenticate: ccs copilot auth'); console.log(' 1. Authenticate: ccs copilot auth');
@@ -260,7 +261,7 @@ async function handleDisable(): Promise<number> {
saveUnifiedConfig(config); saveUnifiedConfig(config);
} }
console.log('[OK] Copilot integration disabled'); console.log(ok('Copilot integration disabled'));
return 0; return 0;
} }
+10 -9
View File
@@ -12,6 +12,7 @@ import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
import { isDaemonRunning, startDaemon } from './copilot-daemon'; import { isDaemonRunning, startDaemon } from './copilot-daemon';
import { ensureCopilotApi } from './copilot-package-manager'; import { ensureCopilotApi } from './copilot-package-manager';
import { CopilotStatus } from './types'; import { CopilotStatus } from './types';
import { fail, info, ok } from '../utils/ui';
/** /**
* Get full copilot status (auth + daemon). * Get full copilot status (auth + daemon).
@@ -73,7 +74,7 @@ export async function executeCopilotProfile(
try { try {
await ensureCopilotApi(); await ensureCopilotApi();
} catch (error) { } catch (error) {
console.error('[X] Failed to install copilot-api.'); console.error(fail('Failed to install copilot-api.'));
console.error(''); console.error('');
console.error(`Error: ${(error as Error).message}`); console.error(`Error: ${(error as Error).message}`);
console.error(''); console.error('');
@@ -84,7 +85,7 @@ export async function executeCopilotProfile(
// Check if copilot-api is installed (should be after ensureCopilotApi) // Check if copilot-api is installed (should be after ensureCopilotApi)
if (!isCopilotApiInstalled()) { if (!isCopilotApiInstalled()) {
console.error('[X] copilot-api is not installed.'); console.error(fail('copilot-api is not installed.'));
console.error(''); console.error('');
console.error('Install with: ccs copilot --install'); console.error('Install with: ccs copilot --install');
return 1; return 1;
@@ -93,7 +94,7 @@ export async function executeCopilotProfile(
// Check authentication // Check authentication
const authStatus = await checkAuthStatus(); const authStatus = await checkAuthStatus();
if (!authStatus.authenticated) { if (!authStatus.authenticated) {
console.error('[X] Not authenticated with GitHub.'); console.error(fail('Not authenticated with GitHub.'));
console.error(''); console.error('');
console.error('Run: npx copilot-api auth'); console.error('Run: npx copilot-api auth');
console.error('Or: ccs copilot auth'); console.error('Or: ccs copilot auth');
@@ -105,16 +106,16 @@ export async function executeCopilotProfile(
if (!daemonRunning) { if (!daemonRunning) {
if (config.auto_start) { if (config.auto_start) {
console.log('[i] Starting copilot-api daemon...'); console.log(info('Starting copilot-api daemon...'));
const result = await startDaemon(config); const result = await startDaemon(config);
if (!result.success) { if (!result.success) {
console.error(`[X] Failed to start daemon: ${result.error}`); console.error(fail(`Failed to start daemon: ${result.error}`));
return 1; return 1;
} }
console.log(`[OK] Daemon started on port ${config.port}`); console.log(ok(`Daemon started on port ${config.port}`));
daemonRunning = true; daemonRunning = true;
} else { } else {
console.error('[X] copilot-api daemon is not running.'); console.error(fail('copilot-api daemon is not running.'));
console.error(''); console.error('');
console.error('Start the daemon manually:'); console.error('Start the daemon manually:');
console.error(` npx copilot-api start --port ${config.port}`); console.error(` npx copilot-api start --port ${config.port}`);
@@ -139,7 +140,7 @@ export async function executeCopilotProfile(
...copilotEnv, ...copilotEnv,
}; };
console.log(`[i] Using GitHub Copilot proxy (model: ${config.model})`); console.log(info(`Using GitHub Copilot proxy (model: ${config.model})`));
console.log(''); console.log('');
// Spawn Claude CLI // Spawn Claude CLI
@@ -155,7 +156,7 @@ export async function executeCopilotProfile(
}); });
proc.on('error', (err) => { proc.on('error', (err) => {
console.error(`[X] Failed to start Claude: ${err.message}`); console.error(fail(`Failed to start Claude: ${err.message}`));
resolve(1); resolve(1);
}); });
}); });
+7 -5
View File
@@ -2,6 +2,8 @@
* Health Check Types and Interfaces * Health Check Types and Interfaces
*/ */
import { ok, fail, warn, info } from '../../utils/ui';
/** /**
* Spinner interface for ora or fallback * Spinner interface for ora or fallback
*/ */
@@ -98,14 +100,14 @@ export function createSpinner(): (text: string) => Spinner {
const oraModule = require('ora'); const oraModule = require('ora');
return oraModule.default || oraModule; return oraModule.default || oraModule;
} catch (_e) { } catch (_e) {
// ora not available, create fallback spinner that uses console.log // ora not available, create fallback spinner that uses console.log with UI colors
return function (text: string): Spinner { return function (text: string): Spinner {
return { return {
start: () => ({ start: () => ({
succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), succeed: (msg?: string) => console.log(msg || ok(text)),
fail: (msg?: string) => console.log(msg || `[X] ${text}`), fail: (msg?: string) => console.log(msg || fail(text)),
warn: (msg?: string) => console.log(msg || `[!] ${text}`), warn: (msg?: string) => console.log(msg || warn(text)),
info: (msg?: string) => console.log(msg || `[i] ${text}`), info: (msg?: string) => console.log(msg || info(text)),
text: '', text: '',
}), }),
}; };
+18 -16
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 { ok, warn } from './ui'; import { ok, fail, warn, info } from './ui';
// Ora fallback type for when ora is not available // Ora fallback type for when ora is not available
interface OraSpinner { interface OraSpinner {
@@ -27,14 +27,14 @@ try {
const oraModule = require('ora'); const oraModule = require('ora');
ora = oraModule.default || oraModule; ora = oraModule.default || oraModule;
} catch { } catch {
// ora not available, create fallback spinner that uses console.log // ora not available, create fallback spinner that uses console.log with UI colors
ora = function (text: string): OraInstance { ora = function (text: string): OraInstance {
return { return {
start: () => ({ start: () => ({
succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), succeed: (msg?: string) => console.log(msg || ok(text)),
fail: (msg?: string) => console.log(msg || `[X] ${text}`), fail: (msg?: string) => console.log(msg || fail(text)),
warn: (msg?: string) => console.log(msg || `[!] ${text}`), warn: (msg?: string) => console.log(msg || warn(text)),
info: (msg?: string) => console.log(msg || `[i] ${text}`), info: (msg?: string) => console.log(msg || info(text)),
text: '', text: '',
}), }),
}; };
@@ -85,11 +85,11 @@ export class ClaudeDirInstaller {
if (!fs.existsSync(packageClaudeDir)) { if (!fs.existsSync(packageClaudeDir)) {
const msg = 'Package .claude/ directory not found'; const msg = 'Package .claude/ directory not found';
if (spinner) { if (spinner) {
spinner.warn(`[!] ${msg}`); spinner.warn(warn(msg));
console.log(` Searched in: ${packageClaudeDir}`); console.log(` Searched in: ${packageClaudeDir}`);
console.log(' This may be a development installation'); console.log(' This may be a development installation');
} else { } else {
console.log(`[!] ${msg}`); console.log(warn(msg));
console.log(` Searched in: ${packageClaudeDir}`); console.log(` Searched in: ${packageClaudeDir}`);
console.log(' This may be a development installation'); console.log(' This may be a development installation');
} }
@@ -119,7 +119,7 @@ export class ClaudeDirInstaller {
if (spinner) { if (spinner) {
spinner.succeed(ok(msg)); spinner.succeed(ok(msg));
} else { } else {
console.log(`[OK] ${msg}`); console.log(ok(msg));
} }
return true; return true;
} catch (err) { } catch (err) {
@@ -129,7 +129,7 @@ export class ClaudeDirInstaller {
spinner.fail(warn(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(warn(msg));
console.warn(' CCS items may not be available'); console.warn(' CCS items may not be available');
} }
return false; return false;
@@ -223,14 +223,14 @@ export class ClaudeDirInstaller {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
const backupPath = `${userSymlinkFile}.backup-${timestamp}`; const backupPath = `${userSymlinkFile}.backup-${timestamp}`;
fs.renameSync(userSymlinkFile, backupPath); fs.renameSync(userSymlinkFile, backupPath);
if (!silent) console.log(`[i] Backed up user file to ${path.basename(backupPath)}`); if (!silent) console.log(info(`Backed up user file to ${path.basename(backupPath)}`));
cleanedFiles.push('user file (backed up)'); cleanedFiles.push('user file (backed up)');
} }
} catch (err) { } catch (err) {
const error = err as NodeJS.ErrnoException; const error = err as NodeJS.ErrnoException;
// File doesn't exist or other error - that's okay // File doesn't exist or other error - that's okay
if (error.code !== 'ENOENT' && !silent) { if (error.code !== 'ENOENT' && !silent) {
console.log(`[!] Failed to remove user symlink: ${error.message}`); console.log(warn(`Failed to remove user symlink: ${error.message}`));
} }
} }
@@ -245,14 +245,16 @@ export class ClaudeDirInstaller {
const backupPath = `${deprecatedFile}.backup-${timestamp}`; const backupPath = `${deprecatedFile}.backup-${timestamp}`;
fs.renameSync(deprecatedFile, backupPath); fs.renameSync(deprecatedFile, backupPath);
if (!silent) if (!silent)
console.log(`[i] Backed up modified deprecated file to ${path.basename(backupPath)}`); console.log(
info(`Backed up modified deprecated file to ${path.basename(backupPath)}`)
);
} else { } else {
fs.rmSync(deprecatedFile, { force: true }); fs.rmSync(deprecatedFile, { force: true });
} }
cleanedFiles.push('package copy'); cleanedFiles.push('package copy');
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
if (!silent) console.log(`[!] Failed to remove package copy: ${error.message}`); if (!silent) console.log(warn(`Failed to remove package copy: ${error.message}`));
} }
} }
@@ -265,14 +267,14 @@ export class ClaudeDirInstaller {
fs.writeFileSync(migrationMarker, new Date().toISOString()); fs.writeFileSync(migrationMarker, new Date().toISOString());
if (!silent) { if (!silent) {
console.log(`[OK] Cleaned up deprecated agent files: ${cleanedFiles.join(', ')}`); console.log(ok(`Cleaned up deprecated agent files: ${cleanedFiles.join(', ')}`));
} }
} }
return { success: true, cleanedFiles }; return { success: true, cleanedFiles };
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
if (!silent) console.log(`[!] Cleanup failed: ${error.message}`); if (!silent) console.log(warn(`Cleanup failed: ${error.message}`));
return { success: false, error: error.message, cleanedFiles }; return { success: false, error: error.message, cleanedFiles };
} }
} }
+23 -22
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 { ok, color } from './ui'; import { ok, fail, warn, info, 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 {
@@ -36,14 +36,14 @@ try {
const oraModule = require('ora'); const oraModule = require('ora');
ora = oraModule.default || oraModule; ora = oraModule.default || oraModule;
} catch { } catch {
// ora not available, create fallback spinner that uses console.log // ora not available, create fallback spinner that uses console.log with UI colors
ora = function (text: string): OraInstance { ora = function (text: string): OraInstance {
return { return {
start: () => ({ start: () => ({
succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), succeed: (msg?: string) => console.log(msg || ok(text)),
fail: (msg?: string) => console.log(msg || `[X] ${text}`), fail: (msg?: string) => console.log(msg || fail(text)),
warn: (msg?: string) => console.log(msg || `[!] ${text}`), warn: (msg?: string) => console.log(msg || warn(text)),
info: (msg?: string) => console.log(msg || `[i] ${text}`), info: (msg?: string) => console.log(msg || info(text)),
text: '', text: '',
}), }),
}; };
@@ -94,9 +94,9 @@ export class ClaudeSymlinkManager {
if (!fs.existsSync(this.ccsClaudeDir)) { if (!fs.existsSync(this.ccsClaudeDir)) {
const msg = 'CCS .claude/ directory not found, skipping symlink installation'; const msg = 'CCS .claude/ directory not found, skipping symlink installation';
if (spinner) { if (spinner) {
spinner.warn(`[!] ${msg}`); spinner.warn(warn(msg));
} else { } else {
console.log(`[!] ${msg}`); console.log(warn(msg));
} }
return; return;
} }
@@ -123,7 +123,7 @@ export class ClaudeSymlinkManager {
if (spinner) { if (spinner) {
spinner.succeed(ok(msg)); spinner.succeed(ok(msg));
} else { } else {
console.log(`[OK] ${msg}`); console.log(ok(msg));
} }
} }
@@ -137,7 +137,7 @@ export class ClaudeSymlinkManager {
// Ensure source exists // Ensure source exists
if (!fs.existsSync(sourcePath)) { if (!fs.existsSync(sourcePath)) {
if (!silent) console.log(`[!] Source not found: ${item.source}, skipping`); if (!silent) console.log(warn(`Source not found: ${item.source}, skipping`));
return false; return false;
} }
@@ -161,7 +161,7 @@ export class ClaudeSymlinkManager {
try { try {
const symlinkType = item.type === 'directory' ? 'dir' : 'file'; const symlinkType = item.type === 'directory' ? 'dir' : 'file';
fs.symlinkSync(sourcePath, targetPath, symlinkType); fs.symlinkSync(sourcePath, targetPath, symlinkType);
if (!silent) console.log(`[OK] Symlinked ${item.target}`); if (!silent) console.log(ok(`Symlinked ${item.target}`));
return true; return true;
} catch (err) { } catch (err) {
// Windows fallback: copy instead of symlink when symlinks unavailable // Windows fallback: copy instead of symlink when symlinks unavailable
@@ -169,7 +169,7 @@ export class ClaudeSymlinkManager {
return this.copyFallback(sourcePath, targetPath, item, silent); return this.copyFallback(sourcePath, targetPath, item, silent);
} else { } else {
const error = err as Error; const error = err as Error;
if (!silent) console.log(`[!] Failed to symlink ${item.target}: ${error.message}`); if (!silent) console.log(warn(`Failed to symlink ${item.target}: ${error.message}`));
} }
return false; return false;
} }
@@ -194,15 +194,15 @@ export class ClaudeSymlinkManager {
fs.copyFileSync(sourcePath, targetPath); fs.copyFileSync(sourcePath, targetPath);
} }
if (!silent) { if (!silent) {
console.log(`[OK] Copied ${item.target} (symlink unavailable)`); console.log(ok(`Copied ${item.target} (symlink unavailable)`));
console.log(`[i] Run 'ccs sync' after CCS updates to refresh`); console.log(info("Run 'ccs sync' after CCS updates to refresh"));
} }
return true; return true;
} catch (copyErr) { } catch (copyErr) {
const error = copyErr as Error; const error = copyErr as Error;
if (!silent) { if (!silent) {
console.log(`[!] Failed to copy ${item.target}: ${error.message}`); console.log(warn(`Failed to copy ${item.target}: ${error.message}`));
console.log(`[i] Enable Developer Mode for symlinks, or check permissions`); console.log(info('Enable Developer Mode for symlinks, or check permissions'));
} }
return false; return false;
} }
@@ -267,10 +267,11 @@ export class ClaudeSymlinkManager {
} }
fs.renameSync(itemPath, finalBackupPath); fs.renameSync(itemPath, finalBackupPath);
if (!silent) console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`); if (!silent)
console.log(info(`Backed up existing item to ${path.basename(finalBackupPath)}`));
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
if (!silent) console.log(`[!] Failed to backup ${itemPath}: ${error.message}`); if (!silent) console.log(warn(`Failed to backup ${itemPath}: ${error.message}`));
throw err; // Don't proceed if backup fails throw err; // Don't proceed if backup fails
} }
} }
@@ -300,19 +301,19 @@ export class ClaudeSymlinkManager {
// Remove symlink or file // Remove symlink or file
fs.unlinkSync(targetPath); fs.unlinkSync(targetPath);
} }
console.log(`[OK] Removed ${item.target}`); console.log(ok(`Removed ${item.target}`));
removed++; removed++;
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
console.log(`[!] Failed to remove ${item.target}: ${error.message}`); console.log(warn(`Failed to remove ${item.target}: ${error.message}`));
} }
} }
} }
if (removed > 0) { if (removed > 0) {
console.log(`[OK] Removed ${removed} delegation commands and skills from ~/.claude/`); console.log(ok(`Removed ${removed} delegation commands and skills from ~/.claude/`));
} else { } else {
console.log('[i] No delegation commands or skills to remove'); console.log(info('No delegation commands or skills to remove'));
} }
} }
+10 -7
View File
@@ -1,13 +1,16 @@
/** /**
* Simple Progress Indicator (no external dependencies) * Simple Progress Indicator
* *
* Features: * Features:
* - ASCII-only spinner frames (cross-platform compatible) * - ASCII-only spinner frames (cross-platform compatible)
* - TTY detection (no spinners in pipes/logs) * - TTY detection (no spinners in pipes/logs)
* - Elapsed time display * - Elapsed time display
* - CI environment detection * - CI environment detection
* - Color support via UI system
*/ */
import { color } from './ui/colors';
interface ProgressOptions { interface ProgressOptions {
frames?: string[]; frames?: string[];
interval?: number; interval?: number;
@@ -44,7 +47,7 @@ export class ProgressIndicator {
start(): void { start(): void {
if (!this.isTTY) { if (!this.isTTY) {
// Non-TTY: just print message once // Non-TTY: just print message once
process.stderr.write(`[i] ${this.message}...\n`); process.stderr.write(`${color('[i]', 'info')} ${this.message}...\n`);
return; return;
} }
@@ -52,7 +55,7 @@ export class ProgressIndicator {
this.interval = setInterval(() => { this.interval = setInterval(() => {
const frame = this.frames[this.frameIndex]; const frame = this.frames[this.frameIndex];
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1); const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
process.stderr.write(`\r[${frame}] ${this.message}... (${elapsed}s)`); process.stderr.write(`\r${color(`[${frame}]`, 'info')} ${this.message}... (${elapsed}s)`);
this.frameIndex = (this.frameIndex + 1) % this.frames.length; this.frameIndex = (this.frameIndex + 1) % this.frames.length;
}, 80); // 12.5fps for smooth animation }, 80); // 12.5fps for smooth animation
} }
@@ -68,10 +71,10 @@ export class ProgressIndicator {
if (this.isTTY) { if (this.isTTY) {
// Clear spinner line and show success // Clear spinner line and show success
process.stderr.write(`\r[OK] ${finalMessage} (${elapsed}s)\n`); process.stderr.write(`\r${color('[OK]', 'success')} ${finalMessage} (${elapsed}s)\n`);
} else { } else {
// Non-TTY: just show completion // Non-TTY: just show completion
process.stderr.write(`[OK] ${finalMessage}\n`); process.stderr.write(`${color('[OK]', 'success')} ${finalMessage}\n`);
} }
} }
@@ -85,10 +88,10 @@ export class ProgressIndicator {
if (this.isTTY) { if (this.isTTY) {
// Clear spinner line and show failure // Clear spinner line and show failure
process.stderr.write(`\r[X] ${finalMessage}\n`); process.stderr.write(`\r${color('[X]', 'error')} ${finalMessage}\n`);
} else { } else {
// Non-TTY: just show failure // Non-TTY: just show failure
process.stderr.write(`[X] ${finalMessage}\n`); process.stderr.write(`${color('[X]', 'error')} ${finalMessage}\n`);
} }
} }
+9 -3
View File
@@ -71,6 +71,7 @@ export function hasAnyWebSearchCli(): boolean {
/** /**
* Get install hints for CLI-only users when no WebSearch CLI is installed * Get install hints for CLI-only users when no WebSearch CLI is installed
* Returns raw message strings (without indicator prefix) for display
*/ */
export function getCliInstallHints(): string[] { export function getCliInstallHints(): string[] {
if (hasAnyWebSearchCli()) { if (hasAnyWebSearchCli()) {
@@ -78,7 +79,7 @@ export function getCliInstallHints(): string[] {
} }
return [ return [
'[i] WebSearch: No CLI tools installed', 'WebSearch: No CLI tools installed',
' Gemini CLI (FREE): npm i -g @google/gemini-cli', ' Gemini CLI (FREE): npm i -g @google/gemini-cli',
' OpenCode (FREE): curl -fsSL https://opencode.ai/install | bash', ' OpenCode (FREE): curl -fsSL https://opencode.ai/install | bash',
' Grok CLI (paid): npm i -g @vibe-kit/grok-cli', ' Grok CLI (paid): npm i -g @vibe-kit/grok-cli',
@@ -182,8 +183,13 @@ export function displayWebSearchStatus(): void {
console.error(fail(`WebSearch: ${status.message}`)); console.error(fail(`WebSearch: ${status.message}`));
const hints = getCliInstallHints(); const hints = getCliInstallHints();
if (hints.length > 0) { if (hints.length > 0) {
for (const hint of hints) { // First line gets [i] prefix, rest are continuation (indented, no prefix)
console.error(info(hint)); for (let i = 0; i < hints.length; i++) {
if (i === 0) {
console.error(info(hints[i]));
} else {
console.error(hints[i]);
}
} }
} }
break; break;