/** * Hook Logger * * Provides a structured logger for hook scripts to report progress * back to the CLI. Integrates with FuelGauge for visual feedback. */ import type { FuelGauge } from '../cli/fuel-gauge'; import { getActiveDisplay } from '../cli/prompts'; import type { HookLogger } from './types'; const DIM = '\x1b[2m'; const RESET = '\x1b[0m'; const GREEN = '\x1b[32m'; const YELLOW = '\x1b[33m'; const RED = '\x1b[31m'; /** * Create a hook logger that outputs to a FuelGauge progress indicator * * @param gauge - FuelGauge instance for visual output * @param moduleId - Module ID for log prefix * @param hookName - Hook name for log prefix * @returns HookLogger instance */ export function createGaugeLogger( gauge: FuelGauge, moduleId: string, hookName: string, ): HookLogger { const prefix = `[${moduleId}:${hookName}]`; const nonInteractive = !process.stdout.isTTY; // When a ProgressDisplay is active, the running step header // (e.g. "… celilo-website: on_install") already names the hook, // so the per-line "[moduleId:hookName]" prefix becomes noise. // Dim the message and skip the stdout double-write — display // owns the output channel and forwards via gauge.addOutput → // display.subEvent. When no display is active, fall back to // the prefixed gauge output. function emit(level: 'info' | 'warn' | 'error' | 'success', message: string) { if (getActiveDisplay()) { const icon = level === 'warn' ? `${YELLOW}⚠${RESET} ` : level === 'error' ? `${RED}✗${RESET} ` : level === 'success' ? `${GREEN}✓${RESET} ` : ''; gauge.addOutput(`${icon}${DIM}${message}${RESET}`); return; } const icon = level === 'warn' ? '⚠ ' : level === 'error' ? '✗ ' : level === 'success' ? '✓ ' : ''; const line = `${prefix} ${icon}${message}`; gauge.addOutput(line); if (nonInteractive) { process.stdout.write(`${line}\n`); } } return { info: (message: string) => emit('info', message), warn: (message: string) => emit('warn', message), error: (message: string) => emit('error', message), success: (message: string) => emit('success', message), beginStep: (name: string) => { const display = getActiveDisplay(); if (display) { // pushStep nests under the FuelGauge step that wraps the hook, // so inner log calls (logger.info within the capability impl) // are sub-events of this nested step. // // No `→`/`✓` glyph in the message — the display's spinner // (in-progress) and green ✔ (done) already convey the state. // Sticking a literal `✓` into the doneMsg produced a double- // checkmark line on completion (`✔ ✓ name`). display.pushStep(name, name); } else { // Without a display, fall back to a plain info-style marker so // the line still appears in raw log output. gauge.addOutput(`${prefix} → ${name}`); if (nonInteractive) { process.stdout.write(`${prefix} → ${name}\n`); } } }, endStep: (_name: string) => { const display = getActiveDisplay(); if (display) { display.doneStep(); } else { gauge.addOutput(`${prefix} ✓ ${_name}`); if (nonInteractive) { process.stdout.write(`${prefix} ✓ ${_name}\n`); } } }, failStep: (name: string, error: string) => { const display = getActiveDisplay(); if (display) { display.failStep(`${name}: ${error}`); } else { gauge.addOutput(`${prefix} ✗ ${name}: ${error}`); if (nonInteractive) { process.stdout.write(`${prefix} ✗ ${name}: ${error}\n`); } } }, }; } /** * Create a hook logger that outputs to console (for testing/debugging) * * @param moduleId - Module ID for log prefix * @param hookName - Hook name for log prefix * @returns HookLogger instance */ export function createConsoleLogger(moduleId: string, hookName: string): HookLogger { const prefix = `[${moduleId}:${hookName}]`; return { info(message: string) { console.log(`${prefix} ${message}`); }, warn(message: string) { console.warn(`${prefix} ⚠ ${message}`); }, error(message: string) { console.error(`${prefix} ✗ ${message}`); }, success(message: string) { console.log(`${prefix} ✓ ${message}`); }, }; } /** * Create a hook logger that captures messages for testing * * @returns Object with logger and captured messages array */ export function createCapturingLogger(): { logger: HookLogger; messages: Array<{ level: string; message: string }>; } { const messages: Array<{ level: string; message: string }> = []; const logger: HookLogger = { info(message: string) { messages.push({ level: 'info', message }); }, warn(message: string) { messages.push({ level: 'warn', message }); }, error(message: string) { messages.push({ level: 'error', message }); }, success(message: string) { messages.push({ level: 'success', message }); }, }; return { logger, messages }; }