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.
- package/bin/cli.js +562 -66
- 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
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
-
|
|
42
|
-
|
|
389
|
+
logs.unshift(log);
|
|
390
|
+
if (logs.length > 100) logs.pop();
|
|
43
391
|
}
|
|
44
|
-
|
|
392
|
+
renderLogs();
|
|
45
393
|
}
|
|
46
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
47
|
-
res.end(content);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
394
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.log(
|
|
71
|
-
console.log(
|
|
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:${
|
|
564
|
+
require('child_process').exec(`${open} http://localhost:${dashboardPort}`);
|
|
76
565
|
});
|
|
77
|
-
}
|
|
566
|
+
});
|
|
78
567
|
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|