// ============================================================================ // AI Testing Suite - Logger Utility // Rich console output with animated spinners, styled panels, and formatting // ============================================================================ import chalk from 'chalk'; import ora, { Ora } from 'ora'; import boxen from 'boxen'; import figures from 'figures'; import { table as formatTable, getBorderCharacters } from 'table'; import { AgentName, AgentLogEntry } from '../types'; // ── Agent Visual Config ────────────────────────────────────────────────────── const AGENT_COLORS: Record string> = { scanner: chalk.cyan, analyzer: chalk.blue, strategist: chalk.magenta, writer: chalk.green, reviewer: chalk.yellow, runner: chalk.redBright, security: chalk.red, reporter: chalk.white, }; const AGENT_ICONS: Record = { scanner: figures.pointer, analyzer: figures.info, strategist: figures.star, writer: figures.play, reviewer: figures.bullet, runner: figures.pointer, security: figures.warning, reporter: figures.circleFilled, }; const AGENT_LABELS: Record = { scanner: 'Scanner', analyzer: 'Analyzer', strategist: 'Strategist', writer: 'Writer', reviewer: 'Reviewer', runner: 'Runner', security: 'Security', reporter: 'Reporter', }; type OraColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'; const AGENT_ORA_COLORS: Record = { scanner: 'cyan', analyzer: 'blue', strategist: 'magenta', writer: 'green', reviewer: 'yellow', runner: 'red', security: 'red', reporter: 'white', }; // ── Logger Class ───────────────────────────────────────────────────────────── export class Logger { private verbose: boolean; private logs: AgentLogEntry[] = []; private activeSpinner: Ora | null = null; constructor(verbose: boolean = true) { this.verbose = verbose; } // ── Section Headers ────────────────────────────────────────────────────── header(title: string): void { console.log( boxen(chalk.bold.white(title), { padding: { top: 0, bottom: 0, left: 2, right: 2 }, margin: { top: 1, bottom: 1, left: 0, right: 0 }, borderStyle: 'round', borderColor: 'cyan', textAlignment: 'center', }) ); } subHeader(title: string): void { console.log( boxen(chalk.bold(title), { padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 0, left: 2, right: 0 }, borderStyle: 'single', borderColor: 'gray', dimBorder: true, }) ); } // ── Agent Lifecycle ────────────────────────────────────────────────────── agentStart(agent: AgentName): void { this.stopSpinner(); const color = AGENT_COLORS[agent]; const label = AGENT_LABELS[agent]; this.activeSpinner = ora({ text: color(`${label} Agent running...`), spinner: 'dots', color: AGENT_ORA_COLORS[agent], }).start(); this.logs.push({ agent, timestamp: new Date().toISOString(), action: 'Agent started', details: '', status: 'start', }); } agentComplete(agent: AgentName, duration?: number): void { const color = AGENT_COLORS[agent]; const label = AGENT_LABELS[agent]; const durationStr = duration ? chalk.gray(` (${(duration / 1000).toFixed(1)}s)`) : ''; if (this.activeSpinner) { this.activeSpinner.succeed(color(`${label} Agent completed${durationStr}`)); this.activeSpinner = null; } else { console.log(`${chalk.green(figures.tick)} ${color(`${label} Agent completed${durationStr}`)}`); } this.logs.push({ agent, timestamp: new Date().toISOString(), action: 'Agent completed', details: duration ? ` (${(duration / 1000).toFixed(1)}s)` : '', duration, status: 'complete', }); } agentError(agent: AgentName, error: string): void { const label = AGENT_LABELS[agent]; if (this.activeSpinner) { this.activeSpinner.fail(chalk.red(`${label} Agent ERROR: ${error}`)); this.activeSpinner = null; } else { console.log(`${chalk.red(figures.cross)} ${chalk.red(`${label} Agent ERROR: ${error}`)}`); } this.logs.push({ agent, timestamp: new Date().toISOString(), action: 'Error', details: error, status: 'error', }); } agent(agent: AgentName, action: string, details: string = ''): void { const color = AGENT_COLORS[agent]; const icon = AGENT_ICONS[agent]; const timestamp = chalk.gray(new Date().toLocaleTimeString()); const message = `${timestamp} ${color(icon)} ${chalk.white(action)}${details ? chalk.gray(` - ${details}`) : ''}`; if (this.activeSpinner) { this.activeSpinner.stop(); console.log(message); this.activeSpinner.start(); } else { console.log(message); } this.logs.push({ agent, timestamp: new Date().toISOString(), action, details, status: 'progress', }); } stopSpinner(): void { if (this.activeSpinner) { this.activeSpinner.stop(); this.activeSpinner = null; } } // ── Messages ───────────────────────────────────────────────────────────── info(message: string): void { if (this.activeSpinner) { this.activeSpinner.stop(); console.log(`${chalk.blue(figures.info)} ${chalk.gray(message)}`); this.activeSpinner.start(); } else { console.log(`${chalk.blue(figures.info)} ${chalk.gray(message)}`); } } success(message: string): void { console.log(`${chalk.green(figures.tick)} ${chalk.green(message)}`); } warning(message: string): void { console.log(`${chalk.yellow(figures.warning)} ${chalk.yellow(message)}`); } error(message: string): void { console.log(`${chalk.red(figures.cross)} ${chalk.red(message)}`); } // ── Data Display ───────────────────────────────────────────────────────── table(headers: string[], rows: string[][]): void { if (rows.length === 0) { console.log(chalk.bold(` ${headers.join(' ')}`)); console.log(''); return; } const data = [headers.map(h => chalk.bold.cyan(h)), ...rows]; const output = formatTable(data, { border: getBorderCharacters('norc'), columnDefault: { paddingLeft: 1, paddingRight: 1, }, drawHorizontalLine: (lineIndex: number, rowCount: number) => { return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount; }, }); console.log(output); } progress(current: number, total: number, label: string): void { const percentage = Math.round((current / total) * 100); const barLength = 25; const filled = Math.round((current / total) * barLength); const bar = chalk.green('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(barLength - filled)); if (this.activeSpinner) { this.activeSpinner.text = `${bar} ${chalk.white(`${percentage}%`)} ${chalk.gray(label)}`; } else { process.stdout.write(`\r ${bar} ${percentage}% ${label}`); if (current === total) { console.log(''); } } } stats(label: string, value: string | number): void { console.log(` ${chalk.gray(label + ':')} ${chalk.white(String(value))}`); } divider(): void { console.log(chalk.gray(' ' + '\u2500'.repeat(50))); } newline(): void { console.log(''); } // ── Panels ─────────────────────────────────────────────────────────────── panel(title: string, content: string, color: string = 'cyan'): void { console.log( boxen(content, { title, titleAlignment: 'center', padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 0, bottom: 1, left: 2, right: 0 }, borderStyle: 'round', borderColor: color as any, }) ); } // ── Test & Security Results ────────────────────────────────────────────── testResult(name: string, passed: boolean, duration?: number): void { const icon = passed ? chalk.green(figures.tick) : chalk.red(figures.cross); const status = passed ? chalk.green('PASS') : chalk.red('FAIL'); const dur = duration ? chalk.gray(` (${duration}ms)`) : ''; console.log(` ${icon} ${status} ${name}${dur}`); } securityFinding(severity: string, message: string, file: string): void { const severityConfig: Record string; icon: string }> = { critical: { color: chalk.bgRed.white, icon: figures.cross }, high: { color: chalk.red, icon: figures.warning }, medium: { color: chalk.yellow, icon: figures.bullet }, low: { color: chalk.blue, icon: figures.info }, }; const config = severityConfig[severity] || { color: chalk.gray, icon: figures.dot }; console.log(` ${config.color(`${config.icon} [${severity.toUpperCase()}]`)} ${message} ${chalk.gray(`(${file})`)}`); } // ── Summary ────────────────────────────────────────────────────────────── summary(data: Record): void { this.subHeader('Summary'); Object.entries(data).forEach(([key, value]) => { this.stats(key, value); }); this.newline(); } getLogs(): AgentLogEntry[] { return [...this.logs]; } } export const logger = new Logger();