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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
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
|
}
|