@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
883 lines • 37.2 kB
JavaScript
import { z } from 'zod';
import chalk from 'chalk';
import { $ } from '@xec-sh/core';
import * as readline from 'readline';
import { validateOptions } from '../utils/validation.js';
import { InteractiveHelpers } from '../utils/interactive-helpers.js';
import { ConfigAwareCommand } from '../utils/command-base.js';
export class LogsCommand extends ConfigAwareCommand {
constructor() {
super({
name: 'logs',
aliases: ['l'],
description: 'View and stream logs from targets (interactive mode if no target specified)',
arguments: '[target] [path]',
options: [
{
flags: '-p, --profile <profile>',
description: 'Configuration profile to use',
},
{
flags: '-f, --follow',
description: 'Follow log output (stream new logs)',
},
{
flags: '-n, --tail <lines>',
description: 'Number of lines to show from the end',
defaultValue: '50',
},
{
flags: '--since <time>',
description: 'Show logs since timestamp (e.g., 10m, 1h, 2d)',
},
{
flags: '--until <time>',
description: 'Show logs until timestamp',
},
{
flags: '-t, --timestamps',
description: 'Show timestamps with log lines',
},
{
flags: '--container <name>',
description: 'Container name (for pods with multiple containers)',
},
{
flags: '--previous',
description: 'Show previous container logs (Kubernetes)',
},
{
flags: '-g, --grep <pattern>',
description: 'Filter logs by pattern (regex)',
},
{
flags: '-v, --invert',
description: 'Invert grep match (exclude matching lines)',
},
{
flags: '-A, --after <lines>',
description: 'Show N lines after grep match',
},
{
flags: '-B, --before <lines>',
description: 'Show N lines before grep match',
},
{
flags: '-C, --context <lines>',
description: 'Show N lines before and after grep match',
},
{
flags: '--no-color',
description: 'Disable colored output',
},
{
flags: '--json',
description: 'Output logs as JSON',
},
{
flags: '--parallel',
description: 'View logs from multiple targets in parallel',
},
{
flags: '--aggregate',
description: 'Aggregate logs from multiple sources',
},
{
flags: '--prefix',
description: 'Prefix each line with target name',
},
{
flags: '--task <task>',
description: 'Run a log analysis task',
},
],
examples: [
{
command: 'xec logs',
description: 'Start interactive mode to select targets and options',
},
{
command: 'xec logs containers.app',
description: 'View last 50 lines from Docker container',
},
{
command: 'xec logs hosts.web-1 /var/log/nginx/access.log -f',
description: 'Stream nginx access logs from SSH host',
},
{
command: 'xec logs pods.api --tail 100 --since 1h',
description: 'View last 100 lines from past hour',
},
{
command: 'xec logs containers.* --parallel --prefix',
description: 'View logs from all containers with prefixes',
},
{
command: 'xec logs hosts.web-* /var/log/app.log --grep ERROR -C 3',
description: 'Find errors with 3 lines of context',
},
{
command: 'xec logs pods.worker --container sidecar -f',
description: 'Stream logs from specific container in pod',
},
{
command: 'xec logs local /var/log/system.log --since "2h ago"',
description: 'View local system logs from 2 hours ago',
},
{
command: 'xec logs hosts.db-* --task analyze-slow-queries',
description: 'Run analysis task on database logs',
},
{
command: 'xec logs --interactive',
description: 'Interactive mode for selecting targets and log options',
},
],
validateOptions: (options) => {
const schema = z.object({
profile: z.string().optional(),
interactive: z.boolean().optional(),
follow: z.boolean().optional(),
tail: z.string().optional(),
since: z.string().optional(),
until: z.string().optional(),
timestamps: z.boolean().optional(),
container: z.string().optional(),
previous: z.boolean().optional(),
grep: z.string().optional(),
invert: z.boolean().optional(),
after: z.string().optional(),
before: z.string().optional(),
context: z.string().optional(),
color: z.boolean().optional(),
json: z.boolean().optional(),
parallel: z.boolean().optional(),
aggregate: z.boolean().optional(),
prefix: z.boolean().optional(),
task: z.string().optional(),
verbose: z.boolean().optional(),
quiet: z.boolean().optional(),
dryRun: z.boolean().optional(),
});
validateOptions(options, schema);
},
});
this.streams = new Map();
this.running = true;
}
getCommandConfigKey() {
return 'logs';
}
async execute(args) {
const lastArg = args[args.length - 1];
const isCommand = lastArg && typeof lastArg === 'object' && lastArg.constructor && lastArg.constructor.name === 'Command';
const options = isCommand ? args[args.length - 2] : lastArg;
const positionalArgs = isCommand ? args.slice(0, -2) : args.slice(0, -1);
let targetPattern = positionalArgs[0];
let logPath = positionalArgs[1];
if (!targetPattern) {
const interactiveResult = await this.runInteractiveMode(options);
if (!interactiveResult)
return;
targetPattern = interactiveResult.targetPattern;
logPath = interactiveResult.logPath;
Object.assign(options, interactiveResult.options);
}
if (!targetPattern) {
throw new Error('Target specification is required');
}
await this.initializeConfig(options);
const defaults = this.getCommandDefaults();
const mergedOptions = this.applyDefaults({
...options,
verbose: options.verbose ?? this.options?.verbose,
quiet: options.quiet ?? this.options?.quiet
}, defaults);
let targets;
if (targetPattern.includes('*') || targetPattern.includes('{')) {
targets = await this.findTargets(targetPattern);
if (targets.length === 0) {
throw new Error(`No targets found matching pattern: ${targetPattern}`);
}
}
else {
const target = await this.resolveTarget(targetPattern);
targets = [target];
}
if (mergedOptions.task) {
await this.executeTask(targets, logPath, mergedOptions.task, mergedOptions);
}
else if (targets.length > 1 && (mergedOptions.parallel || mergedOptions.aggregate)) {
await this.viewMultipleLogs(targets, logPath, mergedOptions);
}
else {
for (const target of targets) {
await this.viewSingleLog(target, logPath, mergedOptions);
}
}
}
async viewSingleLog(target, logPath, options) {
const targetDisplay = this.formatTargetDisplay(target);
if (options.dryRun) {
console.log(`[DRY RUN] Would view logs from ${targetDisplay}${logPath ? `:${logPath}` : ''}`);
return;
}
if (options.follow) {
this.setupCleanupHandlers();
}
try {
if (options.follow) {
console.log(`Streaming logs from ${targetDisplay}${logPath ? `:${logPath}` : ''}...`);
if (options.grep) {
console.log(`Filter: ${options.grep}${options.invert ? ' (inverted)' : ''}`);
}
console.log('Press Ctrl+C to stop\n');
}
else if (!options.quiet) {
this.startSpinner(`Fetching logs from ${targetDisplay}...`);
}
const logCommand = await this.buildLogCommand(target, logPath, options);
if (options.verbose) {
console.log(`[DEBUG] Log command: ${logCommand}`);
}
const useLocalEngine = target.type === 'docker' || target.type === 'k8s';
const engine = useLocalEngine ? $ : await this.createTargetEngine(target);
if (options.follow) {
await this.streamLogs(target, engine, logCommand, options);
}
else {
let result;
result = await engine.raw `${logCommand}`;
if (!options.quiet) {
this.stopSpinner();
}
if (result.stdout && result.stdout.trim()) {
this.displayLogs(result.stdout, target, options);
}
else {
console.log('No logs found matching criteria.');
}
}
}
catch (error) {
if (!options.quiet) {
this.stopSpinner();
}
const errorMessage = error instanceof Error ? error.message : String(error);
this.log(`${chalk.red('✗')} Failed to view logs: ${errorMessage}`, 'error');
throw error;
}
}
async viewMultipleLogs(targets, logPath, options) {
if (options.aggregate) {
this.log('Aggregating logs from multiple targets...', 'info');
throw new Error('Log aggregation is not yet implemented');
}
this.log(`Viewing logs from ${targets.length} targets in parallel...`, 'info');
if (options.follow) {
this.setupCleanupHandlers();
const promises = targets.map(async (target) => {
try {
await this.streamLogsWithPrefix(target, logPath, options);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log(`${chalk.red('✗')} ${this.formatTargetDisplay(target)}: ${errorMessage}`, 'error');
}
});
try {
await Promise.all(promises);
}
catch (error) {
console.error('Unexpected error in parallel log streaming:', error);
}
}
else {
const promises = targets.map(async (target) => {
try {
const logCommand = await this.buildLogCommand(target, logPath, options);
const useLocalEngine = target.type === 'docker' || target.type === 'k8s';
const engine = useLocalEngine ? $ : await this.createTargetEngine(target);
const result = await engine.raw `${logCommand}`;
return { target, logs: result.stdout, error: null };
}
catch (error) {
return { target, logs: null, error };
}
});
const results = await Promise.all(promises);
for (const { target, logs, error } of results) {
if (error) {
this.log(`${chalk.red('✗')} ${this.formatTargetDisplay(target)}: ${error}`, 'error');
}
else if (logs) {
this.displayLogs(logs, target, { ...options, prefix: true });
}
}
}
}
async buildLogCommand(target, logPath, options) {
const parts = [];
switch (target.type) {
case 'docker': {
const config = target.config;
const container = config.container || target.name;
parts.push('docker', 'logs');
if (options.follow)
parts.push('--follow');
if (options.tail)
parts.push('--tail', options.tail);
if (options.since)
parts.push('--since', this.convertTimeSpec(options.since));
if (options.until)
parts.push('--until', this.convertTimeSpec(options.until));
if (options.timestamps)
parts.push('--timestamps');
parts.push(container);
if (options.grep) {
parts.push('2>&1', '|', 'grep', '-E');
if (options.invert)
parts.push('-v');
if (options.before)
parts.push('-B', options.before);
if (options.after)
parts.push('-A', options.after);
if (options.context)
parts.push('-C', options.context);
if (options.color !== false && !options.json)
parts.push('--color=always');
parts.push(`'${options.grep.replace(/'/g, "'\\''")}'`);
}
break;
}
case 'k8s': {
const config = target.config;
const namespace = config.namespace || 'default';
const pod = config.pod || target.name;
parts.push('kubectl', 'logs');
parts.push('-n', namespace);
if (options.follow)
parts.push('--follow');
if (options.tail)
parts.push('--tail', options.tail);
if (options.since)
parts.push('--since', this.convertK8sTimeSpec(options.since));
if (options.timestamps)
parts.push('--timestamps');
if (options.previous)
parts.push('--previous');
if (options.container || config.container) {
parts.push('--container', options.container || config.container);
}
parts.push(pod);
if (options.grep) {
parts.push('2>&1', '|', 'grep', '-E');
if (options.invert)
parts.push('-v');
if (options.before)
parts.push('-B', options.before);
if (options.after)
parts.push('-A', options.after);
if (options.context)
parts.push('-C', options.context);
if (options.color !== false && !options.json)
parts.push('--color=always');
parts.push(`'${options.grep.replace(/'/g, "'\\''")}'`);
}
break;
}
case 'ssh':
case 'local': {
const path = logPath || this.getDefaultLogPath(target);
if (options.follow) {
parts.push('tail', '-f');
if (options.tail)
parts.push('-n', options.tail);
}
else {
if (options.since || options.until) {
parts.push('tail', '-n', '+1');
}
else {
parts.push('tail', '-n', options.tail || '50');
}
}
parts.push(path);
if (options.grep) {
parts.push('|', 'grep', '-E');
if (options.invert)
parts.push('-v');
if (options.before)
parts.push('-B', options.before);
if (options.after)
parts.push('-A', options.after);
if (options.context)
parts.push('-C', options.context);
if (options.color !== false && !options.json)
parts.push('--color=always');
parts.push(`'${options.grep.replace(/'/g, "'\\''")}'`);
}
break;
}
default:
throw new Error(`Log viewing not supported for target type: ${target.type}`);
}
return parts.join(' ');
}
async streamLogs(target, engine, logCommand, options) {
const sessionId = `${target.id}:logs`;
let logProcess;
logProcess = engine.raw `${logCommand}`.nothrow();
if (logProcess.child) {
logProcess.child.on('error', (error) => {
console.error(`Child process error for ${sessionId}:`, error);
this.streams.delete(sessionId);
});
}
this.streams.set(sessionId, {
target,
process: logProcess,
cleanup: async () => {
try {
if (logProcess && typeof logProcess.kill === 'function') {
logProcess.kill('SIGTERM');
}
}
catch (error) {
}
},
});
if (logProcess.child?.stdout) {
const rl = readline.createInterface({
input: logProcess.child.stdout,
crlfDelay: Infinity,
});
rl.on('line', (line) => {
try {
this.displayLogLine(line, target, options);
}
catch (error) {
console.error('Error displaying log line:', error);
}
});
rl.on('close', () => {
this.streams.delete(sessionId);
});
rl.on('error', (error) => {
console.error('Readline error:', error);
this.streams.delete(sessionId);
});
}
if (logProcess.child?.stderr) {
logProcess.child.stderr.on('data', (data) => {
try {
if (options.verbose) {
console.error(chalk.yellow(data.toString().trim()));
}
}
catch (error) {
}
});
logProcess.child.stderr.on('error', (error) => {
if (options.verbose) {
console.error('Stderr stream error:', error);
}
});
}
try {
await logProcess;
}
catch (error) {
if (!this.running) {
return;
}
throw error;
}
}
async streamLogsWithPrefix(target, logPath, options) {
const logCommand = await this.buildLogCommand(target, logPath, options);
const useLocalEngine = target.type === 'docker' || target.type === 'k8s';
const engine = useLocalEngine ? $ : await this.createTargetEngine(target);
await this.streamLogs(target, engine, logCommand, { ...options, prefix: true });
}
displayLogs(logs, target, options) {
const lines = logs.split('\n').filter(line => line.trim());
if (lines.length === 0) {
return;
}
for (const line of lines) {
this.displayLogLine(line, target, options);
}
if (!options.quiet && !options.follow) {
this.log(chalk.gray(`\nDisplayed ${lines.length} log lines`), 'info');
}
}
displayLogLine(line, target, options) {
if (!line.trim())
return;
let output = line;
if (options.prefix) {
const prefix = chalk.cyan(`[${this.formatTargetDisplay(target)}]`);
output = `${prefix} ${output}`;
}
if (options.timestamps && !this.hasTimestamp(line)) {
const timestamp = chalk.gray(new Date().toISOString());
output = `${timestamp} ${output}`;
}
if (options.json) {
try {
const parsed = JSON.parse(line);
console.log(JSON.stringify({
target: target.id,
timestamp: new Date().toISOString(),
data: parsed,
}, null, 2));
return;
}
catch {
console.log(JSON.stringify({
target: target.id,
timestamp: new Date().toISOString(),
message: line.trim(),
}));
return;
}
}
if (options.color !== false) {
output = this.colorizeLogLine(output);
}
console.log(output);
}
colorizeLogLine(line) {
return line
.replace(/\b(ERROR|ERR|FAIL|FAILURE|FATAL)\b/gi, chalk.red('$1'))
.replace(/\b(WARN|WARNING)\b/gi, chalk.yellow('$1'))
.replace(/\b(INFO|INFORMATION)\b/gi, chalk.blue('$1'))
.replace(/\b(DEBUG|TRACE)\b/gi, chalk.gray('$1'))
.replace(/\b(SUCCESS|OK|DONE)\b/gi, chalk.green('$1'));
}
hasTimestamp(line) {
const timestampPatterns = [
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/,
/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}/,
/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/,
/^\w{3} \d{1,2} \d{2}:\d{2}:\d{2}/,
];
return timestampPatterns.some(pattern => pattern.test(line));
}
convertTimeSpec(timeSpec) {
const match = timeSpec.match(/^(\d+)([smhd])(?:\s+ago)?$/);
if (match && match[1] && match[2]) {
const value = match[1];
const unit = match[2];
const seconds = this.getSeconds(parseInt(value, 10), unit);
return `${seconds}s`;
}
return timeSpec;
}
convertK8sTimeSpec(timeSpec) {
const match = timeSpec.match(/^(\d+)([smhd])(?:\s+ago)?$/);
if (match) {
const [, value, unit] = match;
return `${value}${unit}`;
}
return timeSpec;
}
getSeconds(value, unit) {
switch (unit) {
case 's': return value;
case 'm': return value * 60;
case 'h': return value * 3600;
case 'd': return value * 86400;
default: return value;
}
}
getDefaultLogPath(target) {
const config = target.config;
if (config.logPath) {
return config.logPath;
}
switch (target.name) {
case 'nginx':
return '/var/log/nginx/access.log';
case 'apache':
return '/var/log/apache2/access.log';
case 'mysql':
return '/var/log/mysql/error.log';
case 'postgres':
return '/var/log/postgresql/postgresql.log';
default:
return '/var/log/syslog';
}
}
async executeTask(targets, logPath, taskName, options) {
if (!this.taskManager) {
throw new Error('Task manager not initialized');
}
for (const target of targets) {
const targetDisplay = this.formatTargetDisplay(target);
this.log(`Running log analysis task '${taskName}' on ${targetDisplay}...`, 'info');
try {
const result = await this.taskManager.run(taskName, { LOG_PATH: logPath || this.getDefaultLogPath(target) }, { target: target.id });
if (result.success) {
this.log(`${chalk.green('✓')} Task completed on ${targetDisplay}`, 'success');
}
else {
throw new Error(result.error?.message || 'Task failed');
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log(`${chalk.red('✗')} Task failed on ${targetDisplay}: ${errorMessage}`, 'error');
throw error;
}
}
}
async runInteractiveMode(options) {
InteractiveHelpers.startInteractiveMode('Interactive Logs Mode');
try {
const logSourceType = await InteractiveHelpers.selectFromList('What type of logs do you want to view?', [
{ value: 'container', label: '🐳 Container logs (Docker)' },
{ value: 'pod', label: '☸️ Pod logs (Kubernetes)' },
{ value: 'file', label: '📄 Log file (SSH/Local)' },
{ value: 'syslog', label: '🖥️ System logs' },
], (item) => item.label);
if (!logSourceType)
return null;
let targetType = 'all';
switch (logSourceType.value) {
case 'container':
targetType = 'docker';
break;
case 'pod':
targetType = 'k8s';
break;
case 'file':
case 'syslog':
targetType = 'all';
break;
}
const target = await InteractiveHelpers.selectTarget({
message: 'Select target:',
type: targetType,
allowCustom: true,
});
if (!target || Array.isArray(target))
return null;
const targetPattern = target.id;
let logPath;
if (logSourceType.value === 'file') {
const logPathInput = await InteractiveHelpers.inputText('Enter log file path:', {
placeholder: '/var/log/app.log',
validate: (value) => {
if (!value || value.trim().length === 0) {
return 'Log path cannot be empty';
}
return undefined;
},
});
if (!logPathInput)
return null;
logPath = logPathInput;
}
else if (logSourceType.value === 'syslog') {
const syslogType = await InteractiveHelpers.selectFromList('Select system log type:', [
{ value: '/var/log/syslog', label: 'System log' },
{ value: '/var/log/messages', label: 'Messages' },
{ value: '/var/log/auth.log', label: 'Authentication log' },
{ value: '/var/log/kern.log', label: 'Kernel log' },
{ value: 'custom', label: 'Custom path...' },
], (item) => item.label);
if (!syslogType)
return null;
if (syslogType.value === 'custom') {
const customLogPath = await InteractiveHelpers.inputText('Enter custom log path:', {
placeholder: '/var/log/custom.log',
validate: (value) => {
if (!value || value.trim().length === 0) {
return 'Log path cannot be empty';
}
return undefined;
},
});
if (!customLogPath)
return null;
logPath = customLogPath;
}
else {
logPath = syslogType.value;
}
}
const viewingMode = await InteractiveHelpers.selectFromList('How do you want to view the logs?', [
{ value: 'tail', label: '📖 View recent logs (tail)' },
{ value: 'follow', label: '🔄 Stream live logs (follow)' },
{ value: 'search', label: '🔍 Search and filter logs' },
], (item) => item.label);
if (!viewingMode)
return null;
const interactiveOptions = {};
if (viewingMode.value === 'follow') {
interactiveOptions.follow = true;
}
else if (viewingMode.value === 'search') {
const searchPattern = await InteractiveHelpers.inputText('Enter search pattern (regex):', {
placeholder: 'ERROR|WARN',
});
if (searchPattern) {
interactiveOptions.grep = searchPattern;
}
const contextLines = await InteractiveHelpers.selectFromList('Show context around matches?', [
{ value: 'none', label: 'No context' },
{ value: '3', label: '3 lines before and after' },
{ value: '5', label: '5 lines before and after' },
{ value: '10', label: '10 lines before and after' },
], (item) => item.label);
if (contextLines && contextLines.value !== 'none') {
interactiveOptions.context = contextLines.value;
}
const invertMatch = await InteractiveHelpers.confirmAction('Invert match (exclude matching lines)?', false);
if (invertMatch) {
interactiveOptions.invert = true;
}
}
if (!interactiveOptions.follow) {
const tailCount = await InteractiveHelpers.selectFromList('How many recent lines to show?', [
{ value: '50', label: '50 lines' },
{ value: '100', label: '100 lines' },
{ value: '200', label: '200 lines' },
{ value: '500', label: '500 lines' },
{ value: 'custom', label: 'Custom amount...' },
], (item) => item.label);
if (!tailCount)
return null;
if (tailCount.value === 'custom') {
const customCount = await InteractiveHelpers.inputText('Enter number of lines:', {
placeholder: '100',
validate: (value) => {
const num = parseInt(value, 10);
if (isNaN(num) || num <= 0) {
return 'Please enter a positive number';
}
return undefined;
},
});
if (!customCount)
return null;
interactiveOptions.tail = customCount;
}
else {
interactiveOptions.tail = tailCount.value;
}
}
const useTimeRange = await InteractiveHelpers.confirmAction('Filter by time range?', false);
if (useTimeRange) {
const timeRange = await InteractiveHelpers.selectFromList('Select time range:', [
{ value: '5m', label: 'Last 5 minutes' },
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last hour' },
{ value: '6h', label: 'Last 6 hours' },
{ value: '1d', label: 'Last day' },
{ value: 'custom', label: 'Custom time...' },
], (item) => item.label);
if (timeRange) {
if (timeRange.value === 'custom') {
const customTime = await InteractiveHelpers.inputText('Enter time specification:', {
placeholder: '2h (2 hours ago) or 2023-12-01T10:00:00',
});
if (customTime) {
interactiveOptions.since = customTime;
}
}
else {
interactiveOptions.since = timeRange.value;
}
}
}
if (target.type === 'k8s') {
const showPrevious = await InteractiveHelpers.confirmAction('Show logs from previous container instance?', false);
if (showPrevious) {
interactiveOptions.previous = true;
}
const containerName = await InteractiveHelpers.inputText('Container name (leave empty for default):', {
placeholder: 'sidecar, main, etc.',
});
if (containerName) {
interactiveOptions.container = containerName;
}
}
const outputOptions = await InteractiveHelpers.selectFromList('Select output format:', [
{ value: 'default', label: '📝 Standard output' },
{ value: 'timestamps', label: '🕐 Include timestamps' },
{ value: 'json', label: '📋 JSON format' },
], (item) => item.label);
if (outputOptions) {
switch (outputOptions.value) {
case 'timestamps':
interactiveOptions.timestamps = true;
break;
case 'json':
interactiveOptions.json = true;
break;
}
}
const showColors = await InteractiveHelpers.confirmAction('Enable colored output for log levels?', true);
if (!showColors) {
interactiveOptions.color = false;
}
InteractiveHelpers.endInteractiveMode('Logs configuration complete!');
return {
targetPattern,
logPath,
options: interactiveOptions,
};
}
catch (error) {
InteractiveHelpers.showError(`Interactive mode failed: ${error}`);
return null;
}
}
setupCleanupHandlers() {
const cleanup = async () => {
try {
this.running = false;
this.log('\nStopping log streams...', 'info');
for (const [sessionId, stream] of this.streams) {
try {
if (stream.cleanup) {
await stream.cleanup();
}
this.log(`Stopped stream for ${sessionId}`, 'info');
}
catch (error) {
this.log(`Failed to cleanup ${sessionId}: ${error}`, 'error');
}
}
this.streams.clear();
if (process.env['NODE_ENV'] !== 'test') {
process.exit(0);
}
}
catch (error) {
console.error('Error during cleanup:', error);
if (process.env['NODE_ENV'] !== 'test') {
process.exit(1);
}
}
};
const safeCleanup = (...args) => {
cleanup().catch((error) => {
console.error('Unhandled error in cleanup:', error);
if (process.env['NODE_ENV'] !== 'test') {
process.exit(1);
}
});
};
process.once('SIGINT', safeCleanup);
process.once('SIGTERM', safeCleanup);
}
}
export default function command(program) {
const cmd = new LogsCommand();
program.addCommand(cmd.create());
}
//# sourceMappingURL=logs.js.map