network-terminal 1.0.8 → 1.0.10

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