UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

307 lines 12.1 kB
/** * WOARU Activity Logger for Comprehensive Action Tracking * Provides detailed logging of all WOARU actions with timestamps, context, and metadata */ // Explain-for-humans: This system tracks every action WOARU takes, recording when commands start, succeed, or fail, along with performance metrics and context information for complete transparency. import fs from 'fs-extra'; import * as path from 'path'; // Removed unused FilenameHelper import import { APP_CONFIG } from '../config/constants.js'; /** * Singleton class for managing WOARU activity logging */ export class ActivityLogger { static instance; logFile = ''; activeActions = new Map(); constructor() { this.initializeAsync(); } async initializeAsync() { // Create logs directory in user's home const homeDir = (await import('os')).homedir(); const logsDir = path.join(homeDir, APP_CONFIG.DIRECTORIES.HOME_LOGS); this.logFile = path.join(logsDir, APP_CONFIG.FILES.ACTIONS_LOG); // Ensure logs directory exists fs.ensureDirSync(logsDir); } /** * Gets the singleton instance of ActivityLogger * @returns The ActivityLogger instance */ static getInstance() { if (!ActivityLogger.instance) { ActivityLogger.instance = new ActivityLogger(); } return ActivityLogger.instance; } /** * Start tracking an action with context and performance metrics * @param action - The action being performed (e.g., 'analyze', 'review') * @param command - The full command being executed * @param context - Context information including project path, working directory, etc. * @returns Unique action ID for tracking completion */ async startAction(action, command, context) { // Input validation if (!action || typeof action !== 'string') { throw new Error('Action must be a non-empty string'); } if (!command || typeof command !== 'string') { throw new Error('Command must be a non-empty string'); } if (!context || !context.projectPath || !context.workingDirectory) { throw new Error('Context must include projectPath and workingDirectory'); } // Generate unique action ID for tracking const actionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const timestamp = new Date().toISOString(); const entry = { timestamp, action, command, context, metadata: { success: false, // Will be updated when action completes }, performance: { startTime: Date.now(), memoryUsage: process.memoryUsage(), }, }; this.activeActions.set(actionId, entry); // Log action start await this.writeLogEntry(`[START] ${timestamp} | ${action} | ${command} | ${context.projectPath}`); return actionId; } /** * Complete an action with success/error details and performance metrics * @param actionId - The unique action ID returned from startAction * @param metadata - Completion metadata including success status, error details, etc. */ async completeAction(actionId, metadata) { // Input validation if (!actionId || typeof actionId !== 'string') { throw new Error('Action ID must be a non-empty string'); } if (!metadata || typeof metadata.success !== 'boolean') { throw new Error('Metadata must include success boolean'); } const entry = this.activeActions.get(actionId); if (!entry) { console.warn(`ActivityLogger: Action ${actionId} not found`); return; } const endTime = Date.now(); const duration = endTime - entry.performance.startTime; // Update entry entry.metadata = { ...entry.metadata, ...metadata, duration }; entry.performance.endTime = endTime; entry.performance.memoryUsage = process.memoryUsage(); // Log completion const status = metadata.success ? 'SUCCESS' : 'ERROR'; const errorSuffix = metadata.error ? ` | Error: ${metadata.error}` : ''; const outputSuffix = metadata.outputFile ? ` | Output: ${metadata.outputFile}` : ''; await this.writeLogEntry(`[${status}] ${new Date().toISOString()} | ${entry.action} | Duration: ${duration}ms${errorSuffix}${outputSuffix}`); // Remove from active actions this.activeActions.delete(actionId); } /** * Log a simple event (without start/complete cycle) * @param action - The action being logged * @param command - The command being executed * @param context - Context information * @param metadata - Event metadata including success status */ async logEvent(action, command, context, metadata) { const timestamp = new Date().toISOString(); const status = metadata.success ? 'SUCCESS' : 'ERROR'; const errorSuffix = metadata.error ? ` | Error: ${metadata.error}` : ''; const outputSuffix = metadata.outputFile ? ` | Output: ${metadata.outputFile}` : ''; await this.writeLogEntry(`[${status}] ${timestamp} | ${action} | ${command} | ${context.projectPath}${errorSuffix}${outputSuffix}`); } /** * Get recent log entries * @param limit - Maximum number of log entries to return (default: 50) * @returns Array of recent log entries */ async getRecentLogs(limit = 50) { // Input validation if (typeof limit !== 'number' || limit < 1) { throw new Error('Limit must be a positive number'); } if (!(await fs.pathExists(this.logFile))) { return []; } const content = await fs.readFile(this.logFile, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); // Return last N lines return lines.slice(-limit); } /** * Get logs for a specific time range * @param startDate - Start date for filtering * @param endDate - End date for filtering * @returns Array of log entries within the date range */ async getLogsByDateRange(startDate, endDate) { // Input validation if (!(startDate instanceof Date) || !(endDate instanceof Date)) { throw new Error('Start and end dates must be Date objects'); } if (startDate > endDate) { throw new Error('Start date must be before end date'); } const logs = await this.getRecentLogs(1000); // Get more logs for filtering return logs.filter(log => { const timestampMatch = log.match(/\[(?:START|SUCCESS|ERROR)\] (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/); if (!timestampMatch) return false; const logDate = new Date(timestampMatch[1]); return logDate >= startDate && logDate <= endDate; }); } /** * Get logs for a specific action type * @param actionType - The action type to filter by (e.g., 'analyze', 'review') * @returns Array of log entries for the specified action type */ async getLogsByAction(actionType) { // Input validation if (!actionType || typeof actionType !== 'string') { throw new Error('Action type must be a non-empty string'); } const logs = await this.getRecentLogs(1000); return logs.filter(log => log.includes(`| ${actionType} |`)); } /** * Get logs for a specific project * @param projectPath - The project path to filter by * @returns Array of log entries for the specified project */ async getLogsByProject(projectPath) { // Input validation if (!projectPath || typeof projectPath !== 'string') { throw new Error('Project path must be a non-empty string'); } const logs = await this.getRecentLogs(1000); return logs.filter(log => log.includes(projectPath)); } /** * Clear all logs (dangerous operation) * @returns Promise that resolves when logs are cleared */ async clearLogs() { if (await fs.pathExists(this.logFile)) { await fs.remove(this.logFile); } } /** * Get log file path * @returns The absolute path to the log file */ getLogFilePath() { return this.logFile; } /** * Get log file size and statistics * @returns Object containing log file statistics */ async getLogStats() { if (!(await fs.pathExists(this.logFile))) { return { fileSize: 0, totalLines: 0 }; } const stats = await fs.stat(this.logFile); const content = await fs.readFile(this.logFile, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); return { fileSize: stats.size, totalLines: lines.length, oldestEntry: lines.length > 0 ? lines[0] : undefined, newestEntry: lines.length > 0 ? lines[lines.length - 1] : undefined, }; } /** * Write log entry to file (private method) * @param entry - The log entry string to write */ async writeLogEntry(entry) { try { await fs.appendFile(this.logFile, entry + '\n', 'utf-8'); } catch (error) { console.error('ActivityLogger: Failed to write log entry:', error); } } /** * Get current active actions (for monitoring purposes) * @returns Map of active action IDs to their log entries */ getActiveActions() { return new Map(this.activeActions); } /** * Format log entry for display * @param logLine - Raw log line to format * @returns Formatted log entry object or null if parsing fails */ formatLogEntry(logLine) { const match = logLine.match(/\[(START|SUCCESS|ERROR)\] (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z) \| ([^|]+) \| (.+)/); if (!match) { return null; } return { timestamp: match[2], status: match[1], action: match[3].trim(), details: match[4].trim(), }; } /** * Export logs to different formats * @param format - Export format ('json', 'csv', 'txt') * @param outputPath - Path where to save the exported logs */ async exportLogs(format, outputPath) { // Input validation if (!['json', 'csv', 'txt'].includes(format)) { throw new Error('Format must be one of: json, csv, txt'); } if (!outputPath || typeof outputPath !== 'string') { throw new Error('Output path must be a non-empty string'); } const logs = await this.getRecentLogs(1000); switch (format) { case 'json': { const jsonLogs = logs .map(log => this.formatLogEntry(log)) .filter(Boolean); await fs.writeFile(outputPath, JSON.stringify(jsonLogs, null, 2), 'utf-8'); break; } case 'csv': { const csvHeader = 'Timestamp,Status,Action,Details\n'; const csvLines = logs .map(log => this.formatLogEntry(log)) .filter(Boolean) .map(entry => { if (!entry) return ''; return `"${entry.timestamp}","${entry.status}","${entry.action}","${entry.details}"`; }) .join('\n'); await fs.writeFile(outputPath, csvHeader + csvLines, 'utf-8'); break; } case 'txt': default: await fs.writeFile(outputPath, logs.join('\n'), 'utf-8'); break; } } } //# sourceMappingURL=ActivityLogger.js.map