@debugg-ai/cli
Version:
CLI tool for running DebuggAI tests in CI/CD environments
609 lines • 23.8 kB
JavaScript
/**
* 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
;