feat(core): Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors

This commit is contained in:
2026-01-29 17:04:12 +00:00
parent d0e3a4ae74
commit 07648b4880
24 changed files with 1019 additions and 590 deletions

View File

@@ -4,6 +4,7 @@ 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';
import * as helpers from '../helpers/index.ts';
/**
* Class for handling action-related CLI commands
@@ -57,21 +58,7 @@ export class ActionHandler {
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 {
await helpers.withPrompt(async (prompt) => {
logger.log('');
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
logger.log('');
@@ -154,9 +141,7 @@ export class ActionHandler {
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)}`,

View File

@@ -25,26 +25,9 @@ export class FeatureHandler {
*/
public async configureHttpServer(): Promise<void> {
try {
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 {
await helpers.withPrompt(async (prompt) => {
await this.runHttpServerConfig(prompt);
} finally {
rl.close();
process.stdin.destroy();
}
});
} catch (error) {
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
}
@@ -186,17 +169,9 @@ export class FeatureHandler {
if (isActive) {
logger.log('');
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question('Service is running. Restart to apply changes? (Y/n): ', resolve);
});
rl.close();
const { prompt, close } = await helpers.createPrompt();
const answer = await prompt('Service is running. Restart to apply changes? (Y/n): ');
close();
if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
logger.info('Restarting service...');

View File

@@ -3,7 +3,7 @@ import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import { type IGroupConfig } from '../daemon.ts';
import type { IGroupConfig, IUpsConfig, INupstConfig } from '../daemon.ts';
/**
* Class for handling group-related CLI commands
@@ -100,24 +100,7 @@ export class GroupHandler {
*/
public async add(): Promise<void> {
try {
// Import readline module for user input
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await helpers.withPrompt(async (prompt) => {
// Try to load configuration
try {
await this.nupst.getDaemon().loadConfig();
@@ -200,10 +183,7 @@ export class GroupHandler {
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup setup complete!');
} finally {
rl.close();
process.stdin.destroy();
}
});
} catch (error) {
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
}
@@ -215,24 +195,7 @@ export class GroupHandler {
*/
public async edit(groupId: string): Promise<void> {
try {
// Import readline module for user input
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await helpers.withPrompt(async (prompt) => {
// Try to load configuration
try {
await this.nupst.getDaemon().loadConfig();
@@ -318,10 +281,7 @@ export class GroupHandler {
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup edit complete!');
} finally {
rl.close();
process.stdin.destroy();
}
});
} catch (error) {
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
}
@@ -362,23 +322,11 @@ export class GroupHandler {
const groupToDelete = config.groups[groupIndex];
// Get confirmation before deleting
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const confirm = await new Promise<string>((resolve) => {
rl.question(
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
(answer) => {
resolve(answer.toLowerCase());
},
);
});
rl.close();
process.stdin.destroy();
const { prompt, close } = await helpers.createPrompt();
const confirm = (await prompt(
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
)).toLowerCase();
close();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
@@ -419,8 +367,8 @@ export class GroupHandler {
* @param prompt Function to prompt for user input
*/
public async assignUpsToGroups(
ups: any,
groups: any[],
ups: IUpsConfig,
groups: IGroupConfig[],
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Initialize groups array if it doesn't exist
@@ -514,7 +462,7 @@ export class GroupHandler {
*/
public async assignUpsToGroup(
groupId: string,
config: any,
config: INupstConfig,
prompt: (question: string) => Promise<string>,
): Promise<void> {
if (!config.upsDevices || config.upsDevices.length === 0) {
@@ -522,7 +470,7 @@ export class GroupHandler {
return;
}
const group = config.groups.find((g: { id: string }) => g.id === groupId);
const group = config.groups.find((g) => g.id === groupId);
if (!group) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
@@ -530,7 +478,7 @@ export class GroupHandler {
// Show current assignments
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(groupId)
);
if (upsInGroup.length === 0) {

View File

@@ -2,6 +2,7 @@ import process from 'node:process';
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import * as helpers from '../helpers/index.ts';
/**
* Class for handling service-related CLI commands
@@ -196,22 +197,7 @@ export class ServiceHandler {
this.checkRootAccess('This command must be run as root.');
try {
// Import readline module for user input
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
const { prompt, close } = await helpers.createPrompt();
logger.log('');
logger.highlight('NUPST Uninstaller');
@@ -254,15 +240,13 @@ export class ServiceHandler {
if (!uninstallScriptPath) {
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close();
process.stdin.destroy();
close();
process.exit(1);
}
}
// Close readline before executing script
rl.close();
process.stdin.destroy();
// Close prompt before executing script
close();
// Execute uninstall.sh with the appropriate option
logger.log('');

View File

@@ -4,8 +4,17 @@ import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import type { TUpsModel } from '../snmp/types.ts';
import type { INupstConfig } from '../daemon.ts';
import type { ISnmpConfig, TUpsModel, IUpsStatus as ISnmpUpsStatus } from '../snmp/types.ts';
import type { INupstConfig, IUpsConfig, IUpsStatus } from '../daemon.ts';
import type { IActionConfig } from '../actions/base-action.ts';
/**
* Thresholds configuration for CLI display
*/
interface IThresholds {
battery: number;
runtime: number;
}
/**
* Class for handling UPS-related CLI commands
@@ -27,29 +36,9 @@ export class UpsHandler {
*/
public async add(): Promise<void> {
try {
// Import readline module for user input
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await helpers.withPrompt(async (prompt) => {
await this.runAddProcess(prompt);
} finally {
rl.close();
process.stdin.destroy();
}
});
} catch (error) {
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
}
@@ -160,29 +149,9 @@ export class UpsHandler {
*/
public async edit(upsId?: string): Promise<void> {
try {
// Import readline module for user input
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await helpers.withPrompt(async (prompt) => {
await this.runEditProcess(upsId, prompt);
} finally {
rl.close();
process.stdin.destroy();
}
});
} catch (error) {
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
}
@@ -337,23 +306,11 @@ export class UpsHandler {
const upsToDelete = config.upsDevices[upsIndex];
// Get confirmation before deleting
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const confirm = await new Promise<string>((resolve) => {
rl.question(
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
(answer) => {
resolve(answer.toLowerCase());
},
);
});
rl.close();
process.stdin.destroy();
const { prompt, close } = await helpers.createPrompt();
const confirm = (await prompt(
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
)).toLowerCase();
close();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
@@ -509,7 +466,7 @@ export class UpsHandler {
* Display the configuration for testing
* @param config Current configuration or individual UPS configuration
*/
private displayTestConfig(config: any): void {
private displayTestConfig(config: IUpsConfig | INupstConfig): void {
// Check if this is a UPS device or full configuration
const isUpsConfig = config.snmp;
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
@@ -571,7 +528,7 @@ export class UpsHandler {
* Test connection to the UPS
* @param config Current UPS configuration or legacy config
*/
private async testConnection(config: any): Promise<void> {
private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> {
const upsId = config.id || 'default';
const upsName = config.name || 'Default UPS';
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
@@ -610,7 +567,7 @@ export class UpsHandler {
* @param status UPS status
* @param thresholds Threshold configuration
*/
private analyzeThresholds(status: any, thresholds: any): void {
private analyzeThresholds(status: ISnmpUpsStatus, thresholds: IThresholds): void {
const boxWidth = 45;
logger.logBoxTitle('Threshold Analysis', boxWidth);
@@ -649,7 +606,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherSnmpSettings(
snmpConfig: any,
snmpConfig: Partial<ISnmpConfig>,
prompt: (question: string) => Promise<string>,
): Promise<void> {
// SNMP IP Address
@@ -693,7 +650,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherSnmpV3Settings(
snmpConfig: any,
snmpConfig: Partial<ISnmpConfig>,
prompt: (question: string) => Promise<string>,
): Promise<void> {
logger.log('');
@@ -771,7 +728,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherAuthenticationSettings(
snmpConfig: any,
snmpConfig: Partial<ISnmpConfig>,
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Authentication protocol
@@ -798,7 +755,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherPrivacySettings(
snmpConfig: any,
snmpConfig: Partial<ISnmpConfig>,
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Privacy protocol
@@ -823,7 +780,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherUpsModelSettings(
snmpConfig: any,
snmpConfig: Partial<ISnmpConfig>,
prompt: (question: string) => Promise<string>,
): Promise<void> {
logger.log('');
@@ -888,7 +845,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async gatherActionSettings(
actions: any[],
actions: IActionConfig[],
prompt: (question: string) => Promise<string>,
): Promise<void> {
logger.log('');
@@ -915,7 +872,7 @@ export class UpsHandler {
const typeInput = await prompt('Select action type [1]: ');
const typeValue = parseInt(typeInput, 10) || 1;
const action: any = {};
const action: Partial<IActionConfig> = {};
if (typeValue === 1) {
// Shutdown action
@@ -1014,8 +971,8 @@ export class UpsHandler {
};
}
actions.push(action);
logger.success(`${action.type.charAt(0).toUpperCase() + action.type.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
actions.push(action as IActionConfig);
logger.success(`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
const more = await prompt('Add another action? (y/N): ');
addMore = more.toLowerCase() === 'y';
@@ -1031,7 +988,7 @@ export class UpsHandler {
* Display UPS configuration summary
* @param ups UPS configuration
*/
private displayUpsConfigSummary(ups: any): void {
private displayUpsConfigSummary(ups: IUpsConfig): void {
const boxWidth = 45;
logger.log('');
logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth);
@@ -1055,7 +1012,7 @@ export class UpsHandler {
* @param prompt Function to prompt for user input
*/
private async optionallyTestConnection(
snmpConfig: any,
snmpConfig: ISnmpConfig,
prompt: (question: string) => Promise<string>,
): Promise<void> {
const testConnection = await prompt(