network-terminal 1.0.7 → 1.0.9

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.
Files changed (2) hide show
  1. package/bin/cli.js +562 -66
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,86 +1,582 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const http = require('http');
4
- const fs = require('fs');
5
4
  const path = require('path');
6
5
 
7
6
  const args = process.argv.slice(2);
7
+
8
+ // Parse arguments
8
9
  const portIndex = args.indexOf('--port');
9
- let port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3000;
10
-
11
- const distPath = path.join(__dirname, '..', 'standalone-dist');
12
-
13
- const mimeTypes = {
14
- '.html': 'text/html',
15
- '.js': 'application/javascript',
16
- '.css': 'text/css',
17
- '.json': 'application/json',
18
- '.png': 'image/png',
19
- '.svg': 'image/svg+xml',
20
- '.ico': 'image/x-icon',
21
- };
22
-
23
- function serve(filePath, res) {
24
- const ext = path.extname(filePath);
25
- const contentType = mimeTypes[ext] || 'application/octet-stream';
26
-
27
- fs.readFile(filePath, (err, content) => {
28
- if (err) {
29
- if (err.code === 'ENOENT') {
30
- // Serve index.html for SPA routing
31
- fs.readFile(path.join(distPath, 'index.html'), (err2, html) => {
32
- if (err2) {
33
- res.writeHead(500);
34
- res.end('Error loading page');
35
- return;
36
- }
37
- res.writeHead(200, { 'Content-Type': 'text/html' });
38
- res.end(html);
39
- });
10
+ const proxyIndex = args.indexOf('--proxy');
11
+ const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3001;
12
+ const targetPort = proxyIndex !== -1 ? parseInt(args[proxyIndex + 1], 10) : 3000;
13
+ const dashboardPort = port + 1000; // Dashboard on separate port
14
+
15
+ // Store network logs
16
+ const networkLogs = [];
17
+ let logId = 0;
18
+
19
+ // SSE clients for real-time updates
20
+ const sseClients = new Set();
21
+
22
+ function broadcastLog(log) {
23
+ const data = JSON.stringify(log);
24
+ sseClients.forEach(client => {
25
+ client.write(`data: ${data}\n\n`);
26
+ });
27
+ }
28
+
29
+ // Dashboard HTML with two terminals
30
+ const dashboardHTML = `
31
+ <!DOCTYPE html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="UTF-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
+ <title>Network Terminal</title>
37
+ <style>
38
+ * { margin: 0; padding: 0; box-sizing: border-box; }
39
+ body {
40
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
41
+ background: #0a0e17;
42
+ color: #e2e8f0;
43
+ height: 100vh;
44
+ overflow: hidden;
45
+ }
46
+ .header {
47
+ background: #0f172a;
48
+ padding: 12px 24px;
49
+ border-bottom: 2px solid #4ade80;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ }
54
+ .title {
55
+ color: #4ade80;
56
+ font-size: 18px;
57
+ font-weight: bold;
58
+ }
59
+ .subtitle {
60
+ color: #64748b;
61
+ font-size: 12px;
62
+ }
63
+ .controls {
64
+ display: flex;
65
+ gap: 12px;
66
+ }
67
+ .btn {
68
+ background: #1e293b;
69
+ border: 1px solid #334155;
70
+ color: #94a3b8;
71
+ padding: 8px 16px;
72
+ border-radius: 6px;
73
+ cursor: pointer;
74
+ font-size: 12px;
75
+ font-family: inherit;
76
+ }
77
+ .btn:hover {
78
+ background: #334155;
79
+ color: #e2e8f0;
80
+ }
81
+ .container {
82
+ display: flex;
83
+ height: calc(100vh - 60px);
84
+ }
85
+ .panel {
86
+ flex: 1;
87
+ display: flex;
88
+ flex-direction: column;
89
+ border-right: 1px solid #1e293b;
90
+ }
91
+ .panel:last-child {
92
+ border-right: none;
93
+ }
94
+ .panel-header {
95
+ background: #0f172a;
96
+ padding: 12px 16px;
97
+ border-bottom: 1px solid #1e293b;
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ }
102
+ .panel-title {
103
+ font-weight: bold;
104
+ font-size: 14px;
105
+ }
106
+ .panel-title.request { color: #3b82f6; }
107
+ .panel-title.response { color: #22c55e; }
108
+ .panel-count {
109
+ background: #1e293b;
110
+ padding: 2px 8px;
111
+ border-radius: 10px;
112
+ font-size: 11px;
113
+ color: #64748b;
114
+ }
115
+ .logs {
116
+ flex: 1;
117
+ overflow-y: auto;
118
+ padding: 8px;
119
+ }
120
+ .log-entry {
121
+ background: #0f172a;
122
+ border: 1px solid #1e293b;
123
+ border-radius: 8px;
124
+ margin-bottom: 8px;
125
+ overflow: hidden;
126
+ cursor: pointer;
127
+ transition: border-color 0.2s;
128
+ }
129
+ .log-entry:hover {
130
+ border-color: #334155;
131
+ }
132
+ .log-entry.selected {
133
+ border-color: #4ade80;
134
+ }
135
+ .log-header {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 12px;
139
+ padding: 10px 12px;
140
+ background: #1e293b;
141
+ }
142
+ .method {
143
+ font-weight: bold;
144
+ font-size: 11px;
145
+ padding: 3px 8px;
146
+ border-radius: 4px;
147
+ }
148
+ .method.GET { background: #166534; color: #4ade80; }
149
+ .method.POST { background: #1e40af; color: #60a5fa; }
150
+ .method.PUT { background: #a16207; color: #fcd34d; }
151
+ .method.DELETE { background: #991b1b; color: #fca5a5; }
152
+ .method.PATCH { background: #7c3aed; color: #c4b5fd; }
153
+ .status {
154
+ font-size: 11px;
155
+ padding: 3px 8px;
156
+ border-radius: 4px;
157
+ }
158
+ .status.success { background: #166534; color: #4ade80; }
159
+ .status.error { background: #991b1b; color: #fca5a5; }
160
+ .status.pending { background: #374151; color: #9ca3af; }
161
+ .url {
162
+ flex: 1;
163
+ font-size: 12px;
164
+ color: #94a3b8;
165
+ overflow: hidden;
166
+ text-overflow: ellipsis;
167
+ white-space: nowrap;
168
+ }
169
+ .duration {
170
+ font-size: 11px;
171
+ color: #64748b;
172
+ }
173
+ .timestamp {
174
+ font-size: 10px;
175
+ color: #475569;
176
+ }
177
+ .log-body {
178
+ padding: 12px;
179
+ font-size: 11px;
180
+ max-height: 300px;
181
+ overflow: auto;
182
+ }
183
+ .log-body pre {
184
+ margin: 0;
185
+ white-space: pre-wrap;
186
+ word-break: break-all;
187
+ color: #cbd5e1;
188
+ line-height: 1.5;
189
+ }
190
+ .log-body.empty {
191
+ color: #475569;
192
+ font-style: italic;
193
+ }
194
+ .empty-state {
195
+ display: flex;
196
+ flex-direction: column;
197
+ align-items: center;
198
+ justify-content: center;
199
+ height: 100%;
200
+ color: #475569;
201
+ }
202
+ .empty-state svg {
203
+ width: 48px;
204
+ height: 48px;
205
+ margin-bottom: 16px;
206
+ opacity: 0.5;
207
+ }
208
+ .connection-status {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 8px;
212
+ font-size: 12px;
213
+ }
214
+ .status-dot {
215
+ width: 8px;
216
+ height: 8px;
217
+ border-radius: 50%;
218
+ background: #22c55e;
219
+ }
220
+ .status-dot.disconnected {
221
+ background: #ef4444;
222
+ }
223
+ </style>
224
+ </head>
225
+ <body>
226
+ <div class="header">
227
+ <div>
228
+ <div class="title">>_ Network Terminal</div>
229
+ <div class="subtitle">Monitoring localhost:${targetPort} via proxy :${port}</div>
230
+ </div>
231
+ <div class="controls">
232
+ <div class="connection-status">
233
+ <div class="status-dot" id="statusDot"></div>
234
+ <span id="statusText">Connected</span>
235
+ </div>
236
+ <button class="btn" onclick="clearLogs()">Clear All</button>
237
+ </div>
238
+ </div>
239
+
240
+ <div class="container">
241
+ <div class="panel">
242
+ <div class="panel-header">
243
+ <span class="panel-title request">⬆ REQUESTS</span>
244
+ <span class="panel-count" id="requestCount">0</span>
245
+ </div>
246
+ <div class="logs" id="requestLogs">
247
+ <div class="empty-state">
248
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
249
+ <path d="M12 19V5M5 12l7-7 7 7"/>
250
+ </svg>
251
+ <div>Waiting for requests...</div>
252
+ <div style="margin-top: 8px; font-size: 11px;">Make requests in your app at localhost:${port}</div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <div class="panel">
258
+ <div class="panel-header">
259
+ <span class="panel-title response">⬇ RESPONSES</span>
260
+ <span class="panel-count" id="responseCount">0</span>
261
+ </div>
262
+ <div class="logs" id="responseLogs">
263
+ <div class="empty-state">
264
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
265
+ <path d="M12 5v14M5 12l7 7 7-7"/>
266
+ </svg>
267
+ <div>Waiting for responses...</div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <script>
274
+ const logs = [];
275
+ let selectedId = null;
276
+
277
+ function formatJson(data) {
278
+ if (!data) return null;
279
+ if (typeof data === 'string') {
280
+ try {
281
+ data = JSON.parse(data);
282
+ } catch (e) {
283
+ return escapeHtml(data);
284
+ }
285
+ }
286
+ return escapeHtml(JSON.stringify(data, null, 2));
287
+ }
288
+
289
+ function escapeHtml(str) {
290
+ if (typeof str !== 'string') str = String(str);
291
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
292
+ }
293
+
294
+ function formatTime(timestamp) {
295
+ const date = new Date(timestamp);
296
+ return date.toLocaleTimeString('en-US', { hour12: false }) + '.' +
297
+ String(date.getMilliseconds()).padStart(3, '0');
298
+ }
299
+
300
+ function renderLogs() {
301
+ const requestContainer = document.getElementById('requestLogs');
302
+ const responseContainer = document.getElementById('responseLogs');
303
+
304
+ document.getElementById('requestCount').textContent = logs.length;
305
+ document.getElementById('responseCount').textContent = logs.filter(l => l.status).length;
306
+
307
+ if (logs.length === 0) {
308
+ requestContainer.innerHTML = \`
309
+ <div class="empty-state">
310
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
311
+ <path d="M12 19V5M5 12l7-7 7 7"/>
312
+ </svg>
313
+ <div>Waiting for requests...</div>
314
+ <div style="margin-top: 8px; font-size: 11px;">Make requests in your app at localhost:${port}</div>
315
+ </div>
316
+ \`;
317
+ responseContainer.innerHTML = \`
318
+ <div class="empty-state">
319
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
320
+ <path d="M12 5v14M5 12l7 7 7-7"/>
321
+ </svg>
322
+ <div>Waiting for responses...</div>
323
+ </div>
324
+ \`;
325
+ return;
326
+ }
327
+
328
+ // Render requests
329
+ requestContainer.innerHTML = logs.map(log => {
330
+ const isSelected = selectedId === log.id;
331
+ const bodyContent = formatJson(log.requestBody);
332
+
333
+ return \`
334
+ <div class="log-entry \${isSelected ? 'selected' : ''}" onclick="selectLog('\${log.id}')">
335
+ <div class="log-header">
336
+ <span class="method \${log.method}">\${log.method}</span>
337
+ <span class="url">\${log.url}</span>
338
+ <span class="timestamp">\${formatTime(log.timestamp)}</span>
339
+ </div>
340
+ \${isSelected ? \`
341
+ <div class="log-body \${!bodyContent ? 'empty' : ''}">
342
+ \${bodyContent ? \`<pre>\${bodyContent}</pre>\` : 'No request body'}
343
+ </div>
344
+ \` : ''}
345
+ </div>
346
+ \`;
347
+ }).join('');
348
+
349
+ // Render responses
350
+ responseContainer.innerHTML = logs.map(log => {
351
+ const isSelected = selectedId === log.id;
352
+ const statusClass = !log.status ? 'pending' : log.status < 400 ? 'success' : 'error';
353
+ const bodyContent = formatJson(log.responseBody);
354
+
355
+ return \`
356
+ <div class="log-entry \${isSelected ? 'selected' : ''}" onclick="selectLog('\${log.id}')">
357
+ <div class="log-header">
358
+ <span class="status \${statusClass}">\${log.status || '...'}</span>
359
+ <span class="url">\${log.url}</span>
360
+ <span class="duration">\${log.duration ? log.duration + 'ms' : ''}</span>
361
+ </div>
362
+ \${isSelected ? \`
363
+ <div class="log-body \${!bodyContent ? 'empty' : ''}">
364
+ \${bodyContent ? \`<pre>\${bodyContent}</pre>\` : log.status ? 'No response body' : 'Pending...'}
365
+ </div>
366
+ \` : ''}
367
+ </div>
368
+ \`;
369
+ }).join('');
370
+ }
371
+
372
+ function selectLog(id) {
373
+ selectedId = selectedId === id ? null : id;
374
+ renderLogs();
375
+ }
376
+
377
+ function clearLogs() {
378
+ logs.length = 0;
379
+ selectedId = null;
380
+ renderLogs();
381
+ fetch('/__network-terminal__/clear', { method: 'POST' });
382
+ }
383
+
384
+ function addLog(log) {
385
+ const existingIndex = logs.findIndex(l => l.id === log.id);
386
+ if (existingIndex >= 0) {
387
+ logs[existingIndex] = log;
40
388
  } else {
41
- res.writeHead(500);
42
- res.end('Server error');
389
+ logs.unshift(log);
390
+ if (logs.length > 100) logs.pop();
43
391
  }
44
- return;
392
+ renderLogs();
45
393
  }
46
- res.writeHead(200, { 'Content-Type': contentType });
47
- res.end(content);
48
- });
49
- }
50
394
 
51
- function startServer(port) {
52
- const server = http.createServer((req, res) => {
53
- let filePath = path.join(distPath, req.url === '/' ? 'index.html' : req.url);
54
- serve(filePath, res);
55
- });
395
+ // SSE connection for real-time updates
396
+ function connect() {
397
+ const evtSource = new EventSource('/__network-terminal__/events');
56
398
 
57
- server.on('error', (err) => {
58
- if (err.code === 'EADDRINUSE') {
59
- console.log(` Port ${port} is in use, trying ${port + 1}...`);
60
- startServer(port + 1);
61
- } else {
62
- console.error('Server error:', err);
63
- process.exit(1);
399
+ evtSource.onopen = () => {
400
+ document.getElementById('statusDot').classList.remove('disconnected');
401
+ document.getElementById('statusText').textContent = 'Connected';
402
+ };
403
+
404
+ evtSource.onmessage = (event) => {
405
+ const log = JSON.parse(event.data);
406
+ addLog(log);
407
+ };
408
+
409
+ evtSource.onerror = () => {
410
+ document.getElementById('statusDot').classList.add('disconnected');
411
+ document.getElementById('statusText').textContent = 'Reconnecting...';
412
+ evtSource.close();
413
+ setTimeout(connect, 2000);
414
+ };
64
415
  }
416
+
417
+ connect();
418
+ renderLogs();
419
+ </script>
420
+ </body>
421
+ </html>
422
+ `;
423
+
424
+ console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
425
+ console.log(` Proxy: http://localhost:${port} → localhost:${targetPort}`);
426
+ console.log(` Dashboard: http://localhost:${dashboardPort}\n`);
427
+ console.log(' Starting servers...\n');
428
+
429
+ // Dashboard server
430
+ const dashboardServer = http.createServer((req, res) => {
431
+ if (req.url === '/__network-terminal__/events') {
432
+ // SSE endpoint
433
+ res.writeHead(200, {
434
+ 'Content-Type': 'text/event-stream',
435
+ 'Cache-Control': 'no-cache',
436
+ 'Connection': 'keep-alive',
437
+ 'Access-Control-Allow-Origin': '*',
438
+ });
439
+
440
+ sseClients.add(res);
441
+
442
+ // Send existing logs
443
+ networkLogs.forEach(log => {
444
+ res.write(`data: ${JSON.stringify(log)}\n\n`);
445
+ });
446
+
447
+ req.on('close', () => {
448
+ sseClients.delete(res);
449
+ });
450
+ return;
451
+ }
452
+
453
+ if (req.url === '/__network-terminal__/clear' && req.method === 'POST') {
454
+ networkLogs.length = 0;
455
+ res.writeHead(200);
456
+ res.end('OK');
457
+ return;
458
+ }
459
+
460
+ res.writeHead(200, { 'Content-Type': 'text/html' });
461
+ res.end(dashboardHTML);
462
+ });
463
+
464
+ // Proxy server
465
+ const proxyServer = http.createServer((req, res) => {
466
+ const startTime = Date.now();
467
+ const currentLogId = ++logId;
468
+
469
+ // Create log entry for request
470
+ const log = {
471
+ id: String(currentLogId),
472
+ method: req.method,
473
+ url: req.url,
474
+ timestamp: new Date().toISOString(),
475
+ requestBody: null,
476
+ status: null,
477
+ responseBody: null,
478
+ duration: null,
479
+ };
480
+
481
+ // Collect request body
482
+ let requestBody = '';
483
+ req.on('data', chunk => requestBody += chunk);
484
+ req.on('end', () => {
485
+ if (requestBody) {
486
+ try {
487
+ log.requestBody = JSON.parse(requestBody);
488
+ } catch (e) {
489
+ log.requestBody = requestBody;
490
+ }
491
+ }
492
+
493
+ // Add to logs and broadcast
494
+ networkLogs.unshift(log);
495
+ if (networkLogs.length > 100) networkLogs.pop();
496
+ broadcastLog(log);
497
+
498
+ // Forward request to target
499
+ const options = {
500
+ hostname: 'localhost',
501
+ port: targetPort,
502
+ path: req.url,
503
+ method: req.method,
504
+ headers: req.headers,
505
+ };
506
+
507
+ const proxyReq = http.request(options, (proxyRes) => {
508
+ const contentType = proxyRes.headers['content-type'] || '';
509
+
510
+ // Collect response body
511
+ let responseBody = '';
512
+ proxyRes.on('data', chunk => responseBody += chunk);
513
+ proxyRes.on('end', () => {
514
+ // Update log with response
515
+ log.status = proxyRes.statusCode;
516
+ log.duration = Date.now() - startTime;
517
+
518
+ if (responseBody && contentType.includes('application/json')) {
519
+ try {
520
+ log.responseBody = JSON.parse(responseBody);
521
+ } catch (e) {
522
+ log.responseBody = responseBody;
523
+ }
524
+ } else if (responseBody && !contentType.includes('text/html')) {
525
+ log.responseBody = responseBody.substring(0, 1000);
526
+ }
527
+
528
+ // Broadcast updated log
529
+ broadcastLog(log);
530
+
531
+ // Send response to client
532
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
533
+ res.end(responseBody);
534
+ });
535
+ });
536
+
537
+ proxyReq.on('error', (err) => {
538
+ log.status = 502;
539
+ log.duration = Date.now() - startTime;
540
+ log.error = err.message;
541
+ broadcastLog(log);
542
+
543
+ res.writeHead(502);
544
+ res.end(`Proxy Error: ${err.message}`);
545
+ });
546
+
547
+ if (requestBody) {
548
+ proxyReq.write(requestBody);
549
+ }
550
+ proxyReq.end();
65
551
  });
552
+ });
66
553
 
67
- server.listen(port, () => {
68
- console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
69
- console.log('\x1b[32m%s\x1b[0m', ` Server running at:\n`);
70
- console.log(` > Local: \x1b[36mhttp://localhost:${port}\x1b[0m`);
71
- console.log('\n Press \x1b[33mCtrl+C\x1b[0m to stop\n');
554
+ // Start servers
555
+ dashboardServer.listen(dashboardPort, () => {
556
+ proxyServer.listen(port, () => {
557
+ console.log('\x1b[32m%s\x1b[0m', ' Servers running!\n');
558
+ console.log(` Your app: \x1b[36mhttp://localhost:${port}\x1b[0m`);
559
+ console.log(` Dashboard: \x1b[36mhttp://localhost:${dashboardPort}\x1b[0m\n`);
560
+ console.log(' Press \x1b[33mCtrl+C\x1b[0m to stop\n');
72
561
 
73
- // Open browser
562
+ // Open dashboard in browser
74
563
  const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
75
- require('child_process').exec(`${open} http://localhost:${port}`);
564
+ require('child_process').exec(`${open} http://localhost:${dashboardPort}`);
76
565
  });
77
- }
566
+ });
78
567
 
79
- // Check if standalone-dist exists
80
- if (!fs.existsSync(distPath)) {
81
- console.error('\x1b[31mError: standalone-dist folder not found.\x1b[0m');
82
- console.error('Please rebuild the package with: npm run build');
83
- process.exit(1);
84
- }
568
+ proxyServer.on('error', (err) => {
569
+ if (err.code === 'EADDRINUSE') {
570
+ console.error(`\x1b[31m Error: Port ${port} is already in use\x1b[0m`);
571
+ process.exit(1);
572
+ }
573
+ console.error('Proxy server error:', err);
574
+ });
85
575
 
86
- startServer(port);
576
+ dashboardServer.on('error', (err) => {
577
+ if (err.code === 'EADDRINUSE') {
578
+ console.error(`\x1b[31m Error: Port ${dashboardPort} is already in use\x1b[0m`);
579
+ process.exit(1);
580
+ }
581
+ console.error('Dashboard server error:', err);
582
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "network-terminal",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A browser-based terminal UI for monitoring Fetch/XHR requests with real-time display of routes, payloads, and responses",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",