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
|
-
|
|
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
|
-
|
|
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') {
|
|
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.
|
|
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
|
}
|