Files
nupst/ts/cli/feature-handler.ts

214 lines
6.8 KiB
TypeScript

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<void> {
try {
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
try {
await 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<string>): Promise<void> {
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<void> {
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<string>((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
}
}
}