214 lines
6.8 KiB
TypeScript
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|