UNPKG

lsh-framework

Version:

A powerful, extensible shell with advanced job management, database persistence, and modern CLI features

287 lines (286 loc) 9.22 kB
/** * 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;