network-terminal 1.0.8 → 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 +476 -362
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,7 +1,6 @@
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);
@@ -11,458 +10,573 @@ const portIndex = args.indexOf('--port');
11
10
  const proxyIndex = args.indexOf('--proxy');
12
11
  const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3001;
13
12
  const targetPort = proxyIndex !== -1 ? parseInt(args[proxyIndex + 1], 10) : 3000;
13
+ const dashboardPort = port + 1000; // Dashboard on separate port
14
14
 
15
- const distPath = path.join(__dirname, '..', 'standalone-dist');
15
+ // Store network logs
16
+ const networkLogs = [];
17
+ let logId = 0;
16
18
 
17
- // Read the built JS file to inline it
18
- let inlineScript = '';
19
- try {
20
- const assetsDir = path.join(distPath, 'assets');
21
- const files = fs.readdirSync(assetsDir);
22
- const jsFile = files.find(f => f.endsWith('.js'));
23
- if (jsFile) {
24
- inlineScript = fs.readFileSync(path.join(assetsDir, jsFile), 'utf-8');
25
- }
26
- } catch (e) {
27
- // Will use CDN fallback
28
- }
29
-
30
- // Script to inject Network Terminal
31
- const injectionScript = `
32
- <script type="module">
33
- // Network Terminal Injection
34
- (function() {
35
- // Create container for Network Terminal
36
- const container = document.createElement('div');
37
- container.id = 'network-terminal-root';
38
- document.body.appendChild(container);
39
-
40
- // Store original fetch and XHR
41
- const originalFetch = window.fetch;
42
- const originalXHR = window.XMLHttpRequest;
43
-
44
- // Network logs storage
45
- window.__networkLogs = [];
46
- window.__networkTerminalUpdate = null;
47
-
48
- // Intercept Fetch
49
- window.fetch = async function(...args) {
50
- const startTime = Date.now();
51
- const [url, options = {}] = args;
52
- const method = options.method || 'GET';
53
-
54
- const log = {
55
- id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
56
- method: method.toUpperCase(),
57
- url: typeof url === 'string' ? url : url.toString(),
58
- timestamp: new Date().toISOString(),
59
- requestBody: options.body ? tryParse(options.body) : null,
60
- status: null,
61
- responseBody: null,
62
- duration: null,
63
- };
64
-
65
- try {
66
- const response = await originalFetch.apply(this, args);
67
- log.status = response.status;
68
- log.duration = Date.now() - startTime;
69
-
70
- // Clone response to read body
71
- const cloned = response.clone();
72
- try {
73
- const text = await cloned.text();
74
- log.responseBody = tryParse(text);
75
- } catch (e) {}
76
-
77
- addLog(log);
78
- return response;
79
- } catch (error) {
80
- log.status = 0;
81
- log.duration = Date.now() - startTime;
82
- log.error = error.message;
83
- addLog(log);
84
- throw error;
85
- }
86
- };
19
+ // SSE clients for real-time updates
20
+ const sseClients = new Set();
87
21
 
88
- // Intercept XHR
89
- window.XMLHttpRequest = function() {
90
- const xhr = new originalXHR();
91
- const startTime = Date.now();
92
- let method, url, requestBody;
93
-
94
- const originalOpen = xhr.open;
95
- xhr.open = function(m, u, ...rest) {
96
- method = m;
97
- url = u;
98
- return originalOpen.apply(this, [m, u, ...rest]);
99
- };
100
-
101
- const originalSend = xhr.send;
102
- xhr.send = function(body) {
103
- requestBody = body;
104
-
105
- xhr.addEventListener('loadend', function() {
106
- const log = {
107
- id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
108
- method: (method || 'GET').toUpperCase(),
109
- url: url,
110
- timestamp: new Date().toISOString(),
111
- requestBody: tryParse(requestBody),
112
- status: xhr.status,
113
- responseBody: tryParse(xhr.responseText),
114
- duration: Date.now() - startTime,
115
- };
116
- addLog(log);
117
- });
118
-
119
- return originalSend.apply(this, arguments);
120
- };
121
-
122
- return xhr;
123
- };
124
-
125
- function tryParse(data) {
126
- if (!data) return null;
127
- if (typeof data === 'object') return data;
128
- try {
129
- return JSON.parse(data);
130
- } catch (e) {
131
- return data;
132
- }
133
- }
134
-
135
- function addLog(log) {
136
- window.__networkLogs.unshift(log);
137
- if (window.__networkLogs.length > 100) {
138
- window.__networkLogs.pop();
139
- }
140
- if (window.__networkTerminalUpdate) {
141
- window.__networkTerminalUpdate([...window.__networkLogs]);
142
- }
143
- // Also log to console for CLI visibility
144
- console.log('[Network]', log.method, log.status || '...', log.url, log.duration ? log.duration + 'ms' : '');
145
- }
22
+ function broadcastLog(log) {
23
+ const data = JSON.stringify(log);
24
+ sseClients.forEach(client => {
25
+ client.write(`data: ${data}\n\n`);
26
+ });
27
+ }
146
28
 
147
- // Styles
148
- const styles = document.createElement('style');
149
- styles.textContent = \`
150
- #network-terminal-panel {
151
- position: fixed;
152
- bottom: 0;
153
- left: 0;
154
- right: 0;
155
- height: 300px;
156
- background: #0f172a;
157
- border-top: 2px solid #4ade80;
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 {
158
40
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
159
- font-size: 12px;
41
+ background: #0a0e17;
160
42
  color: #e2e8f0;
161
- z-index: 999999;
162
- display: flex;
163
- flex-direction: column;
164
- transition: transform 0.3s ease;
165
- }
166
- #network-terminal-panel.collapsed {
167
- transform: translateY(calc(100% - 36px));
43
+ height: 100vh;
44
+ overflow: hidden;
168
45
  }
169
- .nt-header {
46
+ .header {
47
+ background: #0f172a;
48
+ padding: 12px 24px;
49
+ border-bottom: 2px solid #4ade80;
170
50
  display: flex;
171
51
  justify-content: space-between;
172
52
  align-items: center;
173
- padding: 8px 16px;
174
- background: #1e293b;
175
- border-bottom: 1px solid #334155;
176
- cursor: pointer;
177
- user-select: none;
178
53
  }
179
- .nt-title {
54
+ .title {
180
55
  color: #4ade80;
56
+ font-size: 18px;
181
57
  font-weight: bold;
182
58
  }
183
- .nt-controls {
59
+ .subtitle {
60
+ color: #64748b;
61
+ font-size: 12px;
62
+ }
63
+ .controls {
184
64
  display: flex;
185
65
  gap: 12px;
186
66
  }
187
- .nt-btn {
188
- background: #334155;
189
- border: none;
67
+ .btn {
68
+ background: #1e293b;
69
+ border: 1px solid #334155;
190
70
  color: #94a3b8;
191
- padding: 4px 8px;
192
- border-radius: 4px;
71
+ padding: 8px 16px;
72
+ border-radius: 6px;
193
73
  cursor: pointer;
194
- font-size: 11px;
74
+ font-size: 12px;
75
+ font-family: inherit;
195
76
  }
196
- .nt-btn:hover {
197
- background: #475569;
77
+ .btn:hover {
78
+ background: #334155;
198
79
  color: #e2e8f0;
199
80
  }
200
- .nt-logs {
201
- flex: 1;
202
- overflow-y: auto;
203
- padding: 8px 0;
81
+ .container {
82
+ display: flex;
83
+ height: calc(100vh - 60px);
204
84
  }
205
- .nt-log {
85
+ .panel {
86
+ flex: 1;
206
87
  display: flex;
207
- align-items: flex-start;
208
- padding: 6px 16px;
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;
209
97
  border-bottom: 1px solid #1e293b;
210
- cursor: pointer;
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
211
101
  }
212
- .nt-log:hover {
213
- background: #1e293b;
102
+ .panel-title {
103
+ font-weight: bold;
104
+ font-size: 14px;
214
105
  }
215
- .nt-log.expanded {
216
- flex-direction: column;
106
+ .panel-title.request { color: #3b82f6; }
107
+ .panel-title.response { color: #22c55e; }
108
+ .panel-count {
217
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;
218
134
  }
219
- .nt-log-main {
135
+ .log-header {
220
136
  display: flex;
221
137
  align-items: center;
222
138
  gap: 12px;
223
- width: 100%;
139
+ padding: 10px 12px;
140
+ background: #1e293b;
224
141
  }
225
- .nt-method {
142
+ .method {
226
143
  font-weight: bold;
227
- width: 60px;
228
- flex-shrink: 0;
229
- }
230
- .nt-method.GET { color: #22c55e; }
231
- .nt-method.POST { color: #3b82f6; }
232
- .nt-method.PUT { color: #f59e0b; }
233
- .nt-method.DELETE { color: #ef4444; }
234
- .nt-method.PATCH { color: #a855f7; }
235
- .nt-status {
236
- width: 40px;
237
- flex-shrink: 0;
238
- text-align: center;
239
- padding: 2px 6px;
144
+ font-size: 11px;
145
+ padding: 3px 8px;
240
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 {
241
154
  font-size: 11px;
155
+ padding: 3px 8px;
156
+ border-radius: 4px;
242
157
  }
243
- .nt-status.success { background: #166534; color: #4ade80; }
244
- .nt-status.error { background: #991b1b; color: #fca5a5; }
245
- .nt-status.pending { background: #374151; color: #9ca3af; }
246
- .nt-url {
158
+ .status.success { background: #166534; color: #4ade80; }
159
+ .status.error { background: #991b1b; color: #fca5a5; }
160
+ .status.pending { background: #374151; color: #9ca3af; }
161
+ .url {
247
162
  flex: 1;
163
+ font-size: 12px;
164
+ color: #94a3b8;
248
165
  overflow: hidden;
249
166
  text-overflow: ellipsis;
250
167
  white-space: nowrap;
251
- color: #94a3b8;
252
168
  }
253
- .nt-duration {
254
- color: #64748b;
169
+ .duration {
255
170
  font-size: 11px;
256
- width: 60px;
257
- text-align: right;
258
- flex-shrink: 0;
171
+ color: #64748b;
259
172
  }
260
- .nt-details {
261
- margin-top: 8px;
262
- padding: 8px;
263
- background: #0f172a;
264
- border-radius: 4px;
265
- width: 100%;
266
- max-height: 200px;
173
+ .timestamp {
174
+ font-size: 10px;
175
+ color: #475569;
176
+ }
177
+ .log-body {
178
+ padding: 12px;
179
+ font-size: 11px;
180
+ max-height: 300px;
267
181
  overflow: auto;
268
182
  }
269
- .nt-details pre {
183
+ .log-body pre {
270
184
  margin: 0;
271
185
  white-space: pre-wrap;
272
186
  word-break: break-all;
273
187
  color: #cbd5e1;
274
- font-size: 11px;
188
+ line-height: 1.5;
275
189
  }
276
- .nt-detail-label {
277
- color: #4ade80;
278
- font-weight: bold;
279
- margin-top: 8px;
280
- margin-bottom: 4px;
190
+ .log-body.empty {
191
+ color: #475569;
192
+ font-style: italic;
281
193
  }
282
- .nt-detail-label:first-child {
283
- margin-top: 0;
194
+ .empty-state {
195
+ display: flex;
196
+ flex-direction: column;
197
+ align-items: center;
198
+ justify-content: center;
199
+ height: 100%;
200
+ color: #475569;
284
201
  }
285
- .nt-empty {
286
- text-align: center;
287
- padding: 40px;
288
- color: #64748b;
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;
289
219
  }
290
- \`;
291
- document.head.appendChild(styles);
292
-
293
- // Create UI
294
- function createUI() {
295
- const panel = document.createElement('div');
296
- panel.id = 'network-terminal-panel';
297
- panel.innerHTML = \`
298
- <div class="nt-header">
299
- <span class="nt-title">>_ Network Terminal</span>
300
- <div class="nt-controls">
301
- <button class="nt-btn" id="nt-clear">Clear</button>
302
- <button class="nt-btn" id="nt-toggle">_</button>
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>
303
253
  </div>
304
254
  </div>
305
- <div class="nt-logs" id="nt-logs">
306
- <div class="nt-empty">Waiting for network requests...</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>
307
261
  </div>
308
- \`;
309
- document.body.appendChild(panel);
310
-
311
- // Toggle collapse
312
- document.getElementById('nt-toggle').addEventListener('click', (e) => {
313
- e.stopPropagation();
314
- panel.classList.toggle('collapsed');
315
- e.target.textContent = panel.classList.contains('collapsed') ? '▲' : '_';
316
- });
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
+ }
317
288
 
318
- // Clear logs
319
- document.getElementById('nt-clear').addEventListener('click', (e) => {
320
- e.stopPropagation();
321
- window.__networkLogs = [];
322
- renderLogs([]);
323
- });
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
+ }
324
293
 
325
- // Header click to toggle
326
- panel.querySelector('.nt-header').addEventListener('click', () => {
327
- panel.classList.toggle('collapsed');
328
- document.getElementById('nt-toggle').textContent = panel.classList.contains('collapsed') ? '▲' : '_';
329
- });
330
- }
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
+ }
331
299
 
332
- let expandedId = null;
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
+ }
333
371
 
334
- function renderLogs(logs) {
335
- const container = document.getElementById('nt-logs');
336
- if (!logs.length) {
337
- container.innerHTML = '<div class="nt-empty">Waiting for network requests...</div>';
338
- return;
372
+ function selectLog(id) {
373
+ selectedId = selectedId === id ? null : id;
374
+ renderLogs();
339
375
  }
340
376
 
341
- container.innerHTML = logs.map(log => {
342
- const isExpanded = expandedId === log.id;
343
- const statusClass = !log.status ? 'pending' : log.status < 400 ? 'success' : 'error';
377
+ function clearLogs() {
378
+ logs.length = 0;
379
+ selectedId = null;
380
+ renderLogs();
381
+ fetch('/__network-terminal__/clear', { method: 'POST' });
382
+ }
344
383
 
345
- return \`
346
- <div class="nt-log \${isExpanded ? 'expanded' : ''}" data-id="\${log.id}">
347
- <div class="nt-log-main">
348
- <span class="nt-method \${log.method}">\${log.method}</span>
349
- <span class="nt-status \${statusClass}">\${log.status || '...'}</span>
350
- <span class="nt-url">\${log.url}</span>
351
- <span class="nt-duration">\${log.duration ? log.duration + 'ms' : ''}</span>
352
- </div>
353
- \${isExpanded ? \`
354
- <div class="nt-details">
355
- \${log.requestBody ? \`<div class="nt-detail-label">Request Body:</div><pre>\${formatJson(log.requestBody)}</pre>\` : ''}
356
- \${log.responseBody ? \`<div class="nt-detail-label">Response:</div><pre>\${formatJson(log.responseBody)}</pre>\` : ''}
357
- </div>
358
- \` : ''}
359
- </div>
360
- \`;
361
- }).join('');
362
-
363
- // Add click handlers
364
- container.querySelectorAll('.nt-log').forEach(el => {
365
- el.addEventListener('click', () => {
366
- const id = el.dataset.id;
367
- expandedId = expandedId === id ? null : id;
368
- renderLogs(window.__networkLogs);
369
- });
370
- });
371
- }
384
+ function addLog(log) {
385
+ const existingIndex = logs.findIndex(l => l.id === log.id);
386
+ if (existingIndex >= 0) {
387
+ logs[existingIndex] = log;
388
+ } else {
389
+ logs.unshift(log);
390
+ if (logs.length > 100) logs.pop();
391
+ }
392
+ renderLogs();
393
+ }
372
394
 
373
- function formatJson(data) {
374
- if (typeof data === 'string') return escapeHtml(data);
375
- try {
376
- return escapeHtml(JSON.stringify(data, null, 2));
377
- } catch (e) {
378
- return escapeHtml(String(data));
395
+ // SSE connection for real-time updates
396
+ function connect() {
397
+ const evtSource = new EventSource('/__network-terminal__/events');
398
+
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
+ };
379
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;
380
451
  }
381
452
 
382
- function escapeHtml(str) {
383
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
453
+ if (req.url === '/__network-terminal__/clear' && req.method === 'POST') {
454
+ networkLogs.length = 0;
455
+ res.writeHead(200);
456
+ res.end('OK');
457
+ return;
384
458
  }
385
459
 
386
- // Initialize
387
- createUI();
388
- window.__networkTerminalUpdate = renderLogs;
389
- })();
390
- </script>
391
- `;
460
+ res.writeHead(200, { 'Content-Type': 'text/html' });
461
+ res.end(dashboardHTML);
462
+ });
392
463
 
