diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts index f89835e..d108d50 100644 --- a/frontend/src/config/endpoints.ts +++ b/frontend/src/config/endpoints.ts @@ -27,6 +27,9 @@ export const endpoints = { // Terminal WebSocket terminal: buildWsUrl('/ws/terminal', 4103), + // Git WebSocket (realtime notifications, separate from terminal) + git: buildWsUrl('/ws/git', 4105), + // Claude status WebSocket (same backend as terminal) claudeStatus: buildWsUrl('/ws/status', 4103), diff --git a/frontend/src/pages/GitPage.vue b/frontend/src/pages/GitPage.vue index 907b8c0..c9bae62 100644 --- a/frontend/src/pages/GitPage.vue +++ b/frontend/src/pages/GitPage.vue @@ -50,7 +50,7 @@ const isRealtime = ref(false) function connectGitWatcher() { if (gitSocket?.readyState === WebSocket.OPEN) return - gitSocket = new WebSocket(endpoints.terminal) + gitSocket = new WebSocket(endpoints.git) gitSocket.onopen = () => { isRealtime.value = true diff --git a/server/config.ts b/server/config.ts index fd0626a..1abd7bb 100644 --- a/server/config.ts +++ b/server/config.ts @@ -1,6 +1,7 @@ // Server configuration export const PORT_HTTP = 4101 export const PORT_TERMINAL = 4103 +export const PORT_GIT = 4105 // Terminal configuration export const WORKING_DIR = process.cwd().replace(/[\\\/]server$/, '') diff --git a/server/services/git-watcher.ts b/server/services/git-watcher.ts new file mode 100644 index 0000000..62c9118 --- /dev/null +++ b/server/services/git-watcher.ts @@ -0,0 +1,158 @@ +/** + * 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() +} diff --git a/server/services/terminal.ts b/server/services/terminal.ts index 62890c2..e5a9b4a 100644 --- a/server/services/terminal.ts +++ b/server/services/terminal.ts @@ -1,6 +1,4 @@ import { spawn, type IPty } from '@skitee3000/bun-pty' -import { watch, type FSWatcher } from 'fs' -import { join } from 'path' import { PORT_TERMINAL, WORKING_DIR, SHELL, SHELL_ARGS, DEFAULT_SESSION_ID, MAX_BUFFER_LINES } from '../config' interface TerminalSession { @@ -266,93 +264,3 @@ export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) { console.log(`[Terminal] Claude status broadcast: ${status}${tool ? ` (${tool})` : ''} → ${clientCount} clients`) } - -// Git watcher -let gitWatcher: FSWatcher | null = null -let gitDebounceTimer: Timer | null = null -const GIT_DEBOUNCE_MS = 300 - -function broadcastGitChange() { - const message = JSON.stringify({ - type: 'git-change', - timestamp: Date.now() - }) - - let clientCount = 0 - for (const [, session] of sessions) { - for (const ws of session.clients) { - try { - ws.send(message) - clientCount++ - } catch { - // Client disconnected - } - } - } - - if (clientCount > 0) { - console.log(`[Git] Change detected → ${clientCount} clients notified`) - } -} - -// Files to ignore (noisy or irrelevant) -const GIT_IGNORE_PATTERNS = [ - 'FETCH_HEAD', - 'gc.log', - '.lock', - 'COMMIT_EDITMSG', - 'ORIG_HEAD', - 'gitk.cache', - 'sourcetreeconfig', - '.DS_Store', - 'fsmonitor', - 'packed-refs' -] - -export function startGitWatcher() { - const gitDir = join(WORKING_DIR, '.git') - - try { - // Watch .git directory recursively - gitWatcher = watch(gitDir, { recursive: true }, (eventType, filename) => { - if (!filename) return - - // Ignore noisy files - const shouldIgnore = GIT_IGNORE_PATTERNS.some(pattern => - filename.includes(pattern) - ) - if (shouldIgnore) return - - // Only react to meaningful changes - const isRelevant = - filename.includes('refs/') || // branch/tag changes - filename === 'HEAD' || // checkout - filename === 'index' || // staging area - filename.includes('objects/') // new commits/blobs - - if (!isRelevant) return - - // Debounce to avoid flooding - if (gitDebounceTimer) { - clearTimeout(gitDebounceTimer) - } - - gitDebounceTimer = setTimeout(() => { - console.log(`[Git] Change: ${filename}`) - broadcastGitChange() - }, GIT_DEBOUNCE_MS) - }) - - console.log(`[Git] Watching ${gitDir} for changes`) - } catch (e: any) { - console.error(`[Git] Failed to watch .git directory: ${e.message}`) - } -} - -export function stopGitWatcher() { - if (gitWatcher) { - gitWatcher.close() - gitWatcher = null - console.log('[Git] Watcher stopped') - } -} diff --git a/server/terminal.ts b/server/terminal.ts index 210c999..1c87d26 100644 --- a/server/terminal.ts +++ b/server/terminal.ts @@ -5,15 +5,16 @@ * even when the main server restarts due to code changes. */ -import { startTerminalServer, startGitWatcher } from './services/terminal' +import { startTerminalServer } from './services/terminal' +import { startGitServer } from './services/git-watcher' import { WORKING_DIR } from './config' console.log('') console.log('='.repeat(50)) console.log('Terminal Server (Independent Process)') -console.log(` WebSocket: ws://localhost:4103`) +console.log(` Terminal WebSocket: ws://localhost:4103`) +console.log(` Git WebSocket: ws://localhost:4105`) console.log(` Working Dir: ${WORKING_DIR}`) -console.log(` Git Watcher: enabled`) console.log('') console.log('This process is stable and won\'t restart') console.log('when the main server reloads.') @@ -21,4 +22,4 @@ console.log('='.repeat(50)) console.log('') startTerminalServer() -startGitWatcher() +startGitServer()