Added comprehensive action management: Commands: - nupst action add <ups-id> - Add a new action to a UPS interactively - nupst action remove <ups-id> <index> - Remove an action by index - nupst action list [ups-id] - List all actions (optionally for specific UPS) Features: - Interactive prompts for action configuration - Battery and runtime threshold configuration - Trigger mode selection (onlyPowerChanges, onlyThresholds, powerChangesAndThresholds, anyChange) - Shutdown delay configuration - Table-based display of actions with indices - Support for managing actions across multiple UPS devices Implementation: - Created ActionHandler class in ts/cli/action-handler.ts - Integrated with existing CLI infrastructure - Added to nupst.ts, cli.ts, and help system - Proper TypeScript typing throughout Closes the gap where users had to manually edit config.json to manage actions.
312 lines
10 KiB
TypeScript
312 lines
10 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 } 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<void> {
|
|
try {
|
|
if (!upsId) {
|
|
logger.error('UPS ID is required');
|
|
logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id>')}`);
|
|
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<string> => {
|
|
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<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 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<void> {
|
|
try {
|
|
if (!upsId || !actionIndexStr) {
|
|
logger.error('UPS ID and action index are required');
|
|
logger.log(
|
|
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-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();
|
|
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<void> {
|
|
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 <ups-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
|
|
*/
|
|
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('');
|
|
}
|
|
}
|