/** * Service Handler * * CLI commands for systemd service management. */ import process from 'node:process'; import { execSync } from 'node:child_process'; import { logger } from '../logger.ts'; import { theme } from '../colors.ts'; import { PATHS } from '../constants.ts'; import type { ModelGrid } from '../modelgrid.ts'; /** * Handler for service-related CLI commands */ export class ServiceHandler { private readonly modelgrid: ModelGrid; constructor(modelgrid: ModelGrid) { this.modelgrid = modelgrid; } /** * Enable the service (requires root) */ public async enable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.modelgrid.getSystemd().install(); logger.log('ModelGrid service has been installed. Use "modelgrid service start" to start the service.'); } /** * Start the daemon directly */ public async daemonStart(debugMode: boolean = false): Promise { logger.log('Starting ModelGrid daemon...'); try { if (debugMode) { logger.log('Debug mode enabled'); } await this.modelgrid.getDaemon().start(); } catch (error) { logger.error(`Daemon start failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } /** * Show logs of the systemd service */ public async logs(): Promise { try { const { spawn } = await import('child_process'); logger.log('Tailing modelgrid service logs (Ctrl+C to exit)...\n'); const journalctl = spawn('journalctl', ['-u', 'modelgrid.service', '-n', '50', '-f'], { stdio: ['ignore', 'inherit', 'inherit'], }); process.on('SIGINT', () => { journalctl.kill('SIGINT'); process.exit(0); }); 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.modelgrid.getSystemd().stop(); } /** * Start the systemd service */ public async start(): Promise { try { await this.modelgrid.getSystemd().start(); } catch (error) { process.exit(1); } } /** * Show status of the systemd service */ public async status(): Promise { await this.modelgrid.getSystemd().getStatus(); } /** * Disable the service (requires root) */ public async disable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.modelgrid.getSystemd().disable(); } /** * Check if the user has root access */ private checkRootAccess(errorMessage: string): void { if (process.getuid && process.getuid() !== 0) { logger.error(errorMessage); process.exit(1); } } /** * Update ModelGrid from repository */ public async update(): Promise { try { this.checkRootAccess('This command must be run as root to update ModelGrid.'); console.log(''); logger.info('Checking for updates...'); try { const currentVersion = this.modelgrid.getVersion(); const apiUrl = 'https://code.foss.global/api/v1/repos/modelgrid.com/modelgrid/releases/latest'; const response = execSync(`curl -sSL ${apiUrl}`).toString(); const release = JSON.parse(response); const latestVersion = release.tag_name; 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(''); 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(''); const installUrl = 'https://code.foss.global/modelgrid.com/modelgrid/raw/branch/main/install.sh'; execSync(`curl -sSL ${installUrl} | bash`, { stdio: 'inherit', }); 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 ModelGrid from the system */ public async uninstall(): Promise { this.checkRootAccess('This command must be run as root.'); try { const helpers = await import('../helpers/index.ts'); const { prompt, close } = await helpers.createPrompt(); logger.log(''); logger.highlight('ModelGrid Uninstaller'); logger.dim('====================='); logger.log('This will completely remove ModelGrid from your system.'); logger.log(''); const removeConfig = await prompt('Do you want to remove configuration files? (y/N): '); const removeContainers = await prompt('Do you want to remove Docker containers? (y/N): '); close(); // Stop service first try { await this.modelgrid.getSystemd().stop(); } catch { // Service might not be running } // Disable service try { await this.modelgrid.getSystemd().disable(); } catch { // Service might not be installed } // Remove containers if requested if (removeContainers.toLowerCase() === 'y') { logger.info('Removing Docker containers...'); try { execSync('docker rm -f $(docker ps -aq --filter "name=modelgrid")', { stdio: 'pipe' }); } catch { // No containers to remove } } // Remove configuration if requested if (removeConfig.toLowerCase() === 'y') { logger.info('Removing configuration...'); try { const { rm } = await import('node:fs/promises'); await rm(PATHS.CONFIG_DIR, { recursive: true, force: true }); } catch { // Config might not exist } } // Run uninstall script const { dirname, join } = await import('path'); const binPath = process.argv[1]; const modulePath = dirname(dirname(binPath)); const uninstallScriptPath = join(modulePath, 'uninstall.sh'); logger.log(''); logger.log(`Running uninstaller from ${uninstallScriptPath}...`); execSync(`sudo bash ${uninstallScriptPath}`, { env: { ...process.env, REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no', MODELGRID_CLI_CALL: 'true', }, stdio: 'inherit', }); } catch (error) { logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } }