gemini-cli-devtools 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -9,6 +9,9 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import os from 'node:os';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { WebSocketServer } from 'ws';
13
+ const VITE_DEV_PORT = 5174;
14
+ const isDev = process.env.NODE_ENV !== 'production';
12
15
  /**
13
16
  * DevTools Viewer
14
17
  *
@@ -19,6 +22,8 @@ export class DevTools extends EventEmitter {
19
22
  logs = [];
20
23
  consoleLogs = [];
21
24
  server = null;
25
+ wss = null;
26
+ sessions = new Map();
22
27
  port = 25417;
23
28
  watchedFiles = new Map(); // filePath -> lastSize
24
29
  constructor() {
@@ -180,7 +185,14 @@ export class DevTools extends EventEmitter {
180
185
  return `http://127.0.0.1:${this.port}`;
181
186
  }
182
187
  getClientPath() {
183
- return path.join(path.dirname(fileURLToPath(import.meta.url)), '../client');
188
+ const srcDir = path.dirname(fileURLToPath(import.meta.url));
189
+ // When running with tsx from src/, look for dist/client
190
+ // When running compiled from dist/src/, look for ../client
191
+ const distClient = path.join(srcDir, '../dist/client');
192
+ if (fs.existsSync(distClient)) {
193
+ return distClient;
194
+ }
195
+ return path.join(srcDir, '../client');
184
196
  }
185
197
  start() {
186
198
  return new Promise((resolve) => {
@@ -191,40 +203,8 @@ export class DevTools extends EventEmitter {
191
203
  const clientPath = this.getClientPath();
192
204
  this.server = http.createServer((req, res) => {
193
205
  res.setHeader('Access-Control-Allow-Origin', '*');
194
- if (req.url === '/' || req.url === '/index.html') {
195
- fs.readFile(path.join(clientPath, 'index.html'), (err, data) => {
196
- if (err) {
197
- res.writeHead(500);
198
- res.end('Error loading client');
199
- }
200
- else {
201
- res.writeHead(200, { 'Content-Type': 'text/html' });
202
- res.end(data);
203
- }
204
- });
205
- }
206
- else if (req.url?.startsWith('/assets/')) {
207
- const assetPath = path.join(clientPath, req.url);
208
- fs.readFile(assetPath, (err, data) => {
209
- if (err) {
210
- res.writeHead(404);
211
- res.end('Not Found');
212
- }
213
- else {
214
- const ext = path.extname(assetPath);
215
- let contentType = 'text/plain';
216
- if (ext === '.js')
217
- contentType = 'application/javascript';
218
- if (ext === '.css')
219
- contentType = 'text/css';
220
- if (ext === '.svg')
221
- contentType = 'image/svg+xml';
222
- res.writeHead(200, { 'Content-Type': contentType });
223
- res.end(data);
224
- }
225
- });
226
- }
227
- else if (req.url === '/logs') {
206
+ // API routes
207
+ if (req.url === '/logs') {
228
208
  res.writeHead(200, { 'Content-Type': 'application/json' });
229
209
  res.end(JSON.stringify(this.logs));
230
210
  }
@@ -232,6 +212,11 @@ export class DevTools extends EventEmitter {
232
212
  res.writeHead(200, { 'Content-Type': 'application/json' });
233
213
  res.end(JSON.stringify(this.consoleLogs));
234
214
  }
215
+ else if (req.url === '/sessions') {
216
+ res.writeHead(200, { 'Content-Type': 'application/json' });
217
+ const sessionIds = Array.from(this.sessions.keys());
218
+ res.end(JSON.stringify(sessionIds));
219
+ }
235
220
  else if (req.url === '/events') {
236
221
  res.writeHead(200, {
237
222
  'Content-Type': 'text/event-stream',
@@ -240,13 +225,45 @@ export class DevTools extends EventEmitter {
240
225
  });
241
226
  const l1 = () => res.write(`event: update\ndata: \n\n`);
242
227
  const l2 = () => res.write(`event: console-update\ndata: \n\n`);
228
+ const l3 = () => res.write(`event: session-update\ndata: \n\n`);
243
229
  this.on('update', l1);
244
230
  this.on('console-update', l2);
231
+ this.on('session-update', l3);
245
232
  req.on('close', () => {
246
233
  this.off('update', l1);
247
234
  this.off('console-update', l2);
235
+ this.off('session-update', l3);
248
236
  });
249
237
  }
238
+ else if (req.method === 'POST' && req.url === '/ingest') {
239
+ let body = '';
240
+ req.on('data', (chunk) => (body += chunk));
241
+ req.on('end', () => {
242
+ try {
243
+ const entry = JSON.parse(body);
244
+ if (entry.type === 'console') {
245
+ this.addInternalConsoleLog(entry.payload, entry.sessionId, entry.timestamp);
246
+ }
247
+ else if (entry.type === 'network') {
248
+ this.addInternalNetworkLog(entry.payload, entry.sessionId, entry.timestamp);
249
+ }
250
+ res.writeHead(200);
251
+ res.end('OK');
252
+ }
253
+ catch {
254
+ res.writeHead(400);
255
+ res.end('Invalid JSON');
256
+ }
257
+ });
258
+ }
259
+ else if (isDev) {
260
+ // Dev mode: proxy to Vite
261
+ this.proxyToVite(req, res);
262
+ }
263
+ else {
264
+ // Production: serve static files
265
+ this.serveStatic(req, res, clientPath);
266
+ }
250
267
  });
251
268
  this.server.on('error', (e) => {
252
269
  if (typeof e === 'object' &&
@@ -258,8 +275,130 @@ export class DevTools extends EventEmitter {
258
275
  }
259
276
  });
260
277
  this.server.listen(this.port, '127.0.0.1', () => {
278
+ this.setupWebSocketServer();
261
279
  resolve(this.getUrl());
262
280
  });
263
281
  });
264
282
  }
283
+ setupWebSocketServer() {
284
+ if (!this.server)
285
+ return;
286
+ this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
287
+ this.wss.on('connection', (ws) => {
288
+ let sessionId = null;
289
+ ws.on('message', (data) => {
290
+ try {
291
+ const message = JSON.parse(data.toString());
292
+ // Handle registration first
293
+ if (message.type === 'register') {
294
+ sessionId = message.sessionId;
295
+ this.sessions.set(sessionId, {
296
+ sessionId,
297
+ ws,
298
+ lastPing: Date.now(),
299
+ });
300
+ console.log(`📡 WebSocket registered: ${sessionId}`);
301
+ // Notify session update
302
+ this.emit('session-update');
303
+ // Send registration acknowledgement
304
+ ws.send(JSON.stringify({
305
+ type: 'registered',
306
+ sessionId,
307
+ timestamp: Date.now(),
308
+ }));
309
+ }
310
+ else if (sessionId) {
311
+ this.handleWebSocketMessage(sessionId, message);
312
+ }
313
+ }
314
+ catch (err) {
315
+ console.error('Invalid WebSocket message:', err);
316
+ }
317
+ });
318
+ ws.on('close', () => {
319
+ if (sessionId) {
320
+ console.log(`📡 WebSocket disconnected: ${sessionId}`);
321
+ this.sessions.delete(sessionId);
322
+ this.emit('session-disconnected', sessionId);
323
+ this.emit('session-update');
324
+ }
325
+ });
326
+ ws.on('error', (err) => {
327
+ console.error(`WebSocket error:`, err);
328
+ });
329
+ });
330
+ // Heartbeat mechanism
331
+ setInterval(() => {
332
+ const now = Date.now();
333
+ this.sessions.forEach((session, sessionId) => {
334
+ if (now - session.lastPing > 30000) {
335
+ console.log(`⚠️ Session ${sessionId} timeout, closing...`);
336
+ session.ws.close();
337
+ this.sessions.delete(sessionId);
338
+ }
339
+ else {
340
+ // Send ping
341
+ session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
342
+ }
343
+ });
344
+ }, 10000);
345
+ }
346
+ handleWebSocketMessage(sessionId, message) {
347
+ const session = this.sessions.get(sessionId);
348
+ if (!session)
349
+ return;
350
+ switch (message.type) {
351
+ case 'pong':
352
+ session.lastPing = Date.now();
353
+ break;
354
+ case 'console':
355
+ this.addInternalConsoleLog(message.payload, sessionId, message.timestamp);
356
+ break;
357
+ case 'network':
358
+ this.addInternalNetworkLog(message.payload, sessionId, message.timestamp);
359
+ break;
360
+ default:
361
+ console.warn(`Unknown message type: ${message.type}`);
362
+ }
363
+ }
364
+ proxyToVite(req, res) {
365
+ const proxyReq = http.request({
366
+ hostname: '127.0.0.1',
367
+ port: VITE_DEV_PORT,
368
+ path: req.url,
369
+ method: req.method,
370
+ headers: req.headers,
371
+ }, (proxyRes) => {
372
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
373
+ proxyRes.pipe(res);
374
+ });
375
+ proxyReq.on('error', () => {
376
+ res.writeHead(502);
377
+ res.end('Vite dev server not ready');
378
+ });
379
+ req.pipe(proxyReq);
380
+ }
381
+ serveStatic(req, res, clientPath) {
382
+ const url = req.url === '/' ? '/index.html' : req.url || '/index.html';
383
+ const filePath = path.join(clientPath, url);
384
+ fs.readFile(filePath, (err, data) => {
385
+ if (err) {
386
+ res.writeHead(404);
387
+ res.end('Not Found');
388
+ return;
389
+ }
390
+ const ext = path.extname(filePath);
391
+ const contentTypes = {
392
+ '.html': 'text/html',
393
+ '.js': 'application/javascript',
394
+ '.css': 'text/css',
395
+ '.svg': 'image/svg+xml',
396
+ '.json': 'application/json',
397
+ };
398
+ res.writeHead(200, {
399
+ 'Content-Type': contentTypes[ext] || 'text/plain',
400
+ });
401
+ res.end(data);
402
+ });
403
+ }
265
404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-cli-devtools",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -8,6 +8,7 @@
8
8
  "gemini-cli-devtools": "dist/src/bin.js"
9
9
  },
10
10
  "scripts": {
11
+ "dev": "concurrently -k \"vite --port 5174\" \"tsx watch src/bin.ts\"",
11
12
  "build": "npm run build:client && tsc --outDir dist/src --module ESNext --target ESNext --moduleResolution bundler src/index.ts src/bin.ts",
12
13
  "build:client": "vite build"
13
14
  },
@@ -19,14 +20,18 @@
19
20
  },
20
21
  "dependencies": {
21
22
  "react": "^18.2.0",
22
- "react-dom": "^18.2.0"
23
+ "react-dom": "^18.2.0",
24
+ "ws": "^8.16.0"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@types/react": "^18.2.0",
26
28
  "@types/react-dom": "^18.2.0",
29
+ "@types/ws": "^8.5.10",
27
30
  "@vitejs/plugin-react": "^4.2.0",
28
31
  "typescript": "^5.3.3",
29
32
  "vite": "^5.0.0",
30
- "@types/node": "^20.11.24"
33
+ "@types/node": "^20.11.24",
34
+ "concurrently": "^8.2.2",
35
+ "tsx": "^4.7.0"
31
36
  }
32
37
  }