/** * Git Watcher Service * WebSocket server dedicado para notificaciones de cambios en git. * Completamente separado del terminal. */ import { watch, type FSWatcher } from 'fs' import { join } from 'path' import { PORT_GIT, WORKING_DIR } from '../config' // Connected clients (simple WebSocket set, no PTY, no sessions) const clients = new Set() // Git watcher let gitWatcher: FSWatcher | null = null let debounceTimer: ReturnType | null = null const DEBOUNCE_MS = 300 // Files to ignore const IGNORE_PATTERNS = [ 'FETCH_HEAD', 'gc.log', '.lock', 'COMMIT_EDITMSG', 'ORIG_HEAD', 'gitk.cache', 'sourcetreeconfig', '.DS_Store', 'fsmonitor', 'packed-refs' ] function broadcastGitChange() { const message = JSON.stringify({ type: 'git-change', timestamp: Date.now() }) let count = 0 for (const ws of clients) { try { ws.send(message) count++ } catch { // Client disconnected clients.delete(ws) } } if (count > 0) { console.log(`[Git] Change → ${count} clients`) } } function startWatcher() { const gitDir = join(WORKING_DIR, '.git') try { gitWatcher = watch(gitDir, { recursive: true }, (_, filename) => { if (!filename) return // Ignore noisy files if (IGNORE_PATTERNS.some(p => filename.includes(p))) return // Only meaningful changes const isRelevant = filename.includes('refs/') || filename === 'HEAD' || filename === 'index' || filename.includes('objects/') if (!isRelevant) return // Debounce if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { console.log(`[Git] Change: ${filename}`) broadcastGitChange() }, DEBOUNCE_MS) }) console.log(`[Git] Watching ${gitDir}`) } catch (e: any) { console.error(`[Git] Watch failed: ${e.message}`) } } export function startGitServer() { const server = Bun.serve({ port: PORT_GIT, fetch(req, server) { const url = new URL(req.url) // CORS const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }) } // Health check if (url.pathname === '/health') { return Response.json({ status: 'ok', clients: clients.size, watching: gitWatcher !== null }, { headers: corsHeaders }) } // WebSocket upgrade const upgrade = req.headers.get('upgrade') if (upgrade?.toLowerCase() === 'websocket') { const success = server.upgrade(req) if (success) return undefined return new Response('WebSocket upgrade failed', { status: 400 }) } return new Response('Git WebSocket Server\n\nConnect via WebSocket for real-time git notifications.', { status: 200 }) }, websocket: { open(ws) { clients.add(ws) console.log(`[Git] Client connected (${clients.size} total)`) ws.send(JSON.stringify({ type: 'connected' })) }, message() { // No messages expected from client }, close(ws) { clients.delete(ws) console.log(`[Git] Client disconnected (${clients.size} total)`) } } }) console.log(`[Git] WebSocket server on port ${PORT_GIT}`) // Start file watcher startWatcher() return server } export function stopGitServer() { if (gitWatcher) { gitWatcher.close() gitWatcher = null } if (debounceTimer) { clearTimeout(debounceTimer) debounceTimer = null } clients.clear() }