import { execSync } from 'node:child_process'; import { Nupst } from './nupst.ts'; import { logger, type ITableColumn } from './logger.ts'; import { theme, symbols } from './colors.ts'; /** * Class for handling CLI commands * Provides interface between user commands and the application */ export class NupstCli { private readonly nupst: Nupst; /** * Create a new CLI handler */ constructor() { this.nupst = new Nupst(); } /** * Parse command line arguments and execute the appropriate command * @param args Command line arguments (process.argv) */ public async parseAndExecute(args: string[]): Promise { // Extract debug and version flags from any position const debugOptions = this.extractDebugOptions(args); if (debugOptions.debugMode) { logger.log('Debug mode enabled'); // Enable debug mode in the SNMP client this.nupst.getSnmp().enableDebug(); } // Check for version flag if (debugOptions.cleanedArgs.includes('--version') || debugOptions.cleanedArgs.includes('-v')) { this.showVersion(); return; } // Get the command (default to help if none provided) const command = debugOptions.cleanedArgs[2] || 'help'; const commandArgs = debugOptions.cleanedArgs.slice(3); // Route to the appropriate command handler await this.executeCommand(command, commandArgs, debugOptions.debugMode); } /** * Extract and remove debug options from args * @param args Command line arguments * @returns Object with debug flags and cleaned args */ private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { const debugMode = args.includes('--debug') || args.includes('-d'); // Remove debug flags from args const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d'); return { debugMode, cleanedArgs }; } /** * Execute the command with the given arguments * @param command Command to execute * @param commandArgs Additional command arguments * @param debugMode Whether debug mode is enabled */ private async executeCommand( command: string, commandArgs: string[], debugMode: boolean, ): Promise { // Get access to the handlers const upsHandler = this.nupst.getUpsHandler(); const groupHandler = this.nupst.getGroupHandler(); const serviceHandler = this.nupst.getServiceHandler(); const actionHandler = this.nupst.getActionHandler(); // Handle service subcommands if (command === 'service') { const subcommand = commandArgs[0] || 'status'; switch (subcommand) { case 'enable': await serviceHandler.enable(); break; case 'disable': await serviceHandler.disable(); break; case 'start': await serviceHandler.start(); break; case 'stop': await serviceHandler.stop(); break; case 'restart': await serviceHandler.stop(); await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s await serviceHandler.start(); break; case 'status': await serviceHandler.status(); break; case 'logs': await serviceHandler.logs(); break; case 'start-daemon': await serviceHandler.daemonStart(debugMode); break; default: this.showServiceHelp(); break; } return; } // Handle UPS subcommands if (command === 'ups') { const subcommand = commandArgs[0] || 'list'; const subcommandArgs = commandArgs.slice(1); switch (subcommand) { case 'add': await upsHandler.add(); break; case 'edit': { const upsId = subcommandArgs[0]; await upsHandler.edit(upsId); break; } case 'remove': case 'rm': { const upsIdToRemove = subcommandArgs[0]; if (!upsIdToRemove) { logger.error('UPS ID is required for remove command'); this.showUpsHelp(); return; } await upsHandler.remove(upsIdToRemove); break; } case 'list': case 'ls': // Alias await upsHandler.list(); break; case 'test': await upsHandler.test(debugMode); break; default: this.showUpsHelp(); break; } return; } // Handle group subcommands if (command === 'group') { const subcommand = commandArgs[0] || 'list'; const subcommandArgs = commandArgs.slice(1); switch (subcommand) { case 'add': await groupHandler.add(); break; case 'edit': { const groupId = subcommandArgs[0]; if (!groupId) { logger.error('Group ID is required for edit command'); this.showGroupHelp(); return; } await groupHandler.edit(groupId); break; } case 'remove': case 'rm': { const groupIdToRemove = subcommandArgs[0]; if (!groupIdToRemove) { logger.error('Group ID is required for remove command'); this.showGroupHelp(); return; } await groupHandler.remove(groupIdToRemove); break; } case 'list': case 'ls': // Alias await groupHandler.list(); break; default: this.showGroupHelp(); break; } return; } // Handle action subcommands if (command === 'action') { const subcommand = commandArgs[0] || 'list'; const subcommandArgs = commandArgs.slice(1); switch (subcommand) { case 'add': { const upsId = subcommandArgs[0]; await actionHandler.add(upsId); break; } case 'remove': case 'rm': { const upsId = subcommandArgs[0]; const actionIndex = subcommandArgs[1]; await actionHandler.remove(upsId, actionIndex); break; } case 'list': case 'ls': { // Alias const upsId = subcommandArgs[0]; await actionHandler.list(upsId); break; } default: this.showActionHelp(); break; } return; } // Handle config subcommand if (command === 'config') { const subcommand = commandArgs[0] || 'show'; switch (subcommand) { case 'show': case 'display': await this.showConfig(); break; default: await this.showConfig(); break; } return; } // Handle top-level commands switch (command) { case 'update': await serviceHandler.update(); break; case 'uninstall': await serviceHandler.uninstall(); break; case 'help': case '--help': case '-h': this.showHelp(); break; default: logger.error(`Unknown command: ${command}`); logger.log(''); this.showHelp(); break; } } /** * Display the current configuration */ private async showConfig(): Promise { try { // Try to load configuration try { await this.nupst.getDaemon().loadConfig(); } catch (_error) { logger.logBox('Configuration Error', [ 'No configuration found.', "Please run 'nupst ups add' first to create a configuration.", ], 50, 'error'); return; } // Get current configuration const config = this.nupst.getDaemon().getConfig(); // Check if multi-UPS config if (config.upsDevices && Array.isArray(config.upsDevices)) { // === Multi-UPS Configuration === // Overview Box logger.log(''); logger.logBox('NUPST Configuration', [ `UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`, `Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`, `Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`, '', theme.dim('Configuration File:'), ` ${theme.path('/etc/nupst/config.json')}`, ], 60, 'info'); // UPS Devices Table if (config.upsDevices.length > 0) { const upsRows = config.upsDevices.map((ups) => ({ name: ups.name, id: theme.dim(ups.id), host: `${ups.snmp.host}:${ups.snmp.port}`, model: ups.snmp.upsModel || 'cyberpower', actions: `${(ups.actions || []).length} configured`, groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'), })); const upsColumns: ITableColumn[] = [ { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'ID', key: 'id', align: 'left' }, { header: 'Host:Port', key: 'host', align: 'left', color: theme.info }, { header: 'Model', key: 'model', align: 'left' }, { header: 'Actions', key: 'actions', align: 'left' }, { header: 'Groups', key: 'groups', align: 'left' }, ]; logger.log(''); logger.info(`UPS Devices (${config.upsDevices.length}):`); logger.log(''); logger.logTable(upsColumns, upsRows); } // Groups Table if (config.groups && config.groups.length > 0) { const groupRows = config.groups.map((group) => { const upsInGroup = config.upsDevices.filter((ups) => ups.groups && ups.groups.includes(group.id) ); return { name: group.name, id: theme.dim(group.id), mode: group.mode, upsCount: String(upsInGroup.length), ups: upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : theme.dim('None'), description: group.description || theme.dim('—'), }; }); const groupColumns: ITableColumn[] = [ { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'ID', key: 'id', align: 'left' }, { header: 'Mode', key: 'mode', align: 'left', color: theme.info }, { header: 'UPS', key: 'upsCount', align: 'right' }, { header: 'UPS Devices', key: 'ups', align: 'left' }, { header: 'Description', key: 'description', align: 'left' }, ]; logger.log(''); logger.info(`UPS Groups (${config.groups.length}):`); logger.log(''); logger.logTable(groupColumns, groupRows); } } else { // === Legacy Single UPS Configuration === if (!config.snmp) { logger.logBox('Configuration Error', [ 'Error: Legacy configuration missing SNMP settings', ], 60, 'error'); return; } logger.log(''); logger.logBox('NUPST Configuration (Legacy)', [ theme.warning('Legacy single-UPS configuration format'), '', theme.dim('SNMP Settings:'), ` Host: ${theme.info(config.snmp.host)}`, ` Port: ${theme.info(String(config.snmp.port))}`, ` Version: ${config.snmp.version}`, ` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`, ...(config.snmp.version === 1 || config.snmp.version === 2 ? [` Community: ${config.snmp.community}`] : [] ), ...(config.snmp.version === 3 ? [ ` Security Level: ${config.snmp.securityLevel}`, ` Username: ${config.snmp.username}`, ...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv' ? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`] : [] ), ...(config.snmp.securityLevel === 'authPriv' ? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`] : [] ), ` Timeout: ${config.snmp.timeout / 1000} seconds`, ] : [] ), ...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs ? [ theme.dim('Custom OIDs:'), ` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`, ` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`, ` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`, ] : [] ), '', ` Check Interval: ${config.checkInterval / 1000} seconds`, '', theme.dim('Configuration File:'), ` ${theme.path('/etc/nupst/config.json')}`, '', theme.warning('Note: Using legacy single-UPS configuration format.'), `Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`, ], 70, 'warning'); } // Service Status try { const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; const isEnabled = execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; logger.log(''); logger.logBox('Service Status', [ `Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`, `Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`, ], 50, isActive ? 'success' : 'default'); logger.log(''); } catch (_error) { // Ignore errors checking service status } } catch (error) { logger.error( `Failed to display configuration: ${ error instanceof Error ? error.message : String(error) }`, ); } } /** * Display version information */ private showVersion(): void { const version = this.nupst.getVersion(); logger.log(`NUPST version ${version}`); logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)'); } /** * Display help message */ private showHelp(): void { console.log(''); logger.highlight('NUPST - UPS Shutdown Tool'); logger.dim('Deno-powered UPS monitoring and shutdown automation'); console.log(''); // Usage section logger.log(theme.info('Usage:')); logger.log(` ${theme.command('nupst')} ${theme.dim(' [options]')}`); console.log(''); // Main commands section logger.log(theme.info('Commands:')); this.printCommand('service ', 'Manage systemd service'); this.printCommand('ups ', 'Manage UPS devices'); this.printCommand('group ', 'Manage UPS groups'); this.printCommand('action ', 'Manage UPS actions'); this.printCommand('config [show]', 'Display current configuration'); this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)')); this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)')); this.printCommand('help, --help, -h', 'Show this help message'); this.printCommand('--version, -v', 'Show version information'); console.log(''); // Service subcommands logger.log(theme.info('Service Subcommands:')); this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)')); this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)')); this.printCommand('nupst service start', 'Start the systemd service'); this.printCommand('nupst service stop', 'Stop the systemd service'); this.printCommand('nupst service restart', 'Restart the systemd service'); this.printCommand('nupst service status', 'Show service and UPS status'); this.printCommand('nupst service logs', 'Show service logs in real-time'); this.printCommand('nupst service start-daemon', 'Start daemon process directly'); console.log(''); // UPS subcommands logger.log(theme.info('UPS Subcommands:')); this.printCommand('nupst ups add', 'Add a new UPS device'); this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)'); this.printCommand('nupst ups remove ', 'Remove a UPS device by ID'); this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices'); this.printCommand('nupst ups test', 'Test UPS connections'); console.log(''); // Group subcommands logger.log(theme.info('Group Subcommands:')); this.printCommand('nupst group add', 'Add a new UPS group'); this.printCommand('nupst group edit ', 'Edit an existing UPS group'); this.printCommand('nupst group remove ', 'Remove a UPS group by ID'); this.printCommand('nupst group list (or ls)', 'List all UPS groups'); console.log(''); // Action subcommands logger.log(theme.info('Action Subcommands:')); this.printCommand('nupst action add ', 'Add a new action to a UPS or group'); this.printCommand('nupst action remove ', 'Remove an action by index'); this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)'); console.log(''); // Options logger.log(theme.info('Options:')); this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging'); logger.dim(' (Example: nupst ups test --debug)'); console.log(''); // Examples logger.log(theme.info('Examples:')); logger.dim(' nupst service enable # Install and start the service'); logger.dim(' nupst ups add # Add a new UPS interactively'); logger.dim(' nupst group list # Show all configured groups'); logger.dim(' nupst config # Display current configuration'); console.log(''); } /** * Helper to print a command with description */ private printCommand(command: string, description: string, extra?: string): void { const paddedCommand = command.padEnd(30); logger.log(` ${theme.command(paddedCommand)} ${description}${extra ? ' ' + extra : ''}`); } /** * Display help message for service commands */ private showServiceHelp(): void { logger.log(` NUPST - Service Management Commands Usage: nupst service Subcommands: enable - Install and enable the systemd service (requires root) disable - Stop and disable the systemd service (requires root) start - Start the systemd service stop - Stop the systemd service restart - Restart the systemd service status - Show service status and UPS information logs - Show service logs in real-time start-daemon - Start the daemon process directly (for testing) Options: --debug, -d - Enable debug mode for detailed logging `); } /** * Display help message for UPS commands */ private showUpsHelp(): void { logger.log(` NUPST - UPS Management Commands Usage: nupst ups [arguments] Subcommands: add - Add a new UPS device interactively edit [id] - Edit a UPS device (edits default if no ID provided) remove - Remove a UPS device by ID (alias: rm) list - List all configured UPS devices (alias: ls) test - Test connections to all configured UPS devices Options: --debug, -d - Enable debug mode for detailed SNMP logging Examples: nupst ups add - Add a new UPS device nupst ups edit ups-1 - Edit UPS with ID 'ups-1' nupst ups remove ups-1 - Remove UPS with ID 'ups-1' nupst ups test --debug - Test all UPS connections with debug output `); } /** * Display help message for group commands */ private showGroupHelp(): void { logger.log(` NUPST - Group Management Commands Usage: nupst group [arguments] Subcommands: add - Add a new UPS group interactively edit - Edit an existing UPS group remove - Remove a UPS group by ID (alias: rm) list - List all UPS groups (alias: ls) Options: --debug, -d - Enable debug mode for detailed logging Examples: nupst group add - Create a new group nupst group edit dc-1 - Edit group with ID 'dc-1' nupst group remove dc-1 - Remove group with ID 'dc-1' `); } private showActionHelp(): void { logger.log(` NUPST - Action Management Commands Usage: nupst action [arguments] Subcommands: add - Add a new action to a UPS or group interactively remove - Remove an action by index (alias: rm) list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls) Options: --debug, -d - Enable debug mode for detailed logging Examples: nupst action list - List actions for all UPS devices and groups nupst action list default - List actions for UPS or group with ID 'default' nupst action add default - Add a new action to UPS or group 'default' nupst action remove default 0 - Remove action at index 0 from UPS or group 'default' nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1' `); } }