Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
f8269a1cb7 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "4.0.1",
|
"version": "4.0.2",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
233
test/showcase.ts
Normal file
233
test/showcase.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Showcase test for NUPST CLI outputs
|
||||||
|
* Demonstrates all the beautiful colored output features
|
||||||
|
*
|
||||||
|
* Run with: deno run --allow-all test/showcase.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger, type ITableColumn } from '../ts/logger.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.highlight('NUPST CLI OUTPUT SHOWCASE');
|
||||||
|
logger.dim('Demonstrating beautiful, colored terminal output');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 1. Basic Logging Methods ===
|
||||||
|
logger.logBoxTitle('Basic Logging Methods', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.log('Normal log message (default color)');
|
||||||
|
logger.success('Success message with ✓ symbol');
|
||||||
|
logger.error('Error message with ✗ symbol');
|
||||||
|
logger.warn('Warning message with ⚠ symbol');
|
||||||
|
logger.info('Info message with ℹ symbol');
|
||||||
|
logger.dim('Dim/secondary text for less important info');
|
||||||
|
logger.highlight('Highlighted/bold text for emphasis');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 2. Colored Boxes ===
|
||||||
|
logger.logBoxTitle('Colored Box Styles', 60);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Boxes can be styled with different colors:');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Success Box (Green)', [
|
||||||
|
'Used for successful operations',
|
||||||
|
'Installation complete, service started, etc.',
|
||||||
|
], 60, 'success');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Error Box (Red)', [
|
||||||
|
'Used for critical errors and failures',
|
||||||
|
'Configuration errors, connection failures, etc.',
|
||||||
|
], 60, 'error');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Warning Box (Yellow)', [
|
||||||
|
'Used for warnings and deprecations',
|
||||||
|
'Old command format, missing config, etc.',
|
||||||
|
], 60, 'warning');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Info Box (Cyan)', [
|
||||||
|
'Used for informational messages',
|
||||||
|
'Version info, update available, etc.',
|
||||||
|
], 60, 'info');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 3. Status Symbols ===
|
||||||
|
logger.logBoxTitle('Status Symbols', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.running} Service Running`);
|
||||||
|
logger.logBoxLine(`${symbols.stopped} Service Stopped`);
|
||||||
|
logger.logBoxLine(`${symbols.starting} Service Starting`);
|
||||||
|
logger.logBoxLine(`${symbols.unknown} Status Unknown`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.success} Operation Successful`);
|
||||||
|
logger.logBoxLine(`${symbols.error} Operation Failed`);
|
||||||
|
logger.logBoxLine(`${symbols.warning} Warning Condition`);
|
||||||
|
logger.logBoxLine(`${symbols.info} Information`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 4. Battery Level Colors ===
|
||||||
|
logger.logBoxTitle('Battery Level Color Coding', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Battery levels are color-coded:');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(85)('85%')} - Good (green, ≥60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(45)('45%')} - Medium (yellow, 30-60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(15)('15%')} - Critical (red, <30%)`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 5. Power Status Formatting ===
|
||||||
|
logger.logBoxTitle('Power Status Formatting', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('online')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('onBattery')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('unknown')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 6. Table Formatting ===
|
||||||
|
const upsColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Host', key: 'host' },
|
||||||
|
{ header: 'Status', key: 'status', color: (v) => {
|
||||||
|
if (v.includes('Online')) return theme.success(v);
|
||||||
|
if (v.includes('Battery')) return theme.warning(v);
|
||||||
|
return theme.dim(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
||||||
|
const pct = parseInt(v);
|
||||||
|
return getBatteryColor(pct)(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const upsData = [
|
||||||
|
{
|
||||||
|
id: 'ups-1',
|
||||||
|
name: 'Main UPS',
|
||||||
|
host: '192.168.1.10',
|
||||||
|
status: 'Online',
|
||||||
|
battery: '95%',
|
||||||
|
runtime: '45 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-2',
|
||||||
|
name: 'Backup UPS',
|
||||||
|
host: '192.168.1.11',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '42%',
|
||||||
|
runtime: '12 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-3',
|
||||||
|
name: 'Critical UPS',
|
||||||
|
host: '192.168.1.12',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '18%',
|
||||||
|
runtime: '5 min',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(upsColumns, upsData, 'UPS Devices');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 7. Group Table ===
|
||||||
|
const groupColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Mode', key: 'mode' },
|
||||||
|
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupData = [
|
||||||
|
{ id: 'dc-1', name: 'Data Center 1', mode: 'redundant', count: '3' },
|
||||||
|
{ id: 'office', name: 'Office Servers', mode: 'nonRedundant', count: '2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(groupColumns, groupData, 'UPS Groups');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 8. Service Status Example ===
|
||||||
|
logger.logBoxTitle('Service Status', 70, 'success');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${symbols.running} ${theme.statusActive('Active (Running)')}`);
|
||||||
|
logger.logBoxLine(`Enabled: ${symbols.success} ${theme.success('Yes')}`);
|
||||||
|
logger.logBoxLine(`Uptime: 2 days, 5 hours, 23 minutes`);
|
||||||
|
logger.logBoxLine(`PID: ${theme.dim('12345')}`);
|
||||||
|
logger.logBoxLine(`Memory: ${theme.dim('45.2 MB')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 9. Configuration Example ===
|
||||||
|
logger.logBoxTitle('Configuration', 70);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`UPS Devices: ${theme.highlight('3')}`);
|
||||||
|
logger.logBoxLine(`Groups: ${theme.highlight('2')}`);
|
||||||
|
logger.logBoxLine(`Check Interval: ${theme.dim('30 seconds')}`);
|
||||||
|
logger.logBoxLine(`Config File: ${theme.path('/etc/nupst/config.json')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 10. Update Available Example ===
|
||||||
|
logger.logBoxTitle('Update Available', 70, 'warning');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Current Version: ${theme.dim('4.0.1')}`);
|
||||||
|
logger.logBoxLine(`Latest Version: ${theme.highlight('4.0.2')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Run ${theme.command('sudo nupst update')} to update`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 11. Error Example ===
|
||||||
|
logger.logBoxTitle('Error Example', 70, 'error');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.error} Failed to connect to UPS at 192.168.1.10`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Possible causes:');
|
||||||
|
logger.logBoxLine(` ${theme.dim('• UPS is offline or unreachable')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Incorrect SNMP community string')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Firewall blocking port 161')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Try: ${theme.command('nupst ups test --debug')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === Final Summary ===
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.success('CLI Output Showcase Complete!');
|
||||||
|
logger.dim('All color and formatting features demonstrated');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
114
ts/cli.ts
114
ts/cli.ts
@@ -1,6 +1,7 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from './nupst.ts';
|
import { Nupst } from './nupst.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling CLI commands
|
* Class for handling CLI commands
|
||||||
@@ -475,58 +476,83 @@ export class NupstCli {
|
|||||||
* Display help message
|
* Display help message
|
||||||
*/
|
*/
|
||||||
private showHelp(): void {
|
private showHelp(): void {
|
||||||
logger.log(`
|
console.log('');
|
||||||
NUPST - UPS Shutdown Tool
|
logger.highlight('NUPST - UPS Shutdown Tool');
|
||||||
|
logger.dim('Deno-powered UPS monitoring and shutdown automation');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Usage:
|
// Usage section
|
||||||
nupst <command> [options]
|
logger.log(theme.info('Usage:'));
|
||||||
|
logger.log(` ${theme.command('nupst')} ${theme.dim('<command> [options]')}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Commands:
|
// Main commands section
|
||||||
service <subcommand> - Manage systemd service
|
logger.log(theme.info('Commands:'));
|
||||||
ups <subcommand> - Manage UPS devices
|
this.printCommand('service <subcommand>', 'Manage systemd service');
|
||||||
group <subcommand> - Manage UPS groups
|
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||||
config [show] - Display current configuration
|
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||||
update - Update NUPST from repository (requires root)
|
this.printCommand('config [show]', 'Display current configuration');
|
||||||
uninstall - Completely remove NUPST from system (requires root)
|
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||||
help, --help, -h - Show this help message
|
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||||
--version, -v - Show version information
|
this.printCommand('help, --help, -h', 'Show this help message');
|
||||||
|
this.printCommand('--version, -v', 'Show version information');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Service Subcommands:
|
// Service subcommands
|
||||||
nupst service enable - Install and enable systemd service (requires root)
|
logger.log(theme.info('Service Subcommands:'));
|
||||||
nupst service disable - Stop and disable systemd service (requires root)
|
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
|
||||||
nupst service start - Start the systemd service
|
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
|
||||||
nupst service stop - Stop the systemd service
|
this.printCommand('nupst service start', 'Start the systemd service');
|
||||||
nupst service restart - Restart the systemd service
|
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||||
nupst service status - Show service and UPS status
|
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||||
nupst service logs - Show service logs in real-time
|
this.printCommand('nupst service status', 'Show service and UPS status');
|
||||||
nupst service start-daemon - Start daemon process directly
|
this.printCommand('nupst service logs', 'Show service logs in real-time');
|
||||||
|
this.printCommand('nupst service start-daemon', 'Start daemon process directly');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
UPS Subcommands:
|
// UPS subcommands
|
||||||
nupst ups add - Add a new UPS device
|
logger.log(theme.info('UPS Subcommands:'));
|
||||||
nupst ups edit [id] - Edit a UPS device (default if no ID)
|
this.printCommand('nupst ups add', 'Add a new UPS device');
|
||||||
nupst ups remove <id> - Remove a UPS device by ID
|
this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)');
|
||||||
nupst ups list (or ls) - List all configured UPS devices
|
this.printCommand('nupst ups remove <id>', 'Remove a UPS device by ID');
|
||||||
nupst ups test - Test UPS connections
|
this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices');
|
||||||
|
this.printCommand('nupst ups test', 'Test UPS connections');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Group Subcommands:
|
// Group subcommands
|
||||||
nupst group add - Add a new UPS group
|
logger.log(theme.info('Group Subcommands:'));
|
||||||
nupst group edit <id> - Edit an existing UPS group
|
this.printCommand('nupst group add', 'Add a new UPS group');
|
||||||
nupst group remove <id> - Remove a UPS group by ID
|
this.printCommand('nupst group edit <id>', 'Edit an existing UPS group');
|
||||||
nupst group list (or ls) - List all UPS groups
|
this.printCommand('nupst group remove <id>', 'Remove a UPS group by ID');
|
||||||
|
this.printCommand('nupst group list (or ls)', 'List all UPS groups');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Options:
|
// Options
|
||||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
logger.log(theme.info('Options:'));
|
||||||
(Example: nupst ups test --debug)
|
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||||
|
logger.dim(' (Example: nupst ups test --debug)');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Examples:
|
// Examples
|
||||||
nupst service enable - Install and start the service
|
logger.log(theme.info('Examples:'));
|
||||||
nupst ups add - Add a new UPS interactively
|
logger.dim(' nupst service enable # Install and start the service');
|
||||||
nupst group list - Show all configured groups
|
logger.dim(' nupst ups add # Add a new UPS interactively');
|
||||||
nupst config - Display current configuration
|
logger.dim(' nupst group list # Show all configured groups');
|
||||||
|
logger.dim(' nupst config # Display current configuration');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Note: Old command format (e.g., 'nupst add') still works but is deprecated.
|
// Note about deprecated commands
|
||||||
Use the new format (e.g., 'nupst ups add') going forward.
|
logger.warn('Note: Old command format (e.g., \'nupst add\') still works but is deprecated.');
|
||||||
`);
|
logger.dim(' Use the new format (e.g., \'nupst ups add\') going forward.');
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to print a command with description
|
||||||
|
*/
|
||||||
|
private printCommand(command: string, description: string, extra?: string): void {
|
||||||
|
const paddedCommand = command.padEnd(30);
|
||||||
|
logger.log(` ${theme.command(paddedCommand)} ${description}${extra ? ' ' + extra : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
89
ts/colors.ts
Normal file
89
ts/colors.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Color theme and styling utilities for NUPST CLI
|
||||||
|
* Uses Deno standard library colors module
|
||||||
|
*/
|
||||||
|
import * as colors from '@std/fmt/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color theme for consistent CLI styling
|
||||||
|
*/
|
||||||
|
export const theme = {
|
||||||
|
// Message types
|
||||||
|
error: colors.red,
|
||||||
|
warning: colors.yellow,
|
||||||
|
success: colors.green,
|
||||||
|
info: colors.cyan,
|
||||||
|
dim: colors.dim,
|
||||||
|
highlight: colors.bold,
|
||||||
|
bright: colors.bright,
|
||||||
|
|
||||||
|
// Status indicators
|
||||||
|
statusActive: (text: string) => colors.green(colors.bold(text)),
|
||||||
|
statusInactive: (text: string) => colors.red(text),
|
||||||
|
statusWarning: (text: string) => colors.yellow(text),
|
||||||
|
statusUnknown: (text: string) => colors.dim(text),
|
||||||
|
|
||||||
|
// Battery level colors
|
||||||
|
batteryGood: colors.green, // > 60%
|
||||||
|
batteryMedium: colors.yellow, // 30-60%
|
||||||
|
batteryCritical: colors.red, // < 30%
|
||||||
|
|
||||||
|
// Box borders
|
||||||
|
borderSuccess: colors.green,
|
||||||
|
borderError: colors.red,
|
||||||
|
borderWarning: colors.yellow,
|
||||||
|
borderInfo: colors.cyan,
|
||||||
|
borderDefault: (text: string) => text, // No color
|
||||||
|
|
||||||
|
// Command/code highlighting
|
||||||
|
command: colors.cyan,
|
||||||
|
code: colors.dim,
|
||||||
|
path: colors.blue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status symbols with colors
|
||||||
|
*/
|
||||||
|
export const symbols = {
|
||||||
|
success: colors.green('✓'),
|
||||||
|
error: colors.red('✗'),
|
||||||
|
warning: colors.yellow('⚠'),
|
||||||
|
info: colors.cyan('ℹ'),
|
||||||
|
running: colors.green('●'),
|
||||||
|
stopped: colors.red('○'),
|
||||||
|
starting: colors.yellow('◐'),
|
||||||
|
unknown: colors.dim('◯'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for battery level
|
||||||
|
*/
|
||||||
|
export function getBatteryColor(percentage: number): (text: string) => string {
|
||||||
|
if (percentage >= 60) return theme.batteryGood;
|
||||||
|
if (percentage >= 30) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for runtime remaining
|
||||||
|
*/
|
||||||
|
export function getRuntimeColor(minutes: number): (text: string) => string {
|
||||||
|
if (minutes >= 20) return theme.batteryGood;
|
||||||
|
if (minutes >= 10) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format UPS power status with color
|
||||||
|
*/
|
||||||
|
export function formatPowerStatus(status: 'online' | 'onBattery' | 'unknown'): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'online':
|
||||||
|
return theme.success('Online');
|
||||||
|
case 'onBattery':
|
||||||
|
return theme.warning('On Battery');
|
||||||
|
case 'unknown':
|
||||||
|
default:
|
||||||
|
return theme.dim('Unknown');
|
||||||
|
}
|
||||||
|
}
|
132
ts/daemon.ts
132
ts/daemon.ts
@@ -353,8 +353,9 @@ export class NupstDaemon {
|
|||||||
logger.log('Starting UPS monitoring...');
|
logger.log('Starting UPS monitoring...');
|
||||||
|
|
||||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
logger.warn('No UPS devices found in configuration. Daemon will remain idle...');
|
||||||
this.isRunning = false;
|
// Don't exit - enter idle monitoring mode instead
|
||||||
|
await this.idleMonitoring();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,6 +891,133 @@ export class NupstDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idle monitoring loop when no UPS devices are configured
|
||||||
|
* Watches for config changes and reloads when detected
|
||||||
|
*/
|
||||||
|
private async idleMonitoring(): Promise<void> {
|
||||||
|
const IDLE_CHECK_INTERVAL = 60000; // Check every 60 seconds
|
||||||
|
let lastConfigCheck = Date.now();
|
||||||
|
const CONFIG_CHECK_INTERVAL = 60000; // Check config every minute
|
||||||
|
|
||||||
|
logger.log('Entering idle monitoring mode...');
|
||||||
|
logger.log('Daemon will check for config changes every 60 seconds');
|
||||||
|
|
||||||
|
// Start file watcher for hot-reload
|
||||||
|
this.watchConfigFile();
|
||||||
|
|
||||||
|
while (this.isRunning) {
|
||||||
|
try {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
// Periodically check if config has been updated
|
||||||
|
if (currentTime - lastConfigCheck >= CONFIG_CHECK_INTERVAL) {
|
||||||
|
try {
|
||||||
|
// Try to load config
|
||||||
|
const newConfig = await this.loadConfig();
|
||||||
|
|
||||||
|
// Check if we now have UPS devices configured
|
||||||
|
if (newConfig.upsDevices && newConfig.upsDevices.length > 0) {
|
||||||
|
logger.success('Configuration updated! UPS devices found. Starting monitoring...');
|
||||||
|
this.initializeUpsStatus();
|
||||||
|
// Exit idle mode and start monitoring
|
||||||
|
await this.monitor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Config still doesn't exist or invalid, continue waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
lastConfigCheck = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error during idle monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('Idle monitoring stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch config file for changes and reload automatically
|
||||||
|
*/
|
||||||
|
private watchConfigFile(): void {
|
||||||
|
try {
|
||||||
|
// Use Deno's file watcher to monitor config file
|
||||||
|
const configDir = path.dirname(this.CONFIG_PATH);
|
||||||
|
|
||||||
|
// Spawn a background watcher (non-blocking)
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const watcher = Deno.watchFs(configDir);
|
||||||
|
|
||||||
|
logger.log('Config file watcher started');
|
||||||
|
|
||||||
|
for await (const event of watcher) {
|
||||||
|
// Only respond to modify events on the config file
|
||||||
|
if (
|
||||||
|
event.kind === 'modify' &&
|
||||||
|
event.paths.some((p) => p.includes('config.json'))
|
||||||
|
) {
|
||||||
|
logger.info('Config file changed, reloading...');
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop watching if daemon stopped
|
||||||
|
if (!this.isRunning) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Watcher error - not critical, just log it
|
||||||
|
logger.dim(
|
||||||
|
`Config watcher stopped: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't start the watcher, just log and continue
|
||||||
|
// The periodic check will still work
|
||||||
|
logger.dim('Could not start config file watcher, using periodic checks only');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload configuration and restart monitoring if needed
|
||||||
|
*/
|
||||||
|
private async reloadConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const oldDeviceCount = this.config.upsDevices?.length || 0;
|
||||||
|
|
||||||
|
// Load the new configuration
|
||||||
|
await this.loadConfig();
|
||||||
|
const newDeviceCount = this.config.upsDevices?.length || 0;
|
||||||
|
|
||||||
|
if (newDeviceCount > 0 && oldDeviceCount === 0) {
|
||||||
|
logger.success(`Configuration reloaded! Found ${newDeviceCount} UPS device(s)`);
|
||||||
|
logger.info('Monitoring will start automatically...');
|
||||||
|
} else if (newDeviceCount !== oldDeviceCount) {
|
||||||
|
logger.success(
|
||||||
|
`Configuration reloaded! UPS devices: ${oldDeviceCount} → ${newDeviceCount}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reinitialize UPS status tracking
|
||||||
|
this.initializeUpsStatus();
|
||||||
|
} else {
|
||||||
|
logger.success('Configuration reloaded successfully');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sleep for the specified milliseconds
|
* Sleep for the specified milliseconds
|
||||||
*/
|
*/
|
||||||
|
220
ts/logger.ts
220
ts/logger.ts
@@ -1,9 +1,38 @@
|
|||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column alignment options
|
||||||
|
*/
|
||||||
|
export type TColumnAlign = 'left' | 'right' | 'center';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column definition
|
||||||
|
*/
|
||||||
|
export interface ITableColumn {
|
||||||
|
/** Column header text */
|
||||||
|
header: string;
|
||||||
|
/** Column key in data object */
|
||||||
|
key: string;
|
||||||
|
/** Column alignment (default: left) */
|
||||||
|
align?: TColumnAlign;
|
||||||
|
/** Column width (auto-calculated if not specified) */
|
||||||
|
width?: number;
|
||||||
|
/** Color function to apply to cell values */
|
||||||
|
color?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Box style types with colors
|
||||||
|
*/
|
||||||
|
export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple logger class that provides consistent formatting for log messages
|
* A simple logger class that provides consistent formatting for log messages
|
||||||
* including support for logboxes with title, lines, and closing
|
* including support for logboxes with title, lines, and closing
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private currentBoxWidth: number | null = null;
|
private currentBoxWidth: number | null = null;
|
||||||
|
private currentBoxStyle: TBoxStyle = 'default';
|
||||||
private static instance: Logger;
|
private static instance: Logger;
|
||||||
|
|
||||||
/** Default width to use when no width is specified */
|
/** Default width to use when no width is specified */
|
||||||
@@ -36,36 +65,83 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an error message
|
* Log an error message (red with ✗ symbol)
|
||||||
* @param message Error message to log
|
* @param message Error message to log
|
||||||
*/
|
*/
|
||||||
public error(message: string): void {
|
public error(message: string): void {
|
||||||
console.error(message);
|
console.error(`${symbols.error} ${theme.error(message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a warning message with a warning emoji
|
* Log a warning message (yellow with ⚠ symbol)
|
||||||
* @param message Warning message to log
|
* @param message Warning message to log
|
||||||
*/
|
*/
|
||||||
public warn(message: string): void {
|
public warn(message: string): void {
|
||||||
console.warn(`⚠️ ${message}`);
|
console.warn(`${symbols.warning} ${theme.warning(message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a success message with a checkmark
|
* Log a success message (green with ✓ symbol)
|
||||||
* @param message Success message to log
|
* @param message Success message to log
|
||||||
*/
|
*/
|
||||||
public success(message: string): void {
|
public success(message: string): void {
|
||||||
console.log(`✓ ${message}`);
|
console.log(`${symbols.success} ${theme.success(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an info message (cyan with ℹ symbol)
|
||||||
|
* @param message Info message to log
|
||||||
|
*/
|
||||||
|
public info(message: string): void {
|
||||||
|
console.log(`${symbols.info} ${theme.info(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a dim/secondary message
|
||||||
|
* @param message Message to log in dim style
|
||||||
|
*/
|
||||||
|
public dim(message: string): void {
|
||||||
|
console.log(theme.dim(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a highlighted/bold message
|
||||||
|
* @param message Message to highlight
|
||||||
|
*/
|
||||||
|
public highlight(message: string): void {
|
||||||
|
console.log(theme.highlight(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color function for box based on style
|
||||||
|
*/
|
||||||
|
private getBoxColor(style: TBoxStyle): (text: string) => string {
|
||||||
|
switch (style) {
|
||||||
|
case 'success':
|
||||||
|
return theme.borderSuccess;
|
||||||
|
case 'error':
|
||||||
|
return theme.borderError;
|
||||||
|
case 'warning':
|
||||||
|
return theme.borderWarning;
|
||||||
|
case 'info':
|
||||||
|
return theme.borderInfo;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.borderDefault;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a logbox title and set the current box width
|
* Log a logbox title and set the current box width
|
||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring (default, success, error, warning, info)
|
||||||
*/
|
*/
|
||||||
public logBoxTitle(title: string, width?: number): void {
|
public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void {
|
||||||
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||||
|
this.currentBoxStyle = style || 'default';
|
||||||
|
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the title line with appropriate padding
|
// Create the title line with appropriate padding
|
||||||
const paddedTitle = ` ${title} `;
|
const paddedTitle = ` ${title} `;
|
||||||
@@ -74,7 +150,7 @@ export class Logger {
|
|||||||
// Title line: ┌─ Title ───┐
|
// Title line: ┌─ Title ───┐
|
||||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||||
|
|
||||||
console.log(titleLine);
|
console.log(colorFn(titleLine));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,17 +165,21 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Calculate the available space for content
|
// Calculate the available space for content (use visible length)
|
||||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||||
|
const visibleLen = this.visibleLength(content);
|
||||||
|
|
||||||
if (content.length <= availableSpace - 1) {
|
if (visibleLen <= availableSpace - 1) {
|
||||||
// If content fits with at least one space for the right border stripe
|
// If content fits with at least one space for the right border stripe
|
||||||
const padding = availableSpace - content.length - 1;
|
const padding = availableSpace - visibleLen - 1;
|
||||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||||
|
console.log(colorFn(line));
|
||||||
} else {
|
} else {
|
||||||
// Content is too long, let it flow out of boundaries.
|
// Content is too long, let it flow out of boundaries.
|
||||||
console.log(`│ ${content}`);
|
const line = `│ ${content}`;
|
||||||
|
console.log(colorFn(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +189,15 @@ export class Logger {
|
|||||||
*/
|
*/
|
||||||
public logBoxEnd(width?: number): void {
|
public logBoxEnd(width?: number): void {
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the bottom border: └────────┘
|
// Create the bottom border: └────────┘
|
||||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`;
|
||||||
|
console.log(colorFn(bottomLine));
|
||||||
|
|
||||||
// Reset the current box width
|
// Reset the current box width and style
|
||||||
this.currentBoxWidth = null;
|
this.currentBoxWidth = null;
|
||||||
|
this.currentBoxStyle = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,9 +205,10 @@ export class Logger {
|
|||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param lines Array of content lines
|
* @param lines Array of content lines
|
||||||
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring
|
||||||
*/
|
*/
|
||||||
public logBox(title: string, lines: string[], width?: number): void {
|
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
this.logBoxLine(line);
|
this.logBoxLine(line);
|
||||||
@@ -141,6 +225,108 @@ export class Logger {
|
|||||||
public logDivider(width?: number, character: string = '─'): void {
|
public logDivider(width?: number, character: string = '─'): void {
|
||||||
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI color codes from string for accurate length calculation
|
||||||
|
*/
|
||||||
|
private stripAnsi(text: string): string {
|
||||||
|
// Remove ANSI escape codes
|
||||||
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get visible length of string (excluding ANSI codes)
|
||||||
|
*/
|
||||||
|
private visibleLength(text: string): number {
|
||||||
|
return this.stripAnsi(text).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align text within a column (handles ANSI color codes correctly)
|
||||||
|
*/
|
||||||
|
private alignText(text: string, width: number, align: TColumnAlign = 'left'): string {
|
||||||
|
const visibleLen = this.visibleLength(text);
|
||||||
|
|
||||||
|
if (visibleLen >= width) {
|
||||||
|
// Text is too long, truncate the visible part
|
||||||
|
const stripped = this.stripAnsi(text);
|
||||||
|
return stripped.substring(0, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = width - visibleLen;
|
||||||
|
|
||||||
|
switch (align) {
|
||||||
|
case 'right':
|
||||||
|
return ' '.repeat(padding) + text;
|
||||||
|
case 'center': {
|
||||||
|
const leftPad = Math.floor(padding / 2);
|
||||||
|
const rightPad = padding - leftPad;
|
||||||
|
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
||||||
|
}
|
||||||
|
case 'left':
|
||||||
|
default:
|
||||||
|
return text + ' '.repeat(padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a formatted table
|
||||||
|
* @param columns Column definitions
|
||||||
|
* @param rows Array of data objects
|
||||||
|
* @param title Optional table title
|
||||||
|
*/
|
||||||
|
public logTable(columns: ITableColumn[], rows: Record<string, string>[], title?: string): void {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.dim('No data to display');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
const columnWidths = columns.map((col) => {
|
||||||
|
if (col.width) return col.width;
|
||||||
|
|
||||||
|
// Auto-calculate width based on header and data (use visible length)
|
||||||
|
let maxWidth = this.visibleLength(col.header);
|
||||||
|
for (const row of rows) {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
maxWidth = Math.max(maxWidth, this.visibleLength(value));
|
||||||
|
}
|
||||||
|
return maxWidth;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total table width
|
||||||
|
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1;
|
||||||
|
|
||||||
|
// Print title if provided
|
||||||
|
if (title) {
|
||||||
|
this.logBoxTitle(title, totalWidth);
|
||||||
|
} else {
|
||||||
|
// Print top border
|
||||||
|
console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header row
|
||||||
|
const headerCells = columns.map((col, i) =>
|
||||||
|
theme.highlight(this.alignText(col.header, columnWidths[i], col.align))
|
||||||
|
);
|
||||||
|
console.log('│ ' + headerCells.join(' │ ') + ' │');
|
||||||
|
|
||||||
|
// Print separator
|
||||||
|
console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
|
||||||
|
|
||||||
|
// Print data rows
|
||||||
|
for (const row of rows) {
|
||||||
|
const cells = columns.map((col, i) => {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
const aligned = this.alignText(value, columnWidths[i], col.align);
|
||||||
|
return col.color ? col.color(aligned) : aligned;
|
||||||
|
});
|
||||||
|
console.log('│ ' + cells.join(' │ ') + ' │');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print bottom border
|
||||||
|
console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a singleton instance for easy use
|
// Export a singleton instance for easy use
|
||||||
|
Reference in New Issue
Block a user