UNPKG

create-ai-chat-context-experimental

Version:

Phase 2: TypeScript rewrite - AI Chat Context & Memory System with conversation extraction and AICF format support (powered by aicf-core v2.1.0).

229 lines 7.43 kB
/** * This file is part of create-ai-chat-context-experimental. * Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). * See LICENSE file for details. */ /** * Daemon Manager - Manages background watcher daemon lifecycle * Handles PID file, process management, and daemon status tracking */ import { join } from 'path'; import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; import { Ok, Err } from '../types/result.js'; export class DaemonManager { pidFilePath; watcherConfigPath; constructor(projectPath) { this.pidFilePath = join(projectPath, '.aicf', '.watcher.pid'); this.watcherConfigPath = join(projectPath, '.watcher-config.json'); } /** * Write PID file with daemon information */ writePidFile(pid) { try { const info = { pid, startTime: new Date(), }; writeFileSync(this.pidFilePath, JSON.stringify(info, null, 2)); return Ok(undefined); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Read PID file and return daemon information */ readPidFile() { try { if (!existsSync(this.pidFilePath)) { return Err(new Error('PID file not found')); } const content = readFileSync(this.pidFilePath, 'utf-8'); const info = JSON.parse(content); // Convert ISO string back to Date info.startTime = new Date(info.startTime); if (info.lastSync) { info.lastSync = new Date(info.lastSync); } return Ok(info); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Delete PID file */ deletePidFile() { try { if (existsSync(this.pidFilePath)) { unlinkSync(this.pidFilePath); } return Ok(undefined); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Check if process is running */ isProcessRunning(pid) { try { // Send signal 0 to check if process exists // This doesn't actually send a signal, just checks if we can process.kill(pid, 0); return true; } catch { // Process doesn't exist or we don't have permission return false; } } /** * Get daemon status */ getStatus() { try { const pidResult = this.readPidFile(); if (!pidResult.ok) { // No PID file - daemon not running return Ok({ running: false, }); } const info = pidResult.value; const running = this.isProcessRunning(info.pid); if (!running) { // PID file exists but process is dead - clean up stale PID file this.deletePidFile(); return Ok({ running: false, }); } // Calculate uptime const uptimeMs = Date.now() - info.startTime.getTime(); const uptime = this.formatUptime(uptimeMs); // Get enabled platforms from watcher config const platforms = this.getEnabledPlatforms(); return Ok({ running: true, pid: info.pid, startTime: info.startTime, uptime, lastSync: info.lastSync, platforms, }); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Stop daemon by PID */ stopDaemon() { try { const pidResult = this.readPidFile(); if (!pidResult.ok) { return Err(new Error('No daemon running (PID file not found)')); } const info = pidResult.value; if (!this.isProcessRunning(info.pid)) { // Process already dead - clean up PID file this.deletePidFile(); return Err(new Error('Daemon not running (stale PID file removed)')); } // Send SIGTERM to gracefully stop the process process.kill(info.pid, 'SIGTERM'); // Wait a bit for graceful shutdown const maxWait = 5000; // 5 seconds const startTime = Date.now(); while (Date.now() - startTime < maxWait) { if (!this.isProcessRunning(info.pid)) { // Process stopped - clean up PID file this.deletePidFile(); return Ok(undefined); } // Sleep for 100ms const sleepMs = 100; const start = Date.now(); while (Date.now() - start < sleepMs) { // Busy wait } } // Process didn't stop gracefully - force kill process.kill(info.pid, 'SIGKILL'); this.deletePidFile(); return Ok(undefined); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Update last sync time in PID file */ updateLastSync() { try { const pidResult = this.readPidFile(); if (!pidResult.ok) { return Err(new Error('PID file not found')); } const info = pidResult.value; info.lastSync = new Date(); writeFileSync(this.pidFilePath, JSON.stringify(info, null, 2)); return Ok(undefined); } catch (error) { return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Format uptime in human-readable format */ formatUptime(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } else if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } /** * Get enabled platforms from watcher config */ getEnabledPlatforms() { try { if (!existsSync(this.watcherConfigPath)) { return []; } const content = readFileSync(this.watcherConfigPath, 'utf-8'); const config = JSON.parse(content); const platforms = []; for (const [platform, settings] of Object.entries(config.platforms || {})) { if (settings.enabled) { platforms.push(platform); } } return platforms; } catch { return []; } } } //# sourceMappingURL=DaemonManager.js.map