migration/deno-v4 #1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"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 { Nupst } from './nupst.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { theme, symbols } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for handling CLI commands
|
||||
@@ -475,58 +476,83 @@ export class NupstCli {
|
||||
* Display help message
|
||||
*/
|
||||
private showHelp(): void {
|
||||
logger.log(`
|
||||
NUPST - UPS Shutdown Tool
|
||||
console.log('');
|
||||
logger.highlight('NUPST - UPS Shutdown Tool');
|
||||
logger.dim('Deno-powered UPS monitoring and shutdown automation');
|
||||
console.log('');
|
||||
|
||||
Usage:
|
||||
nupst <command> [options]
|
||||
// Usage section
|
||||
logger.log(theme.info('Usage:'));
|
||||
logger.log(` ${theme.command('nupst')} ${theme.dim('<command> [options]')}`);
|
||||
console.log('');
|
||||
|
||||
Commands:
|
||||
service <subcommand> - Manage systemd service
|
||||
ups <subcommand> - Manage UPS devices
|
||||
group <subcommand> - Manage UPS groups
|
||||
config [show] - Display current configuration
|
||||
update - Update NUPST from repository (requires root)
|
||||
uninstall - Completely remove NUPST from system (requires root)
|
||||
help, --help, -h - Show this help message
|
||||
--version, -v - Show version information
|
||||
// Main commands section
|
||||
logger.log(theme.info('Commands:'));
|
||||
this.printCommand('service <subcommand>', 'Manage systemd service');
|
||||
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||
this.printCommand('config [show]', 'Display current configuration');
|
||||
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||
this.printCommand('help, --help, -h', 'Show this help message');
|
||||
this.printCommand('--version, -v', 'Show version information');
|
||||
console.log('');
|
||||
|
||||
Service Subcommands:
|
||||
nupst service enable - Install and enable systemd service (requires root)
|
||||
nupst service disable - Stop and disable systemd service (requires root)
|
||||
nupst service start - Start the systemd service
|
||||
nupst service stop - Stop the systemd service
|
||||
nupst service restart - Restart the systemd service
|
||||
nupst service status - Show service and UPS status
|
||||
nupst service logs - Show service logs in real-time
|
||||
nupst service start-daemon - Start daemon process directly
|
||||
// Service subcommands
|
||||
logger.log(theme.info('Service Subcommands:'));
|
||||
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
|
||||
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
|
||||
this.printCommand('nupst service start', 'Start the systemd service');
|
||||
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||
this.printCommand('nupst service status', 'Show service and UPS status');
|
||||
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:
|
||||
nupst ups add - Add a new UPS device
|
||||
nupst ups edit [id] - Edit a UPS device (default if no ID)
|
||||
nupst ups remove <id> - Remove a UPS device by ID
|
||||
nupst ups list (or ls) - List all configured UPS devices
|
||||
nupst ups test - Test UPS connections
|
||||
// UPS subcommands
|
||||
logger.log(theme.info('UPS Subcommands:'));
|
||||
this.printCommand('nupst ups add', 'Add a new UPS device');
|
||||
this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)');
|
||||
this.printCommand('nupst ups remove <id>', 'Remove a UPS device by ID');
|
||||
this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices');
|
||||
this.printCommand('nupst ups test', 'Test UPS connections');
|
||||
console.log('');
|
||||
|
||||
Group Subcommands:
|
||||
nupst group add - Add a new UPS group
|
||||
nupst group edit <id> - Edit an existing UPS group
|
||||
nupst group remove <id> - Remove a UPS group by ID
|
||||
nupst group list (or ls) - List all UPS groups
|
||||
// Group subcommands
|
||||
logger.log(theme.info('Group Subcommands:'));
|
||||
this.printCommand('nupst group add', 'Add a new UPS group');
|
||||
this.printCommand('nupst group edit <id>', 'Edit an existing UPS group');
|
||||
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:
|
||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
||||
(Example: nupst ups test --debug)
|
||||
// Options
|
||||
logger.log(theme.info('Options:'));
|
||||
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||
logger.dim(' (Example: nupst ups test --debug)');
|
||||
console.log('');
|
||||
|
||||
Examples:
|
||||
nupst service enable - Install and start the service
|
||||
nupst ups add - Add a new UPS interactively
|
||||
nupst group list - Show all configured groups
|
||||
nupst config - Display current configuration
|
||||
// Examples
|
||||
logger.log(theme.info('Examples:'));
|
||||
logger.dim(' nupst service enable # Install and start the service');
|
||||
logger.dim(' nupst ups add # Add a new UPS interactively');
|
||||
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.
|
||||
Use the new format (e.g., 'nupst ups add') going forward.
|
||||
`);
|
||||
// Note about deprecated commands
|
||||
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...');
|
||||
|
||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
||||
this.isRunning = false;
|
||||
logger.warn('No UPS devices found in configuration. Daemon will remain idle...');
|
||||
// Don't exit - enter idle monitoring mode instead
|
||||
await this.idleMonitoring();
|
||||
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
|
||||
*/
|
||||
|
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
|
||||
* including support for logboxes with title, lines, and closing
|
||||
*/
|
||||
export class Logger {
|
||||
private currentBoxWidth: number | null = null;
|
||||
private currentBoxStyle: TBoxStyle = 'default';
|
||||
private static instance: Logger;
|
||||
|
||||
/** 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
* @param title Title of the logbox
|
||||
* @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.currentBoxStyle = style || 'default';
|
||||
|
||||
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||
|
||||
// Create the title line with appropriate padding
|
||||
const paddedTitle = ` ${title} `;
|
||||
@@ -74,7 +150,7 @@ export class Logger {
|
||||
// Title line: ┌─ Title ───┐
|
||||
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 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 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
|
||||
const padding = availableSpace - content.length - 1;
|
||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
||||
const padding = availableSpace - visibleLen - 1;
|
||||
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||
console.log(colorFn(line));
|
||||
} else {
|
||||
// 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 {
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||
|
||||
// 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.currentBoxStyle = 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,9 +205,10 @@ export class Logger {
|
||||
* @param title Title of the logbox
|
||||
* @param lines Array of content lines
|
||||
* @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 {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
||||
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||
|
||||
for (const line of lines) {
|
||||
this.logBoxLine(line);
|
||||
@@ -141,6 +225,108 @@ export class Logger {
|
||||
public logDivider(width?: number, character: string = '─'): void {
|
||||
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
|
||||
|
Reference in New Issue
Block a user