From 91cd9ffc16e46737d190b3858340e7c745021ef4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 22 Dec 2025 19:41:05 -0500 Subject: [PATCH] fix(ui): use UI color system for consistent CLI indicators --- src/ccs.ts | 13 +++++---- src/commands/cliproxy-command.ts | 2 +- src/commands/config-command.ts | 6 ++-- src/commands/copilot-command.ts | 31 ++++++++++---------- src/copilot/copilot-executor.ts | 19 ++++++------ src/management/checks/types.ts | 12 ++++---- src/utils/claude-dir-installer.ts | 34 ++++++++++++---------- src/utils/claude-symlink-manager.ts | 45 +++++++++++++++-------------- src/utils/progress-indicator.ts | 17 ++++++----- src/utils/websearch/status.ts | 12 ++++++-- 10 files changed, 104 insertions(+), 87 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 8b37d138..f347819f 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -12,6 +12,7 @@ import { getWebSearchHookEnv, } from './utils/websearch-manager'; import { getGlobalEnvConfig } from './config/unified-config-loader'; +import { fail, info } from './utils/ui'; // Import centralized error handling import { handleError, runCleanup } from './errors'; @@ -75,7 +76,7 @@ async function execClaudeWithProxy( const apiKey = envData['ANTHROPIC_AUTH_TOKEN']; 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'); process.exit(1); } @@ -134,7 +135,7 @@ async function execClaudeWithProxy( } catch (error) { const err = error as Error; spinner.fail('Failed to start GLMT proxy'); - console.error('[X] Error:', err.message); + console.error(fail(`Error: ${err.message}`)); console.error(''); console.error('Possible causes:'); console.error(' 1. Port conflict (unlikely with random port)'); @@ -192,7 +193,7 @@ async function execClaudeWithProxy( }); claude.on('error', (error) => { - console.error('[X] Claude CLI error:', error); + console.error(fail(`Claude CLI error: ${error}`)); proxy.kill('SIGTERM'); process.exit(1); }); @@ -475,7 +476,7 @@ async function main(): Promise { const { executeCopilotProfile } = await import('./copilot'); const copilotConfig = profileInfo.copilotConfig; if (!copilotConfig) { - console.error('[X] Copilot configuration not found'); + console.error(fail('Copilot configuration not found')); process.exit(1); } const exitCode = await executeCopilotProfile(copilotConfig, remainingArgs); @@ -505,7 +506,7 @@ async function main(): Promise { // Log global env injection for visibility (debug mode only) if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) { 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 @@ -563,7 +564,7 @@ async function main(): Promise { const allProfiles = err.availableProfiles.split('\n'); await ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions); } else { - console.error(`[X] ${err.message}`); + console.error(fail(err.message)); } process.exit(1); } diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 83c3b5a5..2c14804b 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -616,7 +616,7 @@ export async function handleCliproxyCommand(args: string[]): Promise { if (installIdx !== -1) { const version = args[installIdx + 1]; 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 '); console.error(' Example: ccs cliproxy --install 6.5.53'); process.exit(1); diff --git a/src/commands/config-command.ts b/src/commands/config-command.ts index 838ee899..1814fa06 100644 --- a/src/commands/config-command.ts +++ b/src/commands/config-command.ts @@ -12,7 +12,7 @@ import { startServer } from '../web-server'; import { setupGracefulShutdown } from '../web-server/shutdown'; import { ensureCliproxyService } from '../cliproxy/service-manager'; 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 { port?: number; @@ -33,7 +33,7 @@ function parseArgs(args: string[]): ConfigOptions { if (!isNaN(port) && port > 0 && port < 65536) { result.port = port; } else { - console.error('[X] Invalid port number'); + console.error(fail('Invalid port number')); process.exit(1); } } else if (arg === '--dev') { @@ -137,7 +137,7 @@ export async function handleConfigCommand(args: string[]): Promise { console.log(''); console.log(info('Press Ctrl+C to stop')); } 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); } } diff --git a/src/commands/copilot-command.ts b/src/commands/copilot-command.ts index 09c0904b..306d272e 100644 --- a/src/commands/copilot-command.ts +++ b/src/commands/copilot-command.ts @@ -14,6 +14,7 @@ import { } from '../copilot'; import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader'; import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types'; +import { ok, fail, info, color } from '../utils/ui'; /** * Handle copilot subcommand. @@ -42,7 +43,7 @@ export async function handleCopilotCommand(args: string[]): Promise { case '-h': return handleHelp(); default: - console.error(`[X] Unknown subcommand: ${subcommand}`); + console.error(fail(`Unknown subcommand: ${subcommand}`)); console.error(''); return handleHelp(); } @@ -81,7 +82,7 @@ function handleHelp(): number { */ async function handleAuth(): Promise { if (!isCopilotApiInstalled()) { - console.error('[X] copilot-api is not installed.'); + console.error(fail('copilot-api is not installed.')); console.error(''); console.error('Install with: npm install -g copilot-api'); return 1; @@ -91,7 +92,7 @@ async function handleAuth(): Promise { if (result.success) { console.log(''); - console.log('[OK] Authentication successful!'); + console.log(ok('Authentication successful!')); console.log(''); console.log('Next steps:'); console.log(' 1. Enable copilot: ccs copilot enable'); @@ -100,7 +101,7 @@ async function handleAuth(): Promise { return 0; } else { console.error(''); - console.error(`[X] ${result.error}`); + console.error(fail(result.error || 'Authentication failed')); return 1; } } @@ -119,17 +120,17 @@ async function handleStatus(): Promise { console.log(''); // Enabled status - const enabledIcon = copilotConfig.enabled ? '[OK]' : '[X]'; + const enabledIcon = copilotConfig.enabled ? color('[OK]', 'success') : color('[X]', 'error'); const enabledText = copilotConfig.enabled ? 'Enabled' : 'Disabled'; console.log(`Integration: ${enabledIcon} ${enabledText}`); // 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'; console.log(`Authentication: ${authIcon} ${authText}`); // 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'; console.log(`Daemon: ${daemonIcon} ${daemonText}`); @@ -196,15 +197,15 @@ async function handleStart(): Promise { const config = loadOrCreateUnifiedConfig(); 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); if (result.success) { - console.log(`[OK] Daemon started (PID: ${result.pid})`); + console.log(ok(`Daemon started (PID: ${result.pid})`)); return 0; } else { - console.error(`[X] ${result.error}`); + console.error(fail(result.error || 'Failed to start daemon')); return 1; } } @@ -213,15 +214,15 @@ async function handleStart(): Promise { * Handle stop subcommand. */ async function handleStop(): Promise { - console.log('[i] Stopping copilot-api daemon...'); + console.log(info('Stopping copilot-api daemon...')); const result = await stopDaemon(); if (result.success) { - console.log('[OK] Daemon stopped'); + console.log(ok('Daemon stopped')); return 0; } else { - console.error(`[X] ${result.error}`); + console.error(fail(result.error || 'Failed to stop daemon')); return 1; } } @@ -239,7 +240,7 @@ async function handleEnable(): Promise { config.copilot.enabled = true; saveUnifiedConfig(config); - console.log('[OK] Copilot integration enabled'); + console.log(ok('Copilot integration enabled')); console.log(''); console.log('Next steps:'); console.log(' 1. Authenticate: ccs copilot auth'); @@ -260,7 +261,7 @@ async function handleDisable(): Promise { saveUnifiedConfig(config); } - console.log('[OK] Copilot integration disabled'); + console.log(ok('Copilot integration disabled')); return 0; } diff --git a/src/copilot/copilot-executor.ts b/src/copilot/copilot-executor.ts index 794cf8ff..d2364aba 100644 --- a/src/copilot/copilot-executor.ts +++ b/src/copilot/copilot-executor.ts @@ -12,6 +12,7 @@ import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth'; import { isDaemonRunning, startDaemon } from './copilot-daemon'; import { ensureCopilotApi } from './copilot-package-manager'; import { CopilotStatus } from './types'; +import { fail, info, ok } from '../utils/ui'; /** * Get full copilot status (auth + daemon). @@ -73,7 +74,7 @@ export async function executeCopilotProfile( try { await ensureCopilotApi(); } catch (error) { - console.error('[X] Failed to install copilot-api.'); + console.error(fail('Failed to install copilot-api.')); console.error(''); console.error(`Error: ${(error as Error).message}`); console.error(''); @@ -84,7 +85,7 @@ export async function executeCopilotProfile( // Check if copilot-api is installed (should be after ensureCopilotApi) if (!isCopilotApiInstalled()) { - console.error('[X] copilot-api is not installed.'); + console.error(fail('copilot-api is not installed.')); console.error(''); console.error('Install with: ccs copilot --install'); return 1; @@ -93,7 +94,7 @@ export async function executeCopilotProfile( // Check authentication const authStatus = await checkAuthStatus(); if (!authStatus.authenticated) { - console.error('[X] Not authenticated with GitHub.'); + console.error(fail('Not authenticated with GitHub.')); console.error(''); console.error('Run: npx copilot-api auth'); console.error('Or: ccs copilot auth'); @@ -105,16 +106,16 @@ export async function executeCopilotProfile( if (!daemonRunning) { if (config.auto_start) { - console.log('[i] Starting copilot-api daemon...'); + console.log(info('Starting copilot-api daemon...')); const result = await startDaemon(config); if (!result.success) { - console.error(`[X] Failed to start daemon: ${result.error}`); + console.error(fail(`Failed to start daemon: ${result.error}`)); return 1; } - console.log(`[OK] Daemon started on port ${config.port}`); + console.log(ok(`Daemon started on port ${config.port}`)); daemonRunning = true; } else { - console.error('[X] copilot-api daemon is not running.'); + console.error(fail('copilot-api daemon is not running.')); console.error(''); console.error('Start the daemon manually:'); console.error(` npx copilot-api start --port ${config.port}`); @@ -139,7 +140,7 @@ export async function executeCopilotProfile( ...copilotEnv, }; - console.log(`[i] Using GitHub Copilot proxy (model: ${config.model})`); + console.log(info(`Using GitHub Copilot proxy (model: ${config.model})`)); console.log(''); // Spawn Claude CLI @@ -155,7 +156,7 @@ export async function executeCopilotProfile( }); proc.on('error', (err) => { - console.error(`[X] Failed to start Claude: ${err.message}`); + console.error(fail(`Failed to start Claude: ${err.message}`)); resolve(1); }); }); diff --git a/src/management/checks/types.ts b/src/management/checks/types.ts index 4485575a..c55b0f71 100644 --- a/src/management/checks/types.ts +++ b/src/management/checks/types.ts @@ -2,6 +2,8 @@ * Health Check Types and Interfaces */ +import { ok, fail, warn, info } from '../../utils/ui'; + /** * Spinner interface for ora or fallback */ @@ -98,14 +100,14 @@ export function createSpinner(): (text: string) => Spinner { const oraModule = require('ora'); return oraModule.default || oraModule; } 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 { start: () => ({ - succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), - fail: (msg?: string) => console.log(msg || `[X] ${text}`), - warn: (msg?: string) => console.log(msg || `[!] ${text}`), - info: (msg?: string) => console.log(msg || `[i] ${text}`), + succeed: (msg?: string) => console.log(msg || ok(text)), + fail: (msg?: string) => console.log(msg || fail(text)), + warn: (msg?: string) => console.log(msg || warn(text)), + info: (msg?: string) => console.log(msg || info(text)), text: '', }), }; diff --git a/src/utils/claude-dir-installer.ts b/src/utils/claude-dir-installer.ts index 9664f241..05ac1de0 100644 --- a/src/utils/claude-dir-installer.ts +++ b/src/utils/claude-dir-installer.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { ok, warn } from './ui'; +import { ok, fail, warn, info } from './ui'; // Ora fallback type for when ora is not available interface OraSpinner { @@ -27,14 +27,14 @@ try { const oraModule = require('ora'); ora = oraModule.default || oraModule; } 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 { return { start: () => ({ - succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), - fail: (msg?: string) => console.log(msg || `[X] ${text}`), - warn: (msg?: string) => console.log(msg || `[!] ${text}`), - info: (msg?: string) => console.log(msg || `[i] ${text}`), + succeed: (msg?: string) => console.log(msg || ok(text)), + fail: (msg?: string) => console.log(msg || fail(text)), + warn: (msg?: string) => console.log(msg || warn(text)), + info: (msg?: string) => console.log(msg || info(text)), text: '', }), }; @@ -85,11 +85,11 @@ export class ClaudeDirInstaller { if (!fs.existsSync(packageClaudeDir)) { const msg = 'Package .claude/ directory not found'; if (spinner) { - spinner.warn(`[!] ${msg}`); + spinner.warn(warn(msg)); console.log(` Searched in: ${packageClaudeDir}`); console.log(' This may be a development installation'); } else { - console.log(`[!] ${msg}`); + console.log(warn(msg)); console.log(` Searched in: ${packageClaudeDir}`); console.log(' This may be a development installation'); } @@ -119,7 +119,7 @@ export class ClaudeDirInstaller { if (spinner) { spinner.succeed(ok(msg)); } else { - console.log(`[OK] ${msg}`); + console.log(ok(msg)); } return true; } catch (err) { @@ -129,7 +129,7 @@ export class ClaudeDirInstaller { spinner.fail(warn(msg)); console.warn(' CCS items may not be available'); } else { - console.warn(`[!] ${msg}`); + console.warn(warn(msg)); console.warn(' CCS items may not be available'); } return false; @@ -223,14 +223,14 @@ export class ClaudeDirInstaller { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; const backupPath = `${userSymlinkFile}.backup-${timestamp}`; 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)'); } } catch (err) { const error = err as NodeJS.ErrnoException; // File doesn't exist or other error - that's okay 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}`; fs.renameSync(deprecatedFile, backupPath); 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 { fs.rmSync(deprecatedFile, { force: true }); } cleanedFiles.push('package copy'); } catch (err) { 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()); 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 }; } catch (err) { 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 }; } } diff --git a/src/utils/claude-symlink-manager.ts b/src/utils/claude-symlink-manager.ts index dc252610..4902d698 100644 --- a/src/utils/claude-symlink-manager.ts +++ b/src/utils/claude-symlink-manager.ts @@ -15,7 +15,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { ok, color } from './ui'; +import { ok, fail, warn, info, color } from './ui'; // Ora fallback type for when ora is not available interface OraSpinner { @@ -36,14 +36,14 @@ try { const oraModule = require('ora'); ora = oraModule.default || oraModule; } 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 { return { start: () => ({ - succeed: (msg?: string) => console.log(msg || `[OK] ${text}`), - fail: (msg?: string) => console.log(msg || `[X] ${text}`), - warn: (msg?: string) => console.log(msg || `[!] ${text}`), - info: (msg?: string) => console.log(msg || `[i] ${text}`), + succeed: (msg?: string) => console.log(msg || ok(text)), + fail: (msg?: string) => console.log(msg || fail(text)), + warn: (msg?: string) => console.log(msg || warn(text)), + info: (msg?: string) => console.log(msg || info(text)), text: '', }), }; @@ -94,9 +94,9 @@ export class ClaudeSymlinkManager { if (!fs.existsSync(this.ccsClaudeDir)) { const msg = 'CCS .claude/ directory not found, skipping symlink installation'; if (spinner) { - spinner.warn(`[!] ${msg}`); + spinner.warn(warn(msg)); } else { - console.log(`[!] ${msg}`); + console.log(warn(msg)); } return; } @@ -123,7 +123,7 @@ export class ClaudeSymlinkManager { if (spinner) { spinner.succeed(ok(msg)); } else { - console.log(`[OK] ${msg}`); + console.log(ok(msg)); } } @@ -137,7 +137,7 @@ export class ClaudeSymlinkManager { // Ensure source exists 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; } @@ -161,7 +161,7 @@ export class ClaudeSymlinkManager { try { const symlinkType = item.type === 'directory' ? 'dir' : 'file'; fs.symlinkSync(sourcePath, targetPath, symlinkType); - if (!silent) console.log(`[OK] Symlinked ${item.target}`); + if (!silent) console.log(ok(`Symlinked ${item.target}`)); return true; } catch (err) { // Windows fallback: copy instead of symlink when symlinks unavailable @@ -169,7 +169,7 @@ export class ClaudeSymlinkManager { return this.copyFallback(sourcePath, targetPath, item, silent); } else { 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; } @@ -194,15 +194,15 @@ export class ClaudeSymlinkManager { fs.copyFileSync(sourcePath, targetPath); } if (!silent) { - console.log(`[OK] Copied ${item.target} (symlink unavailable)`); - console.log(`[i] Run 'ccs sync' after CCS updates to refresh`); + console.log(ok(`Copied ${item.target} (symlink unavailable)`)); + console.log(info("Run 'ccs sync' after CCS updates to refresh")); } return true; } catch (copyErr) { const error = copyErr as Error; if (!silent) { - console.log(`[!] Failed to copy ${item.target}: ${error.message}`); - console.log(`[i] Enable Developer Mode for symlinks, or check permissions`); + console.log(warn(`Failed to copy ${item.target}: ${error.message}`)); + console.log(info('Enable Developer Mode for symlinks, or check permissions')); } return false; } @@ -267,10 +267,11 @@ export class ClaudeSymlinkManager { } 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) { 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 } } @@ -300,19 +301,19 @@ export class ClaudeSymlinkManager { // Remove symlink or file fs.unlinkSync(targetPath); } - console.log(`[OK] Removed ${item.target}`); + console.log(ok(`Removed ${item.target}`)); removed++; } catch (err) { 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) { - console.log(`[OK] Removed ${removed} delegation commands and skills from ~/.claude/`); + console.log(ok(`Removed ${removed} delegation commands and skills from ~/.claude/`)); } else { - console.log('[i] No delegation commands or skills to remove'); + console.log(info('No delegation commands or skills to remove')); } } diff --git a/src/utils/progress-indicator.ts b/src/utils/progress-indicator.ts index 36f1b195..104d000c 100644 --- a/src/utils/progress-indicator.ts +++ b/src/utils/progress-indicator.ts @@ -1,13 +1,16 @@ /** - * Simple Progress Indicator (no external dependencies) + * Simple Progress Indicator * * Features: * - ASCII-only spinner frames (cross-platform compatible) * - TTY detection (no spinners in pipes/logs) * - Elapsed time display * - CI environment detection + * - Color support via UI system */ +import { color } from './ui/colors'; + interface ProgressOptions { frames?: string[]; interval?: number; @@ -44,7 +47,7 @@ export class ProgressIndicator { start(): void { if (!this.isTTY) { // Non-TTY: just print message once - process.stderr.write(`[i] ${this.message}...\n`); + process.stderr.write(`${color('[i]', 'info')} ${this.message}...\n`); return; } @@ -52,7 +55,7 @@ export class ProgressIndicator { this.interval = setInterval(() => { const frame = this.frames[this.frameIndex]; 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; }, 80); // 12.5fps for smooth animation } @@ -68,10 +71,10 @@ export class ProgressIndicator { if (this.isTTY) { // 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 { // 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) { // Clear spinner line and show failure - process.stderr.write(`\r[X] ${finalMessage}\n`); + process.stderr.write(`\r${color('[X]', 'error')} ${finalMessage}\n`); } else { // Non-TTY: just show failure - process.stderr.write(`[X] ${finalMessage}\n`); + process.stderr.write(`${color('[X]', 'error')} ${finalMessage}\n`); } } diff --git a/src/utils/websearch/status.ts b/src/utils/websearch/status.ts index 7c47462e..95ddea17 100644 --- a/src/utils/websearch/status.ts +++ b/src/utils/websearch/status.ts @@ -71,6 +71,7 @@ export function hasAnyWebSearchCli(): boolean { /** * 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[] { if (hasAnyWebSearchCli()) { @@ -78,7 +79,7 @@ export function getCliInstallHints(): string[] { } return [ - '[i] WebSearch: No CLI tools installed', + 'WebSearch: No CLI tools installed', ' Gemini CLI (FREE): npm i -g @google/gemini-cli', ' OpenCode (FREE): curl -fsSL https://opencode.ai/install | bash', ' Grok CLI (paid): npm i -g @vibe-kit/grok-cli', @@ -182,8 +183,13 @@ export function displayWebSearchStatus(): void { console.error(fail(`WebSearch: ${status.message}`)); const hints = getCliInstallHints(); if (hints.length > 0) { - for (const hint of hints) { - console.error(info(hint)); + // First line gets [i] prefix, rest are continuation (indented, no prefix) + for (let i = 0; i < hints.length; i++) { + if (i === 0) { + console.error(info(hints[i])); + } else { + console.error(hints[i]); + } } } break;