gemini-cli-devtools 0.0.1 → 0.1.1

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,15 @@ 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
+ // Check if running in dev mode by looking for src directory (not in npm package)
15
+ const isDev = (() => {
16
+ const srcDir = path.dirname(fileURLToPath(import.meta.url));
17
+ // In dev: src/index.ts -> srcDir is "src"
18
+ // In prod: dist/src/index.js -> srcDir is "dist/src"
19
+ return srcDir.endsWith('/src') && !srcDir.includes('/dist/');
20
+ })();
12
21
  /**
13
22
  * DevTools Viewer
14
23
  *
@@ -19,6 +28,8 @@ export class DevTools extends EventEmitter {
19
28
  logs = [];
20
29
  consoleLogs = [];
21
30
  server = null;
31
+ wss = null;
32
+ sessions = new Map();
22
33
  port = 25417;
23
34
  watchedFiles = new Map(); // filePath -> lastSize
24
35
  constructor() {
@@ -180,7 +191,14 @@ export class DevTools extends EventEmitter {
180
191
  return `http://127.0.0.1:${this.port}`;
181
192
  }
182
193
  getClientPath() {
183
- return path.join(path.dirname(fileURLToPath(import.meta.url)), '../client');
194
+ const srcDir = path.dirname(fileURLToPath(import.meta.url));
195
+ // When running with tsx from src/, look for dist/client
196
+ // When running compiled from dist/src/, look for ../client
197
+ const distClient = path.join(srcDir, '../dist/client');
198
+ if (fs.existsSync(distClient)) {
199
+ return distClient;
200
+ }
201
+ return path.join(srcDir, '../client');
184
202
  }
185
203
  start() {
186
204
  return new Promise((resolve) => {
@@ -191,40 +209,8 @@ export class DevTools extends EventEmitter {
191
209
  const clientPath = this.getClientPath();
192
210
  this.server = http.createServer((req, res) => {
193
211
  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') {
212
+ // API routes
213
+ if (req.url === '/logs') {
228
214
  res.writeHead(200, { 'Content-Type': 'application/json' });
229
215
  res.end(JSON.stringify(this.logs));
230
216
  }
@@ -232,6 +218,11 @@ export class DevTools extends EventEmitter {
232
218
  res.writeHead(200, { 'Content-Type': 'application/json' });
233
219
  res.end(JSON.stringify(this.consoleLogs));
234
220
  }
221
+ else if (req.url === '/sessions') {
222
+ res.writeHead(200, { 'Content-Type': 'application/json' });
223
+ const sessionIds = Array.from(this.sessions.keys());
224
+ res.end(JSON.stringify(sessionIds));
225
+ }
235
226
  else if (req.url === '/events') {
236
227
  res.writeHead(200, {
237
228
  'Content-Type': 'text/event-stream',
@@ -240,13 +231,45 @@ export class DevTools extends EventEmitter {
240
231
  });
241
232
  const l1 = () => res.write(`event: update\ndata: \n\n`);
242
233
  const l2 = () => res.write(`event: console-update\ndata: \n\n`);
234
+ const l3 = () => res.write(`event: session-update\ndata: \n\n`);
243
235
  this.on('update', l1);
244
236
  this.on('console-update', l2);
237
+ this.on('session-update', l3);
245
238
  req.on('close', () => {
246
239
  this.off('update', l1);
247
240
  this.off('console-update', l2);
241
+ this.off('session-update', l3);
248
242
  });
249
243
  }
244
+ else if (req.method === 'POST' && req.url === '/ingest') {
245
+ let body = '';
246
+ req.on('data', (chunk) => (body += chunk));
247
+ req.on('end', () => {
248
+ try {
249
+ const entry = JSON.parse(body);
250
+ if (entry.type === 'console') {
251
+ this.addInternalConsoleLog(entry.payload, entry.sessionId, entry.timestamp);
252
+ }
253
+ else if (entry.type === 'network') {
254
+ this.addInternalNetworkLog(entry.payload, entry.sessionId, entry.timestamp);
255
+ }
256
+ res.writeHead(200);
257
+ res.end('OK');
258
+ }
259
+ catch {
260
+ res.writeHead(400);
261
+ res.end('Invalid JSON');
262
+ }
263
+ });
264
+ }
265
+ else if (isDev) {
266
+ // Dev mode: proxy to Vite
267
+ this.proxyToVite(req, res);
268
+ }
269
+ else {
270
+ // Production: serve static files
271
+ this.serveStatic(req, res, clientPath);
272
+ }
250
273
  });
251
274
  this.server.on('error', (e) => {
252
275
  if (typeof e === 'object' &&
@@ -258,8 +281,130 @@ export class DevTools extends EventEmitter {
258
281
  }
259
282
  });
260
283
  this.server.listen(this.port, '127.0.0.1', () => {
284
+ this.setupWebSocketServer();
261
285
  resolve(this.getUrl());
262
286
  });
263
287
  });
264
288
  }
289
+ setupWebSocketServer() {
290
+ if (!this.server)
291
+ return;
292
+ this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
293
+ this.wss.on('connection', (ws) => {
294
+ let sessionId = null;
295
+ ws.on('message', (data) => {
296
+ try {
297
+ const message = JSON.parse(data.toString());
298
+ // Handle registration first
299
+ if (message.type === 'register') {
300
+ sessionId = message.sessionId;
301
+ this.sessions.set(sessionId, {
302
+ sessionId,
303
+ ws,
304
+ lastPing: Date.now(),
305
+ });
306
+ console.log(`📡 WebSocket registered: ${sessionId}`);
307
+ // Notify session update
308
+ this.emit('session-update');
309
+ // Send registration acknowledgement
310
+ ws.send(JSON.stringify({
311
+ type: 'registered',
312
+ sessionId,
313
+ timestamp: Date.now(),
314
+ }));
315
+ }
316
+ else if (sessionId) {
317
+ this.handleWebSocketMessage(sessionId, message);
318
+ }
319
+ }
320
+ catch (err) {
321
+ console.error('Invalid WebSocket message:', err);
322
+ }
323
+ });
324
+ ws.on('close', () => {
325
+ if (sessionId) {
326
+ console.log(`📡 WebSocket disconnected: ${sessionId}`);
327
+ this.sessions.delete(sessionId);
328
+ this.emit('session-disconnected', sessionId);
329
+ this.emit('session-update');
330
+ }
331
+ });
332
+ ws.on('error', (err) => {
333
+ console.error(`WebSocket error:`, err);
334
+ });
335
+ });
336
+ // Heartbeat mechanism
337
+ setInterval(() => {
338
+ const now = Date.now();
339
+ this.sessions.forEach((session, sessionId) => {
340
+ if (now - session.lastPing > 30000) {
341
+ console.log(`⚠️ Session ${sessionId} timeout, closing...`);
342
+ session.ws.close();
343
+ this.sessions.delete(sessionId);
344
+ }
345
+ else {
346
+ // Send ping
347
+ session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
348
+ }
349
+ });
350
+ }, 10000);
351
+ }
352
+ handleWebSocketMessage(sessionId, message) {
353
+ const session = this.sessions.get(sessionId);
354
+ if (!session)
355
+ return;
356
+ switch (message.type) {
357
+ case 'pong':
358
+ session.lastPing = Date.now();
359
+ break;
360
+ case 'console':
361
+ this.addInternalConsoleLog(message.payload, sessionId, message.timestamp);
362
+ break;
363
+ case 'network':
364
+ this.addInternalNetworkLog(message.payload, sessionId, message.timestamp);
365
+ break;
366
+ default:
367
+ console.warn(`Unknown message type: ${message.type}`);
368
+ }
369
+ }
370
+ proxyToVite(req, res) {
371
+ const proxyReq = http.request({
372
+ hostname: '127.0.0.1',
373
+ port: VITE_DEV_PORT,
374
+ path: req.url,
375
+ method: req.method,
376
+ headers: req.headers,
377
+ }, (proxyRes) => {
378
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
379
+ proxyRes.pipe(res);
380
+ });
381
+ proxyReq.on('error', () => {
382
+ res.writeHead(502);
383
+ res.end('Vite dev server not ready');
384
+ });
385
+ req.pipe(proxyReq);
386
+ }
387
+ serveStatic(req, res, clientPath) {
388
+ const url = req.url === '/' ? '/index.html' : req.url || '/index.html';
389
+ const filePath = path.join(clientPath, url);
390
+ fs.readFile(filePath, (err, data) => {
391
+ if (err) {
392
+ res.writeHead(404);
393
+ res.end('Not Found');
394
+ return;
395
+ }
396
+ const ext = path.extname(filePath);
397
+ const contentTypes = {
398
+ '.html': 'text/html',
399
+ '.js': 'application/javascript',
400
+ '.css': 'text/css',
401
+ '.svg': 'image/svg+xml',
402
+ '.json': 'application/json',
403
+ };
404
+ res.writeHead(200, {
405
+ 'Content-Type': contentTypes[ext] || 'text/plain',
406
+ });
407
+ res.end(data);
408
+ });
409
+ }
265
410
  }
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.1",
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
  }