- Add process.stdin.destroy() after rl.close() in all interactive commands to properly release stdin and allow process to exit cleanly - Replace raw console.log with logger methods throughout CLI handlers - Convert manual box drawing to logger.logBox() in daemon.ts - Standardize menu formatting with logger.info() and logger.dim() - Improve migration output to only show when migrations actually run Fixes issue where process would not exit after "Setup complete!" message due to stdin keeping the event loop alive.
597 lines
18 KiB
TypeScript
597 lines
18 KiB
TypeScript
import process from 'node:process';
|
|
import { Nupst } from '../nupst.ts';
|
|
import { logger } from '../logger.ts';
|
|
import * as helpers from '../helpers/index.ts';
|
|
import { type IGroupConfig } from '../daemon.ts';
|
|
|
|
/**
|
|
* Class for handling group-related CLI commands
|
|
* Provides interface for managing UPS groups
|
|
*/
|
|
export class GroupHandler {
|
|
private readonly nupst: Nupst;
|
|
|
|
/**
|
|
* Create a new Group handler
|
|
* @param nupst Reference to the main Nupst instance
|
|
*/
|
|
constructor(nupst: Nupst) {
|
|
this.nupst = nupst;
|
|
}
|
|
|
|
/**
|
|
* List all UPS groups
|
|
*/
|
|
public async list(): Promise<void> {
|
|
try {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
|
logger.logBoxLine('No configuration found.');
|
|
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Check if multi-UPS config
|
|
if (!config.groups || !Array.isArray(config.groups)) {
|
|
// Legacy or missing groups configuration
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('UPS Groups', boxWidth);
|
|
logger.logBoxLine('No groups configured.');
|
|
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Display group list
|
|
const boxWidth = 60;
|
|
logger.logBoxTitle('UPS Groups', boxWidth);
|
|
|
|
if (config.groups.length === 0) {
|
|
logger.logBoxLine('No UPS groups configured.');
|
|
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
|
} else {
|
|
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
|
|
logger.logBoxLine('');
|
|
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
|
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
|
|
|
for (const group of config.groups) {
|
|
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
|
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
|
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
|
|
|
// Count UPS devices in this group
|
|
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
|
const upsCount = upsInGroup.length;
|
|
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
|
|
|
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
|
}
|
|
}
|
|
|
|
logger.logBoxEnd();
|
|
} catch (error) {
|
|
logger.error(
|
|
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new UPS group
|
|
*/
|
|
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 {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
logger.error(
|
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Initialize groups array if not exists
|
|
if (!config.groups) {
|
|
config.groups = [];
|
|
}
|
|
|
|
// Check if upsDevices is initialized
|
|
if (!config.upsDevices) {
|
|
config.upsDevices = [];
|
|
}
|
|
|
|
logger.log('\nNUPST Add Group');
|
|
logger.log('==============\n');
|
|
logger.log('This will guide you through creating a new UPS group.\n');
|
|
|
|
// Generate a new unique group ID
|
|
const groupId = helpers.shortId();
|
|
|
|
// Get group name
|
|
const name = await prompt('Group Name: ');
|
|
|
|
// Get group mode
|
|
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
|
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
|
|
|
// Get optional description
|
|
const description = await prompt('Group Description (optional): ');
|
|
|
|
// Create the new group
|
|
const newGroup: IGroupConfig = {
|
|
id: groupId,
|
|
name: name || `Group-${groupId}`,
|
|
mode,
|
|
description: description || undefined,
|
|
};
|
|
|
|
// Add the group to the configuration
|
|
config.groups.push(newGroup);
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
|
|
// Display summary
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('Group Created', boxWidth);
|
|
logger.logBoxLine(`ID: ${newGroup.id}`);
|
|
logger.logBoxLine(`Name: ${newGroup.name}`);
|
|
logger.logBoxLine(`Mode: ${newGroup.mode}`);
|
|
if (newGroup.description) {
|
|
logger.logBoxLine(`Description: ${newGroup.description}`);
|
|
}
|
|
logger.logBoxEnd();
|
|
|
|
// Check if there are UPS devices to assign to this group
|
|
if (config.upsDevices.length > 0) {
|
|
const assignUps = await prompt(
|
|
'Would you like to assign UPS devices to this group now? (y/N): ',
|
|
);
|
|
if (assignUps.toLowerCase() === 'y') {
|
|
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
|
|
|
// Save again after assigning UPS devices
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
}
|
|
}
|
|
|
|
// Check if service is running and restart it if needed
|
|
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)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Edit an existing UPS group
|
|
* @param groupId ID of the group to edit
|
|
*/
|
|
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 {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
logger.error(
|
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Check if groups are initialized
|
|
if (!config.groups || !Array.isArray(config.groups)) {
|
|
logger.error(
|
|
'No groups configured. Please run "nupst group add" first to create a group.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Find the group to edit
|
|
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
|
if (groupIndex === -1) {
|
|
logger.error(`Group with ID "${groupId}" not found.`);
|
|
return;
|
|
}
|
|
|
|
const group = config.groups[groupIndex];
|
|
|
|
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
|
logger.log('==============================================\n');
|
|
|
|
// Edit group name
|
|
const newName = await prompt(`Group Name [${group.name}]: `);
|
|
if (newName.trim()) {
|
|
group.name = newName;
|
|
}
|
|
|
|
// Edit group mode
|
|
const currentMode = group.mode || 'redundant';
|
|
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
|
if (modeInput.trim()) {
|
|
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
|
}
|
|
|
|
// Edit description
|
|
const currentDesc = group.description || '';
|
|
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
|
if (newDesc.trim() || newDesc === '') {
|
|
group.description = newDesc.trim() || undefined;
|
|
}
|
|
|
|
// Update the group in the configuration
|
|
config.groups[groupIndex] = group;
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
|
|
// Display summary
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('Group Updated', boxWidth);
|
|
logger.logBoxLine(`ID: ${group.id}`);
|
|
logger.logBoxLine(`Name: ${group.name}`);
|
|
logger.logBoxLine(`Mode: ${group.mode}`);
|
|
if (group.description) {
|
|
logger.logBoxLine(`Description: ${group.description}`);
|
|
}
|
|
logger.logBoxEnd();
|
|
|
|
// Edit UPS assignments if requested
|
|
const editAssignments = await prompt(
|
|
'Would you like to edit UPS assignments for this group? (y/N): ',
|
|
);
|
|
if (editAssignments.toLowerCase() === 'y') {
|
|
await this.assignUpsToGroup(group.id, config, prompt);
|
|
|
|
// Save again after editing assignments
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
}
|
|
|
|
// Check if service is running and restart it if needed
|
|
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)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an existing UPS group
|
|
* @param groupId ID of the group to delete
|
|
*/
|
|
public async remove(groupId: string): Promise<void> {
|
|
try {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
logger.error(
|
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Check if groups are initialized
|
|
if (!config.groups || !Array.isArray(config.groups)) {
|
|
logger.error('No groups configured.');
|
|
return;
|
|
}
|
|
|
|
// Find the group to delete
|
|
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
|
if (groupIndex === -1) {
|
|
logger.error(`Group with ID "${groupId}" not found.`);
|
|
return;
|
|
}
|
|
|
|
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();
|
|
|
|
if (confirm !== 'y' && confirm !== 'yes') {
|
|
logger.log('Deletion cancelled.');
|
|
return;
|
|
}
|
|
|
|
// Remove this group from all UPS device group assignments
|
|
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
|
for (const ups of config.upsDevices) {
|
|
const groupIndex = ups.groups.indexOf(groupId);
|
|
if (groupIndex !== -1) {
|
|
ups.groups.splice(groupIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the group from the array
|
|
config.groups.splice(groupIndex, 1);
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
|
|
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
|
|
|
// Check if service is running and restart it if needed
|
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
|
} catch (error) {
|
|
logger.error(
|
|
`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign UPS devices to groups
|
|
* @param ups UPS configuration to update
|
|
* @param groups Available groups
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
public async assignUpsToGroups(
|
|
ups: any,
|
|
groups: any[],
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
// Initialize groups array if it doesn't exist
|
|
if (!ups.groups) {
|
|
ups.groups = [];
|
|
}
|
|
|
|
// Show current group assignments
|
|
logger.log('\nCurrent Group Assignments:');
|
|
if (ups.groups && ups.groups.length > 0) {
|
|
for (const groupId of ups.groups) {
|
|
const group = groups.find((g) => g.id === groupId);
|
|
if (group) {
|
|
logger.log(`- ${group.name} (${group.id})`);
|
|
} else {
|
|
logger.log(`- Unknown group (${groupId})`);
|
|
}
|
|
}
|
|
} else {
|
|
logger.log('- None');
|
|
}
|
|
|
|
// Show available groups
|
|
logger.log('\nAvailable Groups:');
|
|
if (groups.length === 0) {
|
|
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < groups.length; i++) {
|
|
const group = groups[i];
|
|
const assigned = ups.groups && ups.groups.includes(group.id);
|
|
logger.log(
|
|
`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`,
|
|
);
|
|
}
|
|
|
|
// Prompt for group selection
|
|
const selection = await prompt(
|
|
'\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
|
);
|
|
|
|
if (selection.toLowerCase() === 'clear') {
|
|
// Clear all group assignments
|
|
ups.groups = [];
|
|
logger.log('All group assignments cleared.');
|
|
return;
|
|
}
|
|
|
|
if (!selection.trim()) {
|
|
// No change if empty input
|
|
return;
|
|
}
|
|
|
|
// Process selections
|
|
const selections = selection.split(',').map((s) => s.trim());
|
|
|
|
for (const sel of selections) {
|
|
const index = parseInt(sel, 10) - 1;
|
|
if (isNaN(index) || index < 0 || index >= groups.length) {
|
|
logger.error(`Invalid selection: ${sel}`);
|
|
continue;
|
|
}
|
|
|
|
const group = groups[index];
|
|
|
|
// Initialize groups array if needed (should already be done above)
|
|
if (!ups.groups) {
|
|
ups.groups = [];
|
|
}
|
|
|
|
// Toggle assignment
|
|
const groupIndex = ups.groups.indexOf(group.id);
|
|
if (groupIndex === -1) {
|
|
// Add to group
|
|
ups.groups.push(group.id);
|
|
logger.log(`Added to group: ${group.name} (${group.id})`);
|
|
} else {
|
|
// Remove from group
|
|
ups.groups.splice(groupIndex, 1);
|
|
logger.log(`Removed from group: ${group.name} (${group.id})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign UPS devices to a specific group
|
|
* @param groupId Group ID to assign UPS devices to
|
|
* @param config Full configuration
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
public async assignUpsToGroup(
|
|
groupId: string,
|
|
config: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
if (!config.upsDevices || config.upsDevices.length === 0) {
|
|
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
|
return;
|
|
}
|
|
|
|
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
|
if (!group) {
|
|
logger.error(`Group with ID "${groupId}" not found.`);
|
|
return;
|
|
}
|
|
|
|
// Show current assignments
|
|
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
|
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
|
|
ups.groups && ups.groups.includes(groupId)
|
|
);
|
|
if (upsInGroup.length === 0) {
|
|
logger.log('- None');
|
|
} else {
|
|
for (const ups of upsInGroup) {
|
|
logger.log(`- ${ups.name} (${ups.id})`);
|
|
}
|
|
}
|
|
|
|
// Show all UPS devices
|
|
logger.log('\nAvailable UPS devices:');
|
|
for (let i = 0; i < config.upsDevices.length; i++) {
|
|
const ups = config.upsDevices[i];
|
|
const assigned = ups.groups && ups.groups.includes(groupId);
|
|
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
|
}
|
|
|
|
// Prompt for UPS selection
|
|
const selection = await prompt(
|
|
'\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
|
);
|
|
|
|
if (selection.toLowerCase() === 'clear') {
|
|
// Clear all UPS from this group
|
|
for (const ups of config.upsDevices) {
|
|
if (ups.groups) {
|
|
const groupIndex = ups.groups.indexOf(groupId);
|
|
if (groupIndex !== -1) {
|
|
ups.groups.splice(groupIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
logger.log(`All UPS devices removed from group "${group.name}".`);
|
|
return;
|
|
}
|
|
|
|
if (!selection.trim()) {
|
|
// No change if empty input
|
|
return;
|
|
}
|
|
|
|
// Process selections
|
|
const selections = selection.split(',').map((s) => s.trim());
|
|
|
|
for (const sel of selections) {
|
|
const index = parseInt(sel, 10) - 1;
|
|
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
|
logger.error(`Invalid selection: ${sel}`);
|
|
continue;
|
|
}
|
|
|
|
const ups = config.upsDevices[index];
|
|
|
|
// Initialize groups array if needed
|
|
if (!ups.groups) {
|
|
ups.groups = [];
|
|
}
|
|
|
|
// Toggle assignment
|
|
const groupIndex = ups.groups.indexOf(groupId);
|
|
if (groupIndex === -1) {
|
|
// Add to group
|
|
ups.groups.push(groupId);
|
|
logger.log(`Added "${ups.name}" to group "${group.name}"`);
|
|
} else {
|
|
// Remove from group
|
|
ups.groups.splice(groupIndex, 1);
|
|
logger.log(`Removed "${ups.name}" from group "${group.name}"`);
|
|
}
|
|
}
|
|
}
|
|
}
|