From 06cbcd94e2576d8bd04b5d1b26e237662fe1b40a Mon Sep 17 00:00:00 2001 From: Variet Date: Sat, 7 Mar 2026 23:14:51 +0900 Subject: [PATCH] feat(discovery): auto-detect Antigravity via CDP port scan + auto-create sessions --- public/js/app.js | 18 +++++- server/auto-discover.js | 129 ++++++++++++++++++++++++++++++++++++++++ server/index.js | 71 ++++++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 server/auto-discover.js diff --git a/public/js/app.js b/public/js/app.js index 4103c13..d4c430f 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -130,7 +130,23 @@ break; case 'action_clicked': - // 서버에서의 확인 응답 (토스트는 이미 프론트엔드에서 표시) + // 서버에서의 확인 응답 + break; + + case 'auto_discovered': + showToast(`Antigravity 자동 감지: ${msg.session.name}`, 'success'); + // 활성 세션이 없으면 자동 전환 + if (!sessionPanel.activeSessionId) { + sendWs({ type: 'switch_session', sessionId: msg.session.id }); + } + break; + + case 'session_lost': + showToast('Antigravity 연결 소실', 'warning'); + if (sessionPanel.activeSessionId === msg.sessionId) { + chatPanel.showEmpty(); + sessionPanel.setActive(null); + } break; case 'error': diff --git a/server/auto-discover.js b/server/auto-discover.js new file mode 100644 index 0000000..732ed95 --- /dev/null +++ b/server/auto-discover.js @@ -0,0 +1,129 @@ +/** + * Antigravity 자동 발견 모듈 + * + * localhost의 CDP 포트 범위를 스캔하여 실행 중인 Antigravity 인스턴스를 감지. + * 발견된 인스턴스는 자동으로 세션 생성하여 웹 UI에 표시. + */ + +const http = require('http'); + +// 스캔할 포트 범위 +const SCAN_PORTS = [9000, 9222, 9229, 9333, 9001, 9002, 9003, 9004, 9005]; +const SCAN_INTERVAL_MS = 15000; // 15초마다 스캔 + +/** + * 특정 포트의 CDP 엔드포인트에 요청하여 Antigravity인지 확인 + * @returns {Promise<{port, title, url}|null>} + */ +function probeCDPPort(host, port, timeoutMs = 2000) { + return new Promise((resolve) => { + const req = http.get(`http://${host}:${port}/json`, { timeout: timeoutMs }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const targets = JSON.parse(data); + // Antigravity 워크벤치 타겟 찾기 + const mainTarget = targets.find(t => + t.type === 'page' && + t.url.includes('workbench.html') && + !t.url.includes('jetski') + ); + if (mainTarget) { + resolve({ + port, + title: mainTarget.title || 'Antigravity', + url: mainTarget.url, + }); + } else { + resolve(null); + } + } catch { + resolve(null); + } + }); + }); + req.on('error', () => resolve(null)); + req.on('timeout', () => { req.destroy(); resolve(null); }); + }); +} + +/** + * 모든 포트를 병렬 스캔하여 Antigravity 인스턴스 목록 반환 + * @returns {Promise>} + */ +async function scanForAntigravity(host = 'localhost') { + const probes = SCAN_PORTS.map(port => probeCDPPort(host, port)); + const results = await Promise.all(probes); + return results.filter(Boolean); +} + +class AutoDiscovery { + constructor(options = {}) { + this.host = options.host || 'localhost'; + this.intervalMs = options.intervalMs || SCAN_INTERVAL_MS; + this.onDiscovered = options.onDiscovered || null; // callback({port, title, url}) + this.onLost = options.onLost || null; // callback(port) + this.knownPorts = new Set(); + this.timer = null; + } + + /** + * 자동 발견 시작 + */ + async start() { + console.log(`[AutoDiscovery] 시작 — ${this.host}에서 Antigravity 포트 스캔`); + + // 즉시 한 번 스캔 + await this._scan(); + + // 주기적 스캔 + this.timer = setInterval(() => this._scan(), this.intervalMs); + } + + /** + * 자동 발견 중지 + */ + stop() { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + /** + * 내부 스캔 로직 + */ + async _scan() { + try { + const found = await scanForAntigravity(this.host); + const foundPorts = new Set(found.map(f => f.port)); + + // 새로 발견된 인스턴스 + for (const instance of found) { + if (!this.knownPorts.has(instance.port)) { + this.knownPorts.add(instance.port); + console.log(`[AutoDiscovery] 발견: ${this.host}:${instance.port} — ${instance.title}`); + if (this.onDiscovered) { + this.onDiscovered(instance); + } + } + } + + // 사라진 인스턴스 + for (const port of this.knownPorts) { + if (!foundPorts.has(port)) { + this.knownPorts.delete(port); + console.log(`[AutoDiscovery] 소실: ${this.host}:${port}`); + if (this.onLost) { + this.onLost(port); + } + } + } + } catch (err) { + console.error('[AutoDiscovery] 스캔 오류:', err.message); + } + } +} + +module.exports = { AutoDiscovery, scanForAntigravity, probeCDPPort }; diff --git a/server/index.js b/server/index.js index 62e9f79..e571d33 100644 --- a/server/index.js +++ b/server/index.js @@ -10,6 +10,7 @@ const http = require('http'); const path = require('path'); const { WebSocketServer, WebSocket } = require('ws'); const SessionManager = require('./session-manager'); +const { AutoDiscovery } = require('./auto-discover'); const PORT = process.env.PORT || 3300; const app = express(); @@ -326,8 +327,78 @@ server.listen(PORT, () => { console.log(` Gravity Web 서버 시작`); console.log(` http://localhost:${PORT}`); console.log('═'.repeat(50)); + + // Auto Discovery 시작 + const discovery = new AutoDiscovery({ + host: 'localhost', + intervalMs: 15000, + onDiscovered: async (instance) => { + // 이미 같은 포트의 세션이 있는지 확인 + const existing = sessionManager.listSessions().find( + s => s.host === 'localhost' && s.cdpPort === instance.port + ); + if (existing) return; + + // 자동 세션 생성 + try { + const session = await sessionManager.addSession( + `Antigravity :${instance.port}`, + 'localhost', + instance.port + ); + + console.log(`[AutoDiscovery] 자동 세션 생성: ${session.name} (${session.id})`); + + // 모든 WS 클라이언트에 세션 목록 업데이트 + broadcastToAll({ + type: 'sessions_list', + sessions: sessionManager.listSessions(), + }); + + // 자동 발견 알림 + broadcastToAll({ + type: 'auto_discovered', + session: { + id: session.id, + name: session.name, + host: 'localhost', + port: instance.port, + status: session.status, + }, + }); + } catch (err) { + console.error(`[AutoDiscovery] 세션 생성 실패:`, err.message); + } + }, + onLost: (port) => { + // 해당 포트의 세션 상태 업데이트 + const sessions = sessionManager.listSessions(); + const lost = sessions.find(s => s.cdpPort === port && s.host === 'localhost'); + if (lost) { + console.log(`[AutoDiscovery] 세션 소실: ${lost.name}`); + broadcastToAll({ + type: 'session_lost', + sessionId: lost.id, + port, + }); + } + }, + }); + discovery.start(); }); +/** + * 모든 WS 클라이언트에 메시지 브로드캐스트 + */ +function broadcastToAll(data) { + const json = JSON.stringify(data); + for (const [ws] of wsClients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(json); + } + } +} + // Graceful shutdown process.on('SIGINT', async () => { console.log('\n[Server] 종료 중...');