lsh-framework
Version: 
A powerful, extensible shell with advanced job management, database persistence, and modern CLI features
287 lines (286 loc) • 9.22 kB
JavaScript
/**
 * Base Command Registrar
 *
 * Abstract base class for command registration to eliminate duplication in:
 * - Command setup patterns
 * - Error handling
 * - Daemon client management
 * - Output formatting
 *
 * Usage:
 * ```typescript
 * class MyCommandRegistrar extends BaseCommandRegistrar {
 *   constructor() {
 *     super('MyService');
 *   }
 *
 *   async register(program: Command): Promise<void> {
 *     const cmd = this.createCommand(program, 'mycommand', 'My command description');
 *
 *     this.addSubcommand(cmd, {
 *       name: 'list',
 *       description: 'List items',
 *       action: async () => {
 *         await this.withDaemonAction(async (client) => {
 *           const items = await client.listItems();
 *           this.logSuccess('Items:', items);
 *         });
 *       }
 *     });
 *   }
 * }
 * ```
 */
import CronJobManager from './cron-job-manager.js';
import { createLogger } from './logger.js';
import { withDaemonClient, withDaemonClientForUser, isDaemonRunning } from './daemon-client-helper.js';
/**
 * Base class for command registrars
 */
export class BaseCommandRegistrar {
    logger;
    serviceName;
    constructor(serviceName) {
        this.serviceName = serviceName;
        this.logger = createLogger(serviceName);
    }
    /**
     * Create a top-level command
     */
    createCommand(program, name, description) {
        return program
            .command(name)
            .description(description);
    }
    /**
     * Add a subcommand with automatic error handling
     */
    addSubcommand(parent, config) {
        let cmd = parent.command(config.name).description(config.description);
        // Add arguments
        if (config.arguments) {
            config.arguments.forEach(arg => {
                const argStr = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
                if (arg.description) {
                    cmd = cmd.argument(argStr, arg.description);
                }
                else {
                    cmd = cmd.argument(argStr);
                }
            });
        }
        // Add options
        if (config.options) {
            config.options.forEach(opt => {
                if (opt.defaultValue !== undefined) {
                    cmd = cmd.option(opt.flags, opt.description, opt.defaultValue);
                }
                else {
                    cmd = cmd.option(opt.flags, opt.description);
                }
            });
        }
        // Wrap action with error handling
        cmd.action(async (...args) => {
            try {
                await config.action(...args);
            }
            catch (error) {
                this.logError('Command failed', error);
                process.exit(1);
            }
        });
        return cmd;
    }
    /**
     * Execute an action with daemon client
     */
    async withDaemonAction(action, config = {}) {
        const { requireRunning = true, exitOnError = true, forUser = false } = config;
        const helper = forUser ? withDaemonClientForUser : withDaemonClient;
        return await helper(action, { requireRunning, exitOnError });
    }
    /**
     * Execute an action with CronJobManager
     */
    async withCronManager(action, config = {}) {
        const { requireRunning = true } = config;
        const manager = new CronJobManager();
        if (requireRunning && !manager.isDaemonRunning()) {
            this.logError('Daemon is not running. Start it with: lsh daemon start');
            process.exit(1);
        }
        try {
            await manager.connect();
            const result = await action(manager);
            manager.disconnect();
            return result;
        }
        catch (error) {
            manager.disconnect();
            throw error;
        }
    }
    /**
     * Check if daemon is running
     */
    isDaemonRunning() {
        return isDaemonRunning();
    }
    /**
     * Log success message
     */
    logSuccess(message, data) {
        this.logger.info(message);
        if (data !== undefined) {
            if (typeof data === 'object' && !Array.isArray(data)) {
                Object.entries(data).forEach(([key, value]) => {
                    this.logger.info(`  ${key}: ${value}`);
                });
            }
            else if (Array.isArray(data)) {
                data.forEach(item => {
                    if (typeof item === 'object') {
                        this.logger.info(`  ${JSON.stringify(item, null, 2)}`);
                    }
                    else {
                        this.logger.info(`  ${item}`);
                    }
                });
            }
            else {
                this.logger.info(`  ${data}`);
            }
        }
    }
    /**
     * Log error message
     */
    logError(message, error) {
        if (error instanceof Error) {
            this.logger.error(message, error);
        }
        else if (error) {
            this.logger.error(`${message}: ${error}`);
        }
        else {
            this.logger.error(message);
        }
    }
    /**
     * Log info message
     */
    logInfo(message) {
        this.logger.info(message);
    }
    /**
     * Log warning message
     */
    logWarning(message) {
        this.logger.warn(message);
    }
    /**
     * Parse JSON from string with error handling
     */
    parseJSON(jsonString, context = 'JSON') {
        try {
            return JSON.parse(jsonString);
        }
        catch (error) {
            throw new Error(`Invalid ${context}: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    /**
     * Parse comma-separated tags
     */
    parseTags(tagsString) {
        return tagsString.split(',').map(t => t.trim()).filter(t => t.length > 0);
    }
    /**
     * Format job schedule for display
     */
    formatSchedule(schedule) {
        if (schedule?.cron) {
            return schedule.cron;
        }
        if (schedule?.interval) {
            return `${schedule.interval}ms interval`;
        }
        return 'No schedule';
    }
    /**
     * Validate required options
     */
    validateRequired(options, required, commandName = 'command') {
        const missing = required.filter(key => !options[key]);
        if (missing.length > 0) {
            throw new Error(`Missing required options for ${commandName}: ${missing.map(k => `--${k}`).join(', ')}`);
        }
    }
    /**
     * Create a standardized job specification from options
     */
    createJobSpec(options) {
        return {
            id: options.id || `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
            name: options.name,
            description: options.description,
            command: options.command,
            schedule: {
                cron: options.schedule,
                interval: options.interval ? parseInt(options.interval) : undefined,
            },
            workingDirectory: options.workingDir,
            environment: options.env ? this.parseJSON(options.env, 'environment variables') : {},
            tags: options.tags ? this.parseTags(options.tags) : [],
            priority: options.priority ? parseInt(options.priority) : 5,
            maxRetries: options.maxRetries ? parseInt(options.maxRetries) : 3,
            timeout: options.timeout ? parseInt(options.timeout) : 0,
            databaseSync: options.databaseSync !== false,
        };
    }
    /**
     * Display job information
     */
    displayJob(job) {
        this.logInfo(`  ${job.id}: ${job.name}`);
        this.logInfo(`    Command: ${job.command}`);
        this.logInfo(`    Schedule: ${this.formatSchedule(job.schedule)}`);
        this.logInfo(`    Status: ${job.status}`);
        this.logInfo(`    Priority: ${job.priority}`);
        if (job.tags && job.tags.length > 0) {
            this.logInfo(`    Tags: ${job.tags.join(', ')}`);
        }
    }
    /**
     * Display multiple jobs
     */
    displayJobs(jobs) {
        this.logInfo(`Jobs (${jobs.length} total):`);
        jobs.forEach(job => {
            this.displayJob(job);
            this.logInfo('');
        });
    }
    /**
     * Display job report
     */
    displayJobReport(report) {
        this.logInfo(`Job Report: ${report.jobId || 'N/A'}`);
        this.logInfo(`  Executions: ${report.executions}`);
        this.logInfo(`  Successes: ${report.successes}`);
        this.logInfo(`  Failures: ${report.failures}`);
        this.logInfo(`  Success Rate: ${report.successRate.toFixed(1)}%`);
        this.logInfo(`  Average Duration: ${Math.round(report.averageDuration)}ms`);
        this.logInfo(`  Last Execution: ${report.lastExecution?.toISOString() || 'Never'}`);
        this.logInfo(`  Last Success: ${report.lastSuccess?.toISOString() || 'Never'}`);
        this.logInfo(`  Last Failure: ${report.lastFailure?.toISOString() || 'Never'}`);
        if (report.commonErrors && report.commonErrors.length > 0) {
            this.logInfo('\n  Common Errors:');
            report.commonErrors.forEach((error) => {
                this.logInfo(`    - ${error.error} (${error.count} times)`);
            });
        }
    }
}
export default BaseCommandRegistrar;