2025-10-19 12:57:17 +00:00
|
|
|
import process from 'node:process';
|
2025-10-19 13:14:18 +00:00
|
|
|
import { execSync } from 'node:child_process';
|
2025-10-18 11:59:55 +00:00
|
|
|
import { Nupst } from '../nupst.ts';
|
|
|
|
import { logger } from '../logger.ts';
|
2025-03-28 22:12:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<void> {
|
|
|
|
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<void> {
|
|
|
|
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<void> {
|
|
|
|
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<void>((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<void> {
|
|
|
|
await this.nupst.getSystemd().stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the systemd service
|
|
|
|
*/
|
|
|
|
public async start(): Promise<void> {
|
|
|
|
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<void> {
|
|
|
|
// 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<void> {
|
|
|
|
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<void> {
|
|
|
|
try {
|
|
|
|
// Check if running as root
|
|
|
|
this.checkRootAccess(
|
2025-10-19 21:54:05 +00:00
|
|
|
'This command must be run as root to update NUPST.',
|
2025-03-28 22:12:01 +00:00
|
|
|
);
|
|
|
|
|
2025-10-19 21:54:05 +00:00
|
|
|
console.log('');
|
2025-10-19 22:50:03 +00:00
|
|
|
logger.info('Checking for updates...');
|
2025-03-28 22:12:01 +00:00
|
|
|
|
|
|
|
try {
|
2025-10-19 22:50:03 +00:00
|
|
|
// 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"
|
|
|
|
|
2025-10-19 22:56:12 +00:00
|
|
|
// 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}`);
|
2025-10-19 22:50:03 +00:00
|
|
|
console.log('');
|
|
|
|
|
2025-10-19 22:56:12 +00:00
|
|
|
// Compare normalized versions
|
|
|
|
if (normalizedCurrent === normalizedLatest) {
|
2025-10-19 22:50:03 +00:00
|
|
|
logger.success('Already up to date!');
|
|
|
|
console.log('');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`New version available: ${latestVersion}`);
|
|
|
|
logger.dim('Downloading and installing...');
|
|
|
|
console.log('');
|
|
|
|
|
2025-10-19 21:54:05 +00:00
|
|
|
// 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
|
2025-03-28 22:12:01 +00:00
|
|
|
});
|
|
|
|
|
2025-10-19 21:54:05 +00:00
|
|
|
console.log('');
|
2025-10-19 22:50:03 +00:00
|
|
|
logger.success(`Updated to ${latestVersion}`);
|
2025-10-19 21:54:05 +00:00
|
|
|
console.log('');
|
2025-03-28 22:12:01 +00:00
|
|
|
} catch (error) {
|
2025-10-19 21:54:05 +00:00
|
|
|
console.log('');
|
|
|
|
logger.error('Update failed');
|
|
|
|
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
console.log('');
|
2025-03-28 22:12:01 +00:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2025-10-18 21:07:57 +00:00
|
|
|
logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
2025-03-28 22:12:01 +00:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Completely uninstall NUPST from the system
|
|
|
|
*/
|
|
|
|
public async uninstall(): Promise<void> {
|
|
|
|
// 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<string> => {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
rl.question(question, (answer: string) => {
|
|
|
|
resolve(answer);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.log('');
|
|
|
|
logger.highlight('NUPST Uninstaller');
|
|
|
|
logger.dim('===============');
|
|
|
|
logger.log('This will completely remove NUPST from your system.');
|
|
|
|
logger.log('');
|
2025-03-28 22:12:01 +00:00
|
|
|
|
|
|
|
// Ask about removing configuration
|
|
|
|
const removeConfig = await prompt(
|
2025-10-19 13:14:18 +00:00
|
|
|
'Do you want to remove the NUPST configuration files? (y/N): ',
|
2025-03-28 22:12:01 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// 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) {
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
2025-03-28 22:12:01 +00:00
|
|
|
rl.close();
|
2025-10-20 00:32:06 +00:00
|
|
|
process.stdin.destroy();
|
2025-03-28 22:12:01 +00:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close readline before executing script
|
|
|
|
rl.close();
|
2025-10-20 00:32:06 +00:00
|
|
|
process.stdin.destroy();
|
2025-03-28 22:12:01 +00:00
|
|
|
|
|
|
|
// Execute uninstall.sh with the appropriate option
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.log('');
|
|
|
|
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
|
2025-03-28 22:12:01 +00:00
|
|
|
|
|
|
|
// 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) {
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
2025-03-28 22:12:01 +00:00
|
|
|
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 };
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
}
|