import process from 'node:process'; import { execSync } from 'node:child_process'; import { Nupst } from '../nupst.ts'; import { logger } from '../logger.ts'; import { theme } from '../colors.ts'; import * as helpers from '../helpers/index.ts'; /** * Class for handling feature-related CLI commands * Provides interface for managing optional features like HTTP server */ export class FeatureHandler { private readonly nupst: Nupst; /** * Create a new feature handler * @param nupst Reference to the main Nupst instance */ constructor(nupst: Nupst) { this.nupst = nupst; } /** * Configure HTTP server feature */ public async configureHttpServer(): Promise { try { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const prompt = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer); }); }); }; try { 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)}`); } } /** * Run the interactive HTTP server configuration process * @param prompt Function to prompt for user input */ private async runHttpServerConfig(prompt: (question: string) => Promise): Promise { logger.log(''); logger.logBoxTitle('HTTP Server Feature Configuration', 60); logger.logBoxLine('Configure the HTTP server to expose UPS status as JSON'); logger.logBoxEnd(); logger.log(''); // Load config let config; try { await this.nupst.getDaemon().loadConfig(); config = this.nupst.getDaemon().getConfig(); } catch (error) { logger.error('No configuration found. Please run "nupst ups add" first.'); return; } // Show current status if (config.httpServer?.enabled) { logger.info('HTTP Server is currently: ' + theme.success('ENABLED')); logger.log(` Port: ${theme.highlight(String(config.httpServer.port))}`); logger.log(` Path: ${theme.highlight(config.httpServer.path)}`); logger.log(` Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`); logger.log(''); } else { logger.info('HTTP Server is currently: ' + theme.dim('DISABLED')); logger.log(''); } // Ask enable/disable const action = await prompt('Enable or disable HTTP server? (enable/disable/cancel): '); if (action.toLowerCase() === 'cancel' || action.toLowerCase() === 'c') { logger.log('Cancelled.'); return; } if (action.toLowerCase() === 'disable' || action.toLowerCase() === 'd') { // Disable HTTP server config.httpServer = { enabled: false, port: config.httpServer?.port || 8080, path: config.httpServer?.path || '/ups-status', authToken: config.httpServer?.authToken || '', }; this.nupst.getDaemon().saveConfig(config); logger.log(''); logger.success('HTTP Server disabled'); logger.log(''); await this.restartServiceIfRunning(); return; } if (action.toLowerCase() !== 'enable' && action.toLowerCase() !== 'e') { logger.error('Invalid option. Please enter "enable", "disable", or "cancel".'); return; } // Enable - gather configuration logger.log(''); const portInput = await prompt(`HTTP Server Port [${config.httpServer?.port || 8080}]: `); const port = portInput ? parseInt(portInput, 10) : (config.httpServer?.port || 8080); if (isNaN(port) || port < 1 || port > 65535) { logger.error('Invalid port number. Must be between 1 and 65535.'); return; } const pathInput = await prompt(`URL Path [${config.httpServer?.path || '/ups-status'}]: `); const path = pathInput || config.httpServer?.path || '/ups-status'; // Ensure path starts with / const finalPath = path.startsWith('/') ? path : `/${path}`; // Generate or reuse auth token let authToken = config.httpServer?.authToken; if (!authToken) { // Generate new random token authToken = helpers.shortId() + helpers.shortId() + helpers.shortId(); logger.log(''); logger.info('Generated new authentication token'); } else { const regenerate = await prompt('Regenerate authentication token? (y/N): '); if (regenerate.toLowerCase() === 'y' || regenerate.toLowerCase() === 'yes') { authToken = helpers.shortId() + helpers.shortId() + helpers.shortId(); logger.info('Generated new authentication token'); } } // Save configuration config.httpServer = { enabled: true, port, path: finalPath, authToken, }; this.nupst.getDaemon().saveConfig(config); // Display summary logger.log(''); logger.logBoxTitle('HTTP Server Configuration', 70, 'success'); logger.logBoxLine(`Status: ${theme.success('ENABLED')}`); logger.logBoxLine(`Port: ${theme.highlight(String(port))}`); logger.logBoxLine(`Path: ${theme.highlight(finalPath)}`); logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`); logger.logBoxLine(''); logger.logBoxLine(theme.dim('Usage examples:')); logger.logBoxLine(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`); logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`); logger.logBoxEnd(); logger.log(''); logger.warn('IMPORTANT: Save the authentication token securely!'); logger.log(''); await this.restartServiceIfRunning(); } /** * Restart the service if it's currently running */ private async restartServiceIfRunning(): Promise { try { const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; 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((resolve) => { rl.question('Service is running. Restart to apply changes? (Y/n): ', resolve); }); rl.close(); if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { logger.info('Restarting service...'); execSync('sudo systemctl restart nupst.service'); logger.success('Service restarted successfully'); } else { logger.warn('Changes will take effect on next service restart'); } } } catch (error) { // Ignore errors - service might not be installed } } }