UNPKG

codeplot

Version:

Interactive CLI tool for feature planning and ADR generation using Gemini 2.5 Pro

285 lines (242 loc) 8.11 kB
import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace'; interface LogMetadata { [key: string]: unknown; } interface ErrorMetadata extends LogMetadata { name: string; message: string; stack?: string; cause?: unknown; code?: string | number; } class Logger { private readonly isDebugMode: boolean; private readonly logLevel: LogLevel; private readonly logFile: string; private readonly maxLogSize: number; private readonly maxLogFiles: number; constructor() { this.isDebugMode = process.env.DEBUG === 'true' || process.argv.includes('--debug'); this.logLevel = (process.env.LOG_LEVEL as LogLevel) || 'info'; this.logFile = path.join(process.cwd(), 'debug.log'); this.maxLogSize = 10 * 1024 * 1024; // 10MB this.maxLogFiles = 5; // Create logs directory if it doesn't exist const logDir = path.dirname(this.logFile); fs.ensureDirSync(logDir); // Rotate logs if current file is too large this.rotateLogs(); // Initialize log file with session start this.info('='.repeat(80)); this.info(`Logger initialized - Debug Mode: ${this.isDebugMode}`); this.info(`Log Level: ${this.logLevel}`); this.info(`Process: ${process.argv.join(' ')}`); this.info(`Working Directory: ${process.cwd()}`); this.info(`Timestamp: ${new Date().toISOString()}`); this.info('='.repeat(80)); } private rotateLogs(): void { try { if (fs.existsSync(this.logFile)) { const stats = fs.statSync(this.logFile); if (stats.size > this.maxLogSize) { // Rotate existing logs for (let i = this.maxLogFiles - 1; i > 0; i--) { const currentLog = `${this.logFile}.${i}`; const nextLog = `${this.logFile}.${i + 1}`; if (fs.existsSync(currentLog)) { if (i === this.maxLogFiles - 1) { fs.removeSync(currentLog); } else { fs.moveSync(currentLog, nextLog); } } } // Move current log to .1 fs.moveSync(this.logFile, `${this.logFile}.1`); } } } catch (error) { // If rotation fails, continue anyway const errorMessage = error instanceof Error ? error.message : String(error); console.error('Log rotation failed:', errorMessage); } } private formatMessage(level: LogLevel, message: string, meta: LogMetadata = {}): string { const timestamp = new Date().toISOString(); const pid = process.pid; let formattedMessage = `[${timestamp}] [${pid}] [${level.toUpperCase()}] ${message}`; if (Object.keys(meta).length > 0) { formattedMessage += `\nMeta: ${JSON.stringify(meta, null, 2)}`; } return formattedMessage; } private writeToFile(level: LogLevel, message: string, meta: LogMetadata = {}): void { try { const formattedMessage = this.formatMessage(level, message, meta); fs.appendFileSync(this.logFile, formattedMessage + '\n'); } catch (error) { // Fallback to console if file write fails const errorMessage = error instanceof Error ? error.message : String(error); console.error('Failed to write to log file:', errorMessage); console.log(`[${level.toUpperCase()}]`, message, meta); } } private shouldLog(level: LogLevel): boolean { const levels: Record<LogLevel, number> = { error: 0, warn: 1, info: 2, debug: 3, trace: 4, }; return levels[level] <= levels[this.logLevel]; } public error(message: string, meta: LogMetadata = {}): void { if (this.shouldLog('error')) { this.writeToFile('error', message, meta); if (this.isDebugMode) { console.error('❌ [ERROR]', message); if (Object.keys(meta).length > 0) { console.error('Meta:', meta); } } } } public warn(message: string, meta: LogMetadata = {}): void { if (this.shouldLog('warn')) { this.writeToFile('warn', message, meta); if (this.isDebugMode) { console.warn('⚠️ [WARN]', message); if (Object.keys(meta).length > 0) { console.warn('Meta:', meta); } } } } public info(message: string, meta: LogMetadata = {}): void { if (this.shouldLog('info')) { this.writeToFile('info', message, meta); if (this.isDebugMode) { console.log('ℹ️ [INFO]', message); if (Object.keys(meta).length > 0) { console.log('Meta:', meta); } } } } public debug(message: string, meta: LogMetadata = {}): void { if (this.shouldLog('debug')) { this.writeToFile('debug', message, meta); if (this.isDebugMode) { console.log('🐛 [DEBUG]', message); if (Object.keys(meta).length > 0) { console.log('Meta:', meta); } } } } public trace(message: string, meta: LogMetadata = {}): void { if (this.shouldLog('trace')) { this.writeToFile('trace', message, meta); if (this.isDebugMode) { console.log('🔍 [TRACE]', message); if (Object.keys(meta).length > 0) { console.log('Meta:', meta); } } } } // Special method for logging errors with stack traces public errorWithStack(error: Error, message = 'Unhandled error', meta: LogMetadata = {}): never { const errorMeta: ErrorMetadata = { ...meta, name: error.name, message: error.message, stack: error.stack, ...(error.cause && typeof error.cause === 'object' && error.cause !== null ? { cause: error.cause } : {}), ...(typeof (error as Error & { code?: string | number }).code !== 'undefined' && { code: (error as Error & { code?: string | number }).code, }), }; this.error(message, errorMeta); // In debug mode, also throw the error to get a proper stack trace if (this.isDebugMode) { console.error('\n' + '='.repeat(80)); console.error('STACK TRACE FOR DEBUGGING:'); console.error('='.repeat(80)); throw error; } throw error; } // Method to log function entry/exit for debugging public logFunctionCall( functionName: string, args: LogMetadata = {}, result: unknown = null ): void { if (this.isDebugMode) { this.trace(`Function Call: ${functionName}`, { arguments: args, ...(result !== null && { result }), }); } } // Method to log API calls public logApiCall( method: string, url: string, requestData: LogMetadata = {}, responseData: LogMetadata = {}, duration: number | null = null ): void { this.debug(`API Call: ${method} ${url}`, { request: requestData, response: responseData, ...(duration && { duration: `${duration}ms` }), }); } // Method to log state changes public logStateChange(component: string, from: string, to: string, data: LogMetadata = {}): void { this.debug(`State Change: ${component}`, { from, to, data, }); } // Method to get log file path for external access public getLogFilePath(): string { return this.logFile; } // Method to clear logs public clearLogs(): void { try { if (fs.existsSync(this.logFile)) { fs.removeSync(this.logFile); } // Remove rotated logs too for (let i = 1; i <= this.maxLogFiles; i++) { const rotatedLog = `${this.logFile}.${i}`; if (fs.existsSync(rotatedLog)) { fs.removeSync(rotatedLog); } } this.info('Logs cleared'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error('Failed to clear logs', { error: errorMessage }); } } } // Export singleton instance export const logger = new Logger(); // Export class for testing export { Logger }; export type { LogLevel, LogMetadata };