393
- console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal (Proxy Mode)\n');
394
- console.log(` Proxying requests from port ${port} ${targetPort}`);
395
- console.log(' Starting proxy server...\n');
464
+ // Proxy server
465
+ const proxyServer = http.createServer((req, res) => {
466
+ const startTime = Date.now();
467
+ const currentLogId = ++logId;
396
468
 
397
- const server = http.createServer((req, res) => {
398
- const options = {
399
- hostname: 'localhost',
400
- port: targetPort,
401
- path: req.url,
469
+ // Create log entry for request
470
+ const log = {
471
+ id: String(currentLogId),
402
472
  method: req.method,
403
- headers: req.headers,
473
+ url: req.url,
474
+ timestamp: new Date().toISOString(),
475
+ requestBody: null,
476
+ status: null,
477
+ responseBody: null,
478
+ duration: null,
404
479
  };
405
480
 
406
- const proxyReq = http.request(options, (proxyRes) => {
407
- const contentType = proxyRes.headers['content-type'] || '';
408
- const isHtml = contentType.includes('text/html');
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'] || '';
409
509
 
410
- if (isHtml) {
411
- // Collect the response body
412
- let body = '';
413
- proxyRes.on('data', chunk => body += chunk);
510
+ // Collect response body
511
+ let responseBody = '';
512
+ proxyRes.on('data', chunk => responseBody += chunk);
414
513
  proxyRes.on('end', () => {
415
- // Inject the Network Terminal before </body>
416
- let modified = body;
417
- if (body.includes('</body>')) {
418
- modified = body.replace('</body>', injectionScript + '</body>');
419
- } else if (body.includes('</html>')) {
420
- modified = body.replace('</html>', injectionScript + '</html>');
421
- } else {
422
- modified = body + injectionScript;
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);
423
526
  }
424
527
 
425
- // Update content-length
426
- const newHeaders = { ...proxyRes.headers };
427
- delete newHeaders['content-length'];
428
- delete newHeaders['content-encoding']; // Remove encoding since we modified content
528
+ // Broadcast updated log
529
+ broadcastLog(log);
429
530
 
430
- res.writeHead(proxyRes.statusCode, newHeaders);
431
- res.end(modified);
531
+ // Send response to client
532
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
533
+ res.end(responseBody);
432
534
  });
433
- } else {
434
- // Pass through non-HTML responses
435
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
436
- proxyRes.pipe(res);
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);
437
549
  }
550
+ proxyReq.end();
438
551
  });
552
+ });
439
553
 
440
- proxyReq.on('error', (err) => {
441
- console.error(` \x1b[31mProxy error:\x1b[0m ${err.message}`);
442
- res.writeHead(502);
443
- res.end(`Proxy Error: Could not connect to localhost:${targetPort}\n\nMake sure your app is running on port ${targetPort}`);
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');
561
+
562
+ // Open dashboard in browser
563
+ const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
564
+ require('child_process').exec(`${open} http://localhost:${dashboardPort}`);
444
565
  });
445
-
446
- req.pipe(proxyReq);
447
566
  });
448
567
 
449
- server.on('error', (err) => {
568
+ proxyServer.on('error', (err) => {
450
569
  if (err.code === 'EADDRINUSE') {
451
570
  console.error(`\x1b[31m Error: Port ${port} is already in use\x1b[0m`);
452
- console.error(` Try: npx network-terminal --port ${port + 1} --proxy ${targetPort}`);
453
571
  process.exit(1);
454
572
  }
455
- console.error('Server error:', err);
456
- process.exit(1);
573
+ console.error('Proxy server error:', err);
457
574
  });
458
575
 
459
- server.listen(port, () => {
460
- console.log('\x1b[32m%s\x1b[0m', ` Proxy server running!\n`);
461
- console.log(` Open your app at: \x1b[36mhttp://localhost:${port}\x1b[0m`);
462
- console.log(` (instead of http://localhost:${targetPort})\n`);
463
- console.log(' Press \x1b[33mCtrl+C\x1b[0m to stop\n');
464
-
465
- // Open browser
466
- const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
467
- require('child_process').exec(`${open} http://localhost:${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);
468
582
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "network-terminal",
3
- "version": "1.0.8",
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",