The daemon already has automatic config file watching and reloads changes without requiring a restart. Updated action handler messages to correctly reflect this behavior. Changed: - 'Restart service to apply changes: nupst service restart' → 'Changes saved and will be applied automatically' The config file watcher (daemon.ts:986) uses Deno.watchFs() to monitor /etc/nupst/config.json and automatically calls reloadConfig() when changes are detected. No restart needed.
		
			
				
	
	
		
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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, IGroupConfig } 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 or group
 | |
|    */
 | |
|   public async add(targetId?: string): Promise<void> {
 | |
|     try {
 | |
|       if (!targetId) {
 | |
|         logger.error('Target ID is required');
 | |
|         logger.log(
 | |
|           `  ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
 | |
|         );
 | |
|         logger.log('');
 | |
|         logger.log(`  ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
 | |
|         logger.log(`  ${theme.dim('List groups:')} ${theme.command('nupst group list')}`);
 | |
|         logger.log('');
 | |
|         process.exit(1);
 | |
|       }
 | |
| 
 | |
|       const config = await this.nupst.getDaemon().loadConfig();
 | |
| 
 | |
|       // Check if it's a UPS
 | |
|       const ups = config.upsDevices.find((u) => u.id === targetId);
 | |
|       // Check if it's a group
 | |
|       const group = config.groups?.find((g) => g.id === targetId);
 | |
| 
 | |
|       if (!ups && !group) {
 | |
|         logger.error(`UPS or Group with ID '${targetId}' not found`);
 | |
|         logger.log('');
 | |
|         logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
 | |
|         logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
 | |
|         logger.log('');
 | |
|         process.exit(1);
 | |
|       }
 | |
| 
 | |
|       const target = ups || group;
 | |
|       const targetType = ups ? 'UPS' : 'Group';
 | |
|       const targetName = ups ? ups.name : group!.name;
 | |
| 
 | |
|       const readline = await import('node:readline');
 | |
|       const rl = readline.createInterface({
 | |
|         input: process.stdin,
 | |
|         output: process.stdout,
 | |
|       });
 | |
| 
 | |
|       const prompt = (question: string): Promise<string> => {
 | |
|         return new Promise((resolve) => {
 | |
|           rl.question(question, (answer: string) => {
 | |
|             resolve(answer);
 | |
|           });
 | |
|         });
 | |
|       };
 | |
| 
 | |
|       try {
 | |
|         logger.log('');
 | |
|         logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
 | |
|         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<string, string> = {
 | |
|           '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 target (UPS or group)
 | |
|         if (!target!.actions) {
 | |
|           target!.actions = [];
 | |
|         }
 | |
|         target!.actions.push(newAction);
 | |
| 
 | |
|         await this.nupst.getDaemon().saveConfig(config);
 | |
| 
 | |
|         logger.log('');
 | |
|         logger.success(`Action added to ${targetType} ${targetName}`);
 | |
|         logger.log(`  ${theme.dim('Changes saved and will be applied automatically')}`);
 | |
|         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 or group
 | |
|    */
 | |
|   public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
 | |
|     try {
 | |
|       if (!targetId || !actionIndexStr) {
 | |
|         logger.error('Target ID and action index are required');
 | |
|         logger.log(
 | |
|           `  ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
 | |
|         );
 | |
|         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();
 | |
| 
 | |
|       // Check if it's a UPS
 | |
|       const ups = config.upsDevices.find((u) => u.id === targetId);
 | |
|       // Check if it's a group
 | |
|       const group = config.groups?.find((g) => g.id === targetId);
 | |
| 
 | |
|       if (!ups && !group) {
 | |
|         logger.error(`UPS or Group with ID '${targetId}' not found`);
 | |
|         logger.log('');
 | |
|         logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
 | |
|         logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
 | |
|         logger.log('');
 | |
|         process.exit(1);
 | |
|       }
 | |
| 
 | |
|       const target = ups || group;
 | |
|       const targetType = ups ? 'UPS' : 'Group';
 | |
|       const targetName = ups ? ups.name : group!.name;
 | |
| 
 | |
|       if (!target!.actions || target!.actions.length === 0) {
 | |
|         logger.error(`No actions configured for ${targetType} '${targetName}'`);
 | |
|         logger.log('');
 | |
|         process.exit(1);
 | |
|       }
 | |
| 
 | |
|       if (actionIndex >= target!.actions.length) {
 | |
|         logger.error(
 | |
|           `Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
 | |
|         );
 | |
|         logger.log('');
 | |
|         logger.log(
 | |
|           `  ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
 | |
|         );
 | |
|         logger.log('');
 | |
|         process.exit(1);
 | |
|       }
 | |
| 
 | |
|       const removedAction = target!.actions[actionIndex];
 | |
|       target!.actions.splice(actionIndex, 1);
 | |
| 
 | |
|       await this.nupst.getDaemon().saveConfig(config);
 | |
| 
 | |
|       logger.log('');
 | |
|       logger.success(`Action removed from ${targetType} ${targetName}`);
 | |
|       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(`  ${theme.dim('Changes saved and will be applied automatically')}`);
 | |
|       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/group or all devices
 | |
|    */
 | |
|   public async list(targetId?: string): Promise<void> {
 | |
|     try {
 | |
|       const config = await this.nupst.getDaemon().loadConfig();
 | |
| 
 | |
|       if (targetId) {
 | |
|         // List actions for specific UPS or group
 | |
|         const ups = config.upsDevices.find((u) => u.id === targetId);
 | |
|         const group = config.groups?.find((g) => g.id === targetId);
 | |
| 
 | |
|         if (!ups && !group) {
 | |
|           logger.error(`UPS or Group with ID '${targetId}' not found`);
 | |
|           logger.log('');
 | |
|           logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
 | |
|           logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
 | |
|           logger.log('');
 | |
|           process.exit(1);
 | |
|         }
 | |
| 
 | |
|         if (ups) {
 | |
|           this.displayTargetActions(ups, 'UPS');
 | |
|         } else {
 | |
|           this.displayTargetActions(group!, 'Group');
 | |
|         }
 | |
|       } else {
 | |
|         // List actions for all UPS devices and groups
 | |
|         logger.log('');
 | |
|         logger.info('Actions for All UPS Devices and Groups');
 | |
|         logger.log('');
 | |
| 
 | |
|         let hasAnyActions = false;
 | |
| 
 | |
|         // Display UPS actions
 | |
|         for (const ups of config.upsDevices) {
 | |
|           if (ups.actions && ups.actions.length > 0) {
 | |
|             hasAnyActions = true;
 | |
|             this.displayTargetActions(ups, 'UPS');
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // Display Group actions
 | |
|         for (const group of config.groups || []) {
 | |
|           if (group.actions && group.actions.length > 0) {
 | |
|             hasAnyActions = true;
 | |
|             this.displayTargetActions(group, 'Group');
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (!hasAnyActions) {
 | |
|           logger.log(`  ${theme.dim('No actions configured')}`);
 | |
|           logger.log('');
 | |
|           logger.log(
 | |
|             `  ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
 | |
|           );
 | |
|           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 or Group
 | |
|    */
 | |
|   private displayTargetActions(
 | |
|     target: IUpsConfig | IGroupConfig,
 | |
|     targetType: 'UPS' | 'Group',
 | |
|   ): void {
 | |
|     logger.log(
 | |
|       `${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
 | |
|     );
 | |
|     logger.log('');
 | |
| 
 | |
|     if (!target.actions || target.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 = target.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('');
 | |
|   }
 | |
| }
 |