import process from 'node:process'; import { Nupst } from '../nupst.ts'; import { logger, type ITableColumn } from '../logger.ts'; import { theme, symbols } from '../colors.ts'; import type { IActionConfig } from '../actions/base-action.ts'; import type { IUpsConfig } from '../daemon.ts'; /** * Class for handling action-related CLI commands * Provides interface for managing UPS actions */ export class ActionHandler { private readonly nupst: Nupst; /** * Create a new action handler * @param nupst Reference to the main Nupst instance */ constructor(nupst: Nupst) { this.nupst = nupst; } /** * Add a new action to a UPS */ public async add(upsId?: string): Promise { try { if (!upsId) { logger.error('UPS ID is required'); logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add ')}`); logger.log(''); logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(''); process.exit(1); } const config = await this.nupst.getDaemon().loadConfig(); const ups = config.upsDevices.find((u) => u.id === upsId); if (!ups) { logger.error(`UPS with ID '${upsId}' not found`); logger.log(''); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(''); process.exit(1); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const prompt = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer); }); }); }; try { logger.log(''); logger.info(`Add Action to ${theme.highlight(ups.name)}`); logger.log(''); // Action type (currently only shutdown is supported) const type = 'shutdown'; logger.log(` ${theme.dim('Action type:')} ${theme.highlight('shutdown')}`); // Battery threshold const batteryStr = await prompt( ` ${theme.dim('Battery threshold')} ${theme.dim('(%):')} `, ); const battery = parseInt(batteryStr, 10); if (isNaN(battery) || battery < 0 || battery > 100) { logger.error('Invalid battery threshold. Must be 0-100.'); process.exit(1); } // Runtime threshold const runtimeStr = await prompt( ` ${theme.dim('Runtime threshold')} ${theme.dim('(minutes):')} `, ); const runtime = parseInt(runtimeStr, 10); if (isNaN(runtime) || runtime < 0) { logger.error('Invalid runtime threshold. Must be >= 0.'); process.exit(1); } // Trigger mode logger.log(''); logger.log(` ${theme.dim('Trigger mode:')}`); logger.log(` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`); logger.log( ` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`, ); logger.log( ` ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`, ); logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`); const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `); const triggerModeMap: Record = { '1': 'onlyPowerChanges', '2': 'onlyThresholds', '3': 'powerChangesAndThresholds', '4': 'anyChange', '': 'onlyThresholds', // Default }; const triggerMode = triggerModeMap[triggerChoice] || 'onlyThresholds'; // Shutdown delay const delayStr = await prompt( ` ${theme.dim('Shutdown delay')} ${theme.dim('(seconds) [5]:')} `, ); const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5; if (isNaN(shutdownDelay) || shutdownDelay < 0) { logger.error('Invalid shutdown delay. Must be >= 0.'); process.exit(1); } // Create the action const newAction: IActionConfig = { type, thresholds: { battery, runtime, }, triggerMode: triggerMode as IActionConfig['triggerMode'], shutdownDelay, }; // Add to UPS if (!ups.actions) { ups.actions = []; } ups.actions.push(newAction); await this.nupst.getDaemon().saveConfig(config); logger.log(''); logger.success(`Action added to ${ups.name}`); logger.log(''); logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`); logger.log(''); } finally { rl.close(); } } catch (error) { logger.error(`Failed to add action: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } /** * Remove an action from a UPS */ public async remove(upsId?: string, actionIndexStr?: string): Promise { try { if (!upsId || !actionIndexStr) { logger.error('UPS ID and action index are required'); logger.log( ` ${theme.dim('Usage:')} ${theme.command('nupst action remove ')}`, ); logger.log(''); logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`); logger.log(''); process.exit(1); } const actionIndex = parseInt(actionIndexStr, 10); if (isNaN(actionIndex) || actionIndex < 0) { logger.error('Invalid action index. Must be >= 0.'); process.exit(1); } const config = await this.nupst.getDaemon().loadConfig(); const ups = config.upsDevices.find((u) => u.id === upsId); if (!ups) { logger.error(`UPS with ID '${upsId}' not found`); logger.log(''); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(''); process.exit(1); } if (!ups.actions || ups.actions.length === 0) { logger.error(`No actions configured for UPS '${ups.name}'`); logger.log(''); process.exit(1); } if (actionIndex >= ups.actions.length) { logger.error( `Invalid action index. UPS '${ups.name}' has ${ups.actions.length} action(s) (index 0-${ups.actions.length - 1})`, ); logger.log(''); logger.log(` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${upsId}`)}`); logger.log(''); process.exit(1); } const removedAction = ups.actions[actionIndex]; ups.actions.splice(actionIndex, 1); await this.nupst.getDaemon().saveConfig(config); logger.log(''); logger.success(`Action removed from ${ups.name}`); logger.log(` ${theme.dim('Type:')} ${removedAction.type}`); if (removedAction.thresholds) { logger.log( ` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`, ); } logger.log(''); logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`); logger.log(''); } catch (error) { logger.error( `Failed to remove action: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } } /** * List all actions for a specific UPS or all UPS devices */ public async list(upsId?: string): Promise { try { const config = await this.nupst.getDaemon().loadConfig(); if (upsId) { // List actions for specific UPS const ups = config.upsDevices.find((u) => u.id === upsId); if (!ups) { logger.error(`UPS with ID '${upsId}' not found`); logger.log(''); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(''); process.exit(1); } this.displayUpsActions(ups); } else { // List actions for all UPS devices logger.log(''); logger.info('Actions for All UPS Devices'); logger.log(''); let hasAnyActions = false; for (const ups of config.upsDevices) { if (ups.actions && ups.actions.length > 0) { hasAnyActions = true; this.displayUpsActions(ups); } } if (!hasAnyActions) { logger.log(` ${theme.dim('No actions configured')}`); logger.log(''); logger.log(` ${theme.dim('Add an action:')} ${theme.command('nupst action add ')}`); logger.log(''); } } } catch (error) { logger.error( `Failed to list actions: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } } /** * Display actions for a single UPS */ private displayUpsActions(ups: IUpsConfig): void { logger.log(`${symbols.info} ${theme.highlight(ups.name)} ${theme.dim(`(${ups.id})`)}`); logger.log(''); if (!ups.actions || ups.actions.length === 0) { logger.log(` ${theme.dim('No actions configured')}`); logger.log(''); return; } const columns: ITableColumn[] = [ { header: 'Index', key: 'index', align: 'right' }, { header: 'Type', key: 'type', align: 'left' }, { header: 'Battery', key: 'battery', align: 'right' }, { header: 'Runtime', key: 'runtime', align: 'right' }, { header: 'Trigger Mode', key: 'triggerMode', align: 'left' }, { header: 'Delay', key: 'delay', align: 'right' }, ]; const rows = ups.actions.map((action, index) => ({ index: theme.dim(index.toString()), type: theme.highlight(action.type), battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'), runtime: action.thresholds ? `${action.thresholds.runtime}min` : theme.dim('N/A'), triggerMode: theme.dim(action.triggerMode || 'onlyThresholds'), delay: `${action.shutdownDelay || 5}s`, })); logger.logTable(columns, rows); logger.log(''); } }