import process from 'node:process'; import { execSync } from 'node:child_process'; import { Nupst } from '../nupst.ts'; import { logger } from '../logger.ts'; /** * Class for handling service-related CLI commands * Provides interface for managing systemd service */ export class ServiceHandler { private readonly nupst: Nupst; /** * Create a new Service handler * @param nupst Reference to the main Nupst instance */ constructor(nupst: Nupst) { this.nupst = nupst; } /** * Enable the service (requires root) */ public async enable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().install(); logger.log('NUPST service has been installed. Use "nupst start" to start the service.'); } /** * Start the daemon directly * @param debugMode Whether to enable debug mode */ public async daemonStart(debugMode: boolean = false): Promise { logger.log('Starting NUPST daemon...'); try { // Enable debug mode for SNMP if requested if (debugMode) { this.nupst.getSnmp().enableDebug(); logger.log('SNMP debug mode enabled'); } await this.nupst.getDaemon().start(); } catch (error) { // Error is already logged and process.exit is called in daemon.start() // No need to handle it here } } /** * Show logs of the systemd service */ public async logs(): Promise { try { // Use exec with spawn to properly follow logs in real-time const { spawn } = await import('child_process'); logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n'); const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], { stdio: ['ignore', 'inherit', 'inherit'], }); // Forward signals to child process process.on('SIGINT', () => { journalctl.kill('SIGINT'); process.exit(0); }); // Wait for process to exit await new Promise((resolve) => { journalctl.on('exit', () => resolve()); }); } catch (error) { logger.error(`Failed to retrieve logs: ${error}`); process.exit(1); } } /** * Stop the systemd service */ public async stop(): Promise { await this.nupst.getSystemd().stop(); } /** * Start the systemd service */ public async start(): Promise { try { await this.nupst.getSystemd().start(); } catch (error) { // Error will be displayed by systemd.start() process.exit(1); } } /** * Show status of the systemd service and UPS */ public async status(): Promise { // Extract debug options from args array const debugOptions = this.extractDebugOptions(process.argv); await this.nupst.getSystemd().getStatus(debugOptions.debugMode); } /** * Disable the service (requires root) */ public async disable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().disable(); } /** * Check if the user has root access * @param errorMessage Error message to display if not root */ private checkRootAccess(errorMessage: string): void { if (process.getuid && process.getuid() !== 0) { logger.error(errorMessage); process.exit(1); } } /** * Update NUPST from repository and refresh systemd service */ public async update(): Promise { try { // Check if running as root this.checkRootAccess( 'This command must be run as root to update NUPST.', ); console.log(''); logger.info('Checking for updates...'); try { // Get current version const currentVersion = this.nupst.getVersion(); // Fetch latest version from Gitea API const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest'; const response = execSync(`curl -sSL ${apiUrl}`).toString(); const release = JSON.parse(response); const latestVersion = release.tag_name; // e.g., "v4.0.7" // Normalize versions for comparison (ensure both have "v" prefix) const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`; const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`; logger.dim(`Current version: ${normalizedCurrent}`); logger.dim(`Latest version: ${normalizedLatest}`); console.log(''); // Compare normalized versions if (normalizedCurrent === normalizedLatest) { logger.success('Already up to date!'); console.log(''); return; } logger.info(`New version available: ${latestVersion}`); logger.dim('Downloading and installing...'); console.log(''); // Download and run the install script // This handles everything: download binary, stop service, replace, restart const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh'; execSync(`curl -sSL ${installUrl} | bash`, { stdio: 'inherit', // Show install script output to user }); console.log(''); logger.success(`Updated to ${latestVersion}`); console.log(''); } catch (error) { console.log(''); logger.error('Update failed'); logger.dim(`${error instanceof Error ? error.message : String(error)}`); console.log(''); process.exit(1); } } catch (error) { logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } /** * Completely uninstall NUPST from the system */ public async uninstall(): Promise { // Check if running as root this.checkRootAccess('This command must be run as root.'); try { // Import readline module for user input const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Helper function to prompt for input const prompt = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer); }); }); }; console.log('\nNUPST Uninstaller'); console.log('==============='); console.log('This will completely remove NUPST from your system.\n'); // Ask about removing configuration const removeConfig = await prompt( 'Do you want to remove the NUPST configuration files? (y/N): ', ); // Find the uninstall.sh script location let uninstallScriptPath: string; // Try to determine script location based on executable path try { // For ESM, we can use import.meta.url, but since we might be in CJS // we'll use a more reliable approach based on process.argv[1] const binPath = process.argv[1]; const { dirname, join } = await import('path'); const modulePath = dirname(dirname(binPath)); uninstallScriptPath = join(modulePath, 'uninstall.sh'); // Check if the script exists const { access } = await import('fs/promises'); await access(uninstallScriptPath); } catch (error) { // If we can't find it in the expected location, try common installation paths const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`]; const { existsSync } = await import('fs'); uninstallScriptPath = ''; for (const path of commonPaths) { if (existsSync(path)) { uninstallScriptPath = path; break; } } if (!uninstallScriptPath) { console.error('Could not locate uninstall.sh script. Aborting uninstall.'); rl.close(); process.exit(1); } } // Close readline before executing script rl.close(); // Execute uninstall.sh with the appropriate option console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); // Pass the configuration removal option as an environment variable const env = { ...process.env, REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no', REMOVE_REPO: 'yes', // Always remove repo as requested NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI }; // Run the uninstall script with sudo execSync(`sudo bash ${uninstallScriptPath}`, { env, stdio: 'inherit', // Show output in the terminal }); } catch (error) { console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } /** * Extract and remove debug options from args array * @param args Command line arguments * @returns Object with debug flags and cleaned args */ private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { const debugMode = args.includes('--debug') || args.includes('-d'); // Remove debug flags from args const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d'); return { debugMode, cleanedArgs }; } }