UNPKG

@debugg-ai/cli

Version:
609 lines 23.8 kB
"use strict"; /** * System-wide logging architecture for DebuggAI CLI * * Provides two distinct logging modes: * 1. DevLogger: For --dev flag - sequential output with full technical details * 2. UserLogger: Default mode - clean spinner interface with minimal user-friendly messages * * Usage: * import { systemLogger } from '../util/system-logger'; * systemLogger.api.request('POST', '/test-suite'); * systemLogger.tunnel.connecting('localhost:3000'); * systemLogger.progress.start('Creating test suite...'); */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.systemLogger = void 0; const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); /** * Development Logger - Sequential output with full technical details * Used when --dev or --verbose flag is active */ class DevLogger { constructor() { // Tunnel-specific logging this.tunnel = { connecting: (target, context) => { this.log('info', 'tunnel', `Connecting to ${target}`, { ...context, details: { target, status: 'connecting', ...context?.details } }); }, connected: (url, timing, context) => { this.log('success', 'tunnel', `Tunnel established: ${url}`, { ...context, details: { url, timing: timing ? `${timing}ms` : undefined, status: 'connected', ...context?.details } }); }, failed: (target, error, timing, context) => { this.log('error', 'tunnel', `Tunnel connection failed: ${target}`, { ...context, details: { target, error, timing: timing ? `${timing}ms` : undefined, status: 'failed', ...context?.details } }); }, disconnected: (url, timing, context) => { this.log('info', 'tunnel', `Tunnel disconnected: ${url}`, { ...context, details: { url, timing: timing ? `${timing}ms` : undefined, status: 'disconnected', ...context?.details } }); }, status: (uuid, active, context) => { this.log('debug', 'tunnel', `Tunnel status check: ${uuid} - ${active ? 'active' : 'inactive'}`, { ...context, details: { uuid, active, ...context?.details } }); } }; // API-specific logging this.api = { request: (method, url, context) => { this.log('info', 'api', `${method.toUpperCase()} ${url}`, { ...context, details: { method: method.toUpperCase(), url: this.truncateUrl(url), ...context?.details }, truncate: 60 }); }, response: (status, url, timing, context) => { const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info'; this.log(level, 'api', `Response ${status} from ${this.truncateUrl(url)}`, { ...context, details: { status, url: this.truncateUrl(url), timing: timing ? `${timing}ms` : undefined, ...context?.details } }); }, error: (method, url, error, context) => { this.log('error', 'api', `${method.toUpperCase()} ${this.truncateUrl(url)} failed`, { ...context, details: { method: method.toUpperCase(), url: this.truncateUrl(url), error, ...context?.details }, truncate: 80 }); }, auth: (success, userInfo, context) => { const message = success ? `Authentication successful: ${userInfo || 'user'}` : 'Authentication failed'; this.log(success ? 'success' : 'error', 'api', message, { ...context, details: { success, userInfo, ...context?.details } }); } }; // Git-specific logging this.git = { analyzing: (type, target, context) => { this.log('info', 'git', `Analyzing ${type} changes: ${target}`, { ...context, details: { changeType: type, target, ...context?.details } }); }, found: (fileCount, type, context) => { this.log('info', 'git', `Found ${fileCount} changed files (${type})`, { ...context, details: { fileCount, changeType: type, ...context?.details } }); }, commit: (hash, message, fileCount, context) => { const shortHash = hash.substring(0, 8); const commitMsg = message ? ` - ${message.substring(0, 40)}${message.length > 40 ? '...' : ''}` : ''; this.log('info', 'git', `Commit ${shortHash}${commitMsg}`, { ...context, details: { commitHash: shortHash, message, fileCount, ...context?.details } }); }, branch: (branch, context) => { this.log('debug', 'git', `Current branch: ${branch}`, { ...context, details: { branch, ...context?.details } }); }, error: (operation, error, context) => { this.log('error', 'git', `Git ${operation} failed`, { ...context, details: { operation, error, ...context?.details } }); } }; // Test-specific logging this.test = { phase: (phase, message, context) => { this.log('info', 'test', `[${phase?.toUpperCase()}] ${message}`, { ...context, details: { phase, ...context?.details } }); }, suite: (action, suiteId, context) => { const message = `Test suite ${action}${suiteId ? `: ${suiteId}` : ''}`; this.log('info', 'test', message, { ...context, details: { action, suiteId, ...context?.details } }); }, progress: (completed, total, context) => { this.log('debug', 'test', `Test progress: ${completed}/${total} completed`, { ...context, details: { completed, total, ...context?.details } }); }, artifact: (type, filename, success, context) => { const level = success ? 'info' : 'warn'; const action = success ? 'Saved' : 'Failed to save'; this.log(level, 'test', `${action} ${type}: ${filename}`, { ...context, details: { artifactType: type, filename, success, ...context?.details } }); } }; // Server/general progress logging this.progress = { server: (port, status, timing, context) => { const message = status === 'waiting' ? `Waiting for server on port ${port}` : status === 'ready' ? `Server ready on port ${port}` : `Server timeout on port ${port}`; const level = status === 'ready' ? 'success' : status === 'timeout' ? 'error' : 'info'; this.log(level, 'server', message, { ...context, details: { port, status, timing: timing ? `${timing}ms` : undefined, ...context?.details } }); } }; } formatTimestamp() { const now = new Date(); return chalk_1.default.gray(`[${now.toISOString()}]`); } formatMessage(level, category, message, context) { const timestamp = this.formatTimestamp(); const levelTag = this.getLevelTag(level); const categoryTag = chalk_1.default.cyan(`[${category.toUpperCase()}]`); let output = `${timestamp} ${levelTag} ${categoryTag} ${message}`; if (context?.details) { const details = this.truncateDetails(context.details, context.truncate); if (Object.keys(details).length > 0) { output += chalk_1.default.gray(` ${JSON.stringify(details)}`); } } return output; } getLevelTag(level) { switch (level) { case 'info': return chalk_1.default.blue('INFO'); case 'success': return chalk_1.default.green('SUCCESS'); case 'warn': return chalk_1.default.yellow('WARN'); case 'error': return chalk_1.default.red('ERROR'); case 'debug': return chalk_1.default.magenta('DEBUG'); default: return chalk_1.default.white(level.toUpperCase()); } } truncateDetails(details, maxLength = 100) { const result = {}; for (const [key, value] of Object.entries(details)) { if (value === null || value === undefined) continue; if (typeof value === 'string') { result[key] = value.length > maxLength ? `${value.substring(0, maxLength)}...` : value; } else if (Array.isArray(value)) { result[`${key}Count`] = value.length; } else if (typeof value === 'object') { result[`${key}Type`] = 'object'; } else { result[key] = value; } } return result; } log(level, category, message, context) { const formattedMessage = this.formatMessage(level, category, message, context); console.log(formattedMessage); } // General logging methods info(message, context) { this.log('info', context?.category || 'general', message, context); } success(message, context) { this.log('success', context?.category || 'general', message, context); } warn(message, context) { this.log('warn', context?.category || 'general', message, context); } error(message, context) { this.log('error', context?.category || 'general', message, context); } debug(message, context) { this.log('debug', context?.category || 'general', message, context); } truncateUrl(url) { return url.length > 60 ? url.substring(0, 60) + '...' : url; } } /** * User Logger - Clean spinner interface with minimal messages * Used in default mode for clean user experience */ class UserLogger { constructor() { this.spinner = null; this.isQuiet = false; // Progress management with spinners this.progress = { start: (message) => { if (this.isQuiet) { console.log(`⏳ ${message}`); return; } this.spinner?.stop(); this.spinner = (0, ora_1.default)(message).start(); }, update: (message) => { if (this.isQuiet) { console.log(`⏳ ${message}`); return; } if (this.spinner) { this.spinner.text = message; } else { this.spinner = (0, ora_1.default)(message).start(); } }, succeed: (message) => { if (this.isQuiet) { console.log(`✅ ${message}`); return; } if (this.spinner) { this.spinner.succeed(message); this.spinner = null; } else { console.log(chalk_1.default.green(`✅ ${message}`)); } }, fail: (message) => { if (this.isQuiet) { console.log(`❌ ${message}`); return; } if (this.spinner) { this.spinner.fail(message); this.spinner = null; } else { console.log(chalk_1.default.red(`❌ ${message}`)); } }, warn: (message) => { if (this.isQuiet) { console.log(`⚠️ ${message}`); return; } if (this.spinner) { this.spinner.warn(message); this.spinner = null; } else { console.log(chalk_1.default.yellow(`⚠️ ${message}`)); } }, stop: () => { if (this.spinner) { this.spinner.stop(); this.spinner = null; } } }; // Specialized user-friendly messages this.tunnel = { connecting: (target) => { this.progress.start(`Creating tunnel to ${target}...`); }, connected: (url) => { this.progress.succeed(`Tunnel connected: ${url}`); }, failed: (target) => { this.progress.fail(`Failed to create tunnel to ${target}`); }, disconnected: () => { this.info('Tunnel disconnected'); } }; this.api = { auth: (success, userInfo) => { if (success) { this.info(`Authenticated as: ${userInfo || 'user'}`); } else { this.error('Authentication failed'); } }, request: (message) => { this.progress.update(message); } }; this.git = { analyzing: () => { this.progress.start('Analyzing git changes...'); }, found: (fileCount, type) => { this.progress.update(`Found ${fileCount} changed files${type ? ` (${type})` : ''}`); }, noChanges: () => { this.progress.succeed('No changes detected - skipping test generation'); } }; this.test = { creating: () => { this.progress.start('Creating test suite...'); }, created: (suiteId) => { this.progress.update(`Test suite created: ${suiteId.substring(0, 8)}`); }, running: (completed, total) => { const progressText = completed !== undefined && total !== undefined ? `Running tests... (${completed}/${total} completed)` : 'Running tests...'; this.progress.update(progressText); }, downloading: () => { this.progress.update('Downloading test artifacts...'); }, completed: (testCount) => { this.progress.succeed(`Tests completed! Generated ${testCount} test files`); }, failed: (error) => { this.progress.fail(`Tests failed: ${error}`); } }; // Check if we're in a non-TTY environment (CI/CD) or test mode this.isQuiet = !process.stdout.isTTY || process.env.NODE_ENV === 'test'; } // High-level user-friendly messages success(message) { this.progress.stop(); console.log(chalk_1.default.green(`✅ ${message}`)); } error(message) { this.progress.stop(); console.log(chalk_1.default.red(`❌ ${message}`)); } warn(message) { this.progress.stop(); console.log(chalk_1.default.yellow(`⚠️ ${message}`)); } info(message) { this.progress.stop(); console.log(chalk_1.default.blue(`ℹ️ ${message}`)); } // Results display displayResults(suite) { console.log('\n' + chalk_1.default.bold('=== Test Results ===')); console.log(`Suite: ${suite.name || suite.uuid}`); console.log(`Status: ${this.getStatusColor(suite.status || 'unknown')}`); console.log(`Tests: ${suite.tests?.length || 0}`); if (suite.tests && suite.tests.length > 0) { // Use outcome field instead of status for more accurate results const passed = suite.tests.filter((t) => t.curRun?.outcome === 'pass').length; const failed = suite.tests.filter((t) => t.curRun?.outcome === 'fail').length; const skipped = suite.tests.filter((t) => t.curRun?.outcome === 'skipped').length; const pending = suite.tests.filter((t) => t.curRun?.outcome === 'pending').length; const unknown = suite.tests.filter((t) => !t.curRun?.outcome || t.curRun?.outcome === 'unknown').length; const total = suite.tests.length; console.log('\n' + chalk_1.default.bold('Test Outcomes:')); console.log(` ${chalk_1.default.green(`✓ Passed: ${passed}`)}`); console.log(` ${chalk_1.default.red(`✗ Failed: ${failed}`)}`); if (skipped > 0) { console.log(` ${chalk_1.default.yellow(`⏩ Skipped: ${skipped}`)}`); } if (pending > 0) { console.log(` ${chalk_1.default.blue(`⏸ Pending: ${pending}`)}`); } if (unknown > 0) { console.log(` ${chalk_1.default.gray(`❓ Unknown: ${unknown}`)}`); } console.log(` ${chalk_1.default.blue(`📊 Total: ${total}`)}`); if (failed > 0) { console.log(`\n${chalk_1.default.yellow('⚠ Some tests failed. Check the generated test files and recordings for details.')}`); } else if (passed === total && total > 0) { console.log(`\n${chalk_1.default.green('🎉 All tests passed successfully!')}`); } } } displayFileList(files, repoPath) { if (files.length === 0) return; console.log(chalk_1.default.blue('\nGenerated test files:')); for (const file of files) { const relativePath = file.replace(repoPath, '').replace(/^\//, ''); console.log(chalk_1.default.gray(` • ${relativePath}`)); } } getStatusColor(status) { switch (status) { case 'completed': return chalk_1.default.green('✓ COMPLETED'); case 'failed': return chalk_1.default.red('✗ FAILED'); case 'running': return chalk_1.default.yellow('⏳ RUNNING'); case 'pending': return chalk_1.default.blue('⏸ PENDING'); default: return chalk_1.default.gray('❓ UNKNOWN'); } } getOutcomeColor(outcome) { switch (outcome) { case 'pass': return chalk_1.default.green('✓ PASSED'); case 'fail': return chalk_1.default.red('✗ FAILED'); case 'skipped': return chalk_1.default.yellow('⏩ SKIPPED'); case 'pending': return chalk_1.default.blue('⏸ PENDING'); case 'unknown': default: return chalk_1.default.gray('❓ UNKNOWN'); } } } /** * Environment detection and logger selection */ class SystemLogger { constructor() { this.isDevMode = false; this.devLogger = new DevLogger(); this.userLogger = new UserLogger(); // Detect dev mode from various sources this.detectDevMode(); } detectDevMode() { // Check for explicit dev mode indicators this.isDevMode = // CLI flags process.argv.includes('--dev') || process.argv.includes('--verbose') || process.argv.includes('-v') || // Environment variables process.env.NODE_ENV === 'development' || process.env.DEBUGGAI_LOG_LEVEL === 'DEBUG' || process.env.DEBUG === 'true' || // npm scripts context process.env.npm_lifecycle_event?.includes('dev') || false; } /** * Force dev mode (useful for testing or programmatic usage) */ setDevMode(enabled) { this.isDevMode = enabled; } /** * Check if currently in dev mode */ getDevMode() { return this.isDevMode; } // Route calls to appropriate logger get tunnel() { return this.isDevMode ? this.devLogger.tunnel : this.userLogger.tunnel; } get api() { return this.isDevMode ? this.devLogger.api : this.userLogger.api; } get git() { return this.isDevMode ? this.devLogger.git : this.userLogger.git; } get test() { return this.isDevMode ? this.devLogger.test : this.userLogger.test; } get progress() { return this.isDevMode ? this.devLogger.progress : this.userLogger.progress; } // General logging methods info(message, context) { if (this.isDevMode) { this.devLogger.info(message, context); } else { this.userLogger.info(message); } } success(message, context) { if (this.isDevMode) { this.devLogger.success(message, context); } else { this.userLogger.success(message); } } warn(message, context) { if (this.isDevMode) { this.devLogger.warn(message, context); } else { this.userLogger.warn(message); } } error(message, context) { if (this.isDevMode) { this.devLogger.error(message, context); } else { this.userLogger.error(message); } } debug(message, context) { if (this.isDevMode) { this.devLogger.debug(message, context); } // UserLogger doesn't show debug messages } // User logger specific methods (only available in user mode) displayResults(suite) { if (!this.isDevMode) { this.userLogger.displayResults(suite); } else { // In dev mode, just log the suite info this.devLogger.info('Test suite completed', { category: 'test', details: { suiteId: suite.uuid, status: suite.status, testCount: suite.tests?.length } }); } } displayFileList(files, repoPath) { if (!this.isDevMode) { this.userLogger.displayFileList(files, repoPath); } else { // In dev mode, log file list this.devLogger.info(`Generated ${files.length} test files`, { category: 'test', details: { files: files.map(f => f.replace(repoPath, '').replace(/^\//, '')) } }); } } } // Export singleton instance exports.systemLogger = new SystemLogger(); // Export individual loggers for direct access if needed (commented out to avoid conflicts) // export { DevLogger, UserLogger }; // Export types for external use (commented out to avoid conflicts) // export type { // LogContext, // TunnelLogContext, // ApiLogContext, // GitLogContext, // TestLogContext // }; //# sourceMappingURL=system-logger.js.map