UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

216 lines (200 loc) 5.18 kB
import { createColors } from './colors.js'; /** * Simple console UI utilities for CLI output */ export class ConsoleUI { constructor(options = {}) { this.colors = createColors({ useColor: options.color !== false }); this.json = options.json || false; this.verbose = options.verbose || false; this.spinner = null; this.lastLine = ''; } /** * Show a success message */ success(message, data = {}) { this.stopSpinner(); if (this.json) { console.log(JSON.stringify({ status: 'success', message, timestamp: new Date().toISOString(), ...data })); } else { console.log(this.colors.green(`✓ ${message}`)); } } /** * Show an error message and exit */ error(message, error = {}, exitCode = 1) { this.stopSpinner(); if (this.json) { const errorData = { status: 'error', message, timestamp: new Date().toISOString() }; if (error instanceof Error) { errorData.error = { name: error.name, message: error.message, ...(this.verbose && { stack: error.stack }) }; } else if (typeof error === 'object') { errorData.error = error; } console.error(JSON.stringify(errorData)); } else { console.error(this.colors.red(`✖ ${message}`)); if (this.verbose && error.stack) { console.error(this.colors.dim(error.stack)); } } if (exitCode > 0) { process.exit(exitCode); } } /** * Show an info message */ info(message, data = {}) { if (this.json) { console.log(JSON.stringify({ status: 'info', message, timestamp: new Date().toISOString(), ...data })); } else { console.log(this.colors.cyan(`ℹ ${message}`)); } } /** * Show a warning message */ warning(message, data = {}) { if (this.json) { console.log(JSON.stringify({ status: 'warning', message, timestamp: new Date().toISOString(), ...data })); } else { console.log(this.colors.yellow(`⚠ ${message}`)); } } /** * Show progress with spinner */ progress(message, current = 0, total = 0) { if (this.json) { console.log(JSON.stringify({ status: 'progress', message, progress: { current, total }, timestamp: new Date().toISOString() })); } else { this.updateSpinner(message, current, total); } } /** * Update a status line in place (for dynamic updates) */ updateStatus(line, message) { if (this.json) { console.log(JSON.stringify({ status: 'update', line, message, timestamp: new Date().toISOString() })); } else { // Move cursor up to the target line and overwrite it process.stdout.write(`\u001b[${line}A`); // Move up process.stdout.write('\u001b[2K'); // Clear line process.stdout.write('\r'); // Move to beginning console.log(this.colors.blue(`ℹ ${message}`)); process.stdout.write(`\u001b[${line}B`); // Move back down } } /** * Output structured data */ data(data) { if (this.json) { console.log(JSON.stringify({ status: 'data', data, timestamp: new Date().toISOString() })); } else { console.log(JSON.stringify(data, null, 2)); } } /** * Start a spinner with message */ startSpinner(message) { if (this.json || !process.stdout.isTTY) return; this.stopSpinner(); this.currentMessage = message; const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; let i = 0; this.spinner = setInterval(() => { const frame = frames[i++ % frames.length]; const line = `${this.colors.blue(frame)} ${this.currentMessage || message}`; // Clear previous line and write new one process.stdout.write('\r' + ' '.repeat(this.lastLine.length) + '\r'); process.stdout.write(line); this.lastLine = line; }, 80); } /** * Update spinner message and progress */ updateSpinner(message, current = 0, total = 0) { if (this.json || !process.stdout.isTTY) return; if (!this.spinner) { this.startSpinner(message); return; } const progressText = total > 0 ? ` (${current}/${total})` : ''; const fullMessage = `${message}${progressText}`; // The spinner will pick up the new message on next frame this.currentMessage = fullMessage; } /** * Stop the spinner */ stopSpinner() { if (this.spinner) { clearInterval(this.spinner); this.spinner = null; // Clear the spinner line if (process.stdout.isTTY) { process.stdout.write('\r' + ' '.repeat(this.lastLine.length) + '\r'); } this.lastLine = ''; } } /** * Clean up on exit */ cleanup() { this.stopSpinner(); } } // Note: Global process event listeners are handled in individual commands // to avoid interference between tests and proper cleanup