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.
- package/bin/cli.js +476 -362
- 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
|
-
|
|
15
|
+
// Store network logs
|
|
16
|
+
const networkLogs = [];
|
|
17
|
+
let logId = 0;
|
|
16
18
|
|
|
17
|
-
//
|
|
18
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
41
|
+
background: #0a0e17;
|
|
160
42
|
color: #e2e8f0;
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
54
|
+
.title {
|
|
180
55
|
color: #4ade80;
|
|
56
|
+
font-size: 18px;
|
|
181
57
|
font-weight: bold;
|
|
182
58
|
}
|
|
183
|
-
.
|
|
59
|
+
.subtitle {
|
|
60
|
+
color: #64748b;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
}
|
|
63
|
+
.controls {
|
|
184
64
|
display: flex;
|
|
185
65
|
gap: 12px;
|
|
186
66
|
}
|
|
187
|
-
.
|
|
188
|
-
background: #
|
|
189
|
-
border:
|
|
67
|
+
.btn {
|
|
68
|
+
background: #1e293b;
|
|
69
|
+
border: 1px solid #334155;
|
|
190
70
|
color: #94a3b8;
|
|
191
|
-
padding:
|
|
192
|
-
border-radius:
|
|
71
|
+
padding: 8px 16px;
|
|
72
|
+
border-radius: 6px;
|
|
193
73
|
cursor: pointer;
|
|
194
|
-
font-size:
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
font-family: inherit;
|
|
195
76
|
}
|
|
196
|
-
.
|
|
197
|
-
background: #
|
|
77
|
+
.btn:hover {
|
|
78
|
+
background: #334155;
|
|
198
79
|
color: #e2e8f0;
|
|
199
80
|
}
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
padding: 8px 0;
|
|
81
|
+
.container {
|
|
82
|
+
display: flex;
|
|
83
|
+
height: calc(100vh - 60px);
|
|
204
84
|
}
|
|
205
|
-
.
|
|
85
|
+
.panel {
|
|
86
|
+
flex: 1;
|
|
206
87
|
display: flex;
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
align-items: center;
|
|
211
101
|
}
|
|
212
|
-
.
|
|
213
|
-
|
|
102
|
+
.panel-title {
|
|
103
|
+
font-weight: bold;
|
|
104
|
+
font-size: 14px;
|
|
214
105
|
}
|
|
215
|
-
.
|
|
216
|
-
|
|
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
|
-
.
|
|
135
|
+
.log-header {
|
|
220
136
|
display: flex;
|
|
221
137
|
align-items: center;
|
|
222
138
|
gap: 12px;
|
|
223
|
-
|
|
139
|
+
padding: 10px 12px;
|
|
140
|
+
background: #1e293b;
|
|
224
141
|
}
|
|
225
|
-
.
|
|
142
|
+
.method {
|
|
226
143
|
font-weight: bold;
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
.
|
|
244
|
-
.
|
|
245
|
-
.
|
|
246
|
-
.
|
|
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
|
-
.
|
|
254
|
-
color: #64748b;
|
|
169
|
+
.duration {
|
|
255
170
|
font-size: 11px;
|
|
256
|
-
|
|
257
|
-
text-align: right;
|
|
258
|
-
flex-shrink: 0;
|
|
171
|
+
color: #64748b;
|
|
259
172
|
}
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
188
|
+
line-height: 1.5;
|
|
275
189
|
}
|
|
276
|
-
.
|
|
277
|
-
color: #
|
|
278
|
-
font-
|
|
279
|
-
margin-top: 8px;
|
|
280
|
-
margin-bottom: 4px;
|
|
190
|
+
.log-body.empty {
|
|
191
|
+
color: #475569;
|
|
192
|
+
font-style: italic;
|
|
281
193
|
}
|
|
282
|
-
.
|
|
283
|
-
|
|
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
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<div class="
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
renderLogs([]);
|
|
323
|
-
});
|
|
289
|
+
function escapeHtml(str) {
|
|
290
|
+
if (typeof str !== 'string') str = String(str);
|
|
291
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
292
|
+
}
|
|
324
293
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
377
|
+
function clearLogs() {
|
|
378
|
+
logs.length = 0;
|
|
379
|
+
selectedId = null;
|
|
380
|
+
renderLogs();
|
|
381
|
+
fetch('/__network-terminal__/clear', { method: 'POST' });
|
|
382
|
+
}
|
|
344
383
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})();
|
|
390
|
-
</script>
|
|
391
|
-
`;
|
|
460
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
461
|
+
res.end(dashboardHTML);
|
|
462
|
+
});
|
|
392
463
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
464
|
+
// Proxy server
|
|
465
|
+
const proxyServer = http.createServer((req, res) => {
|
|
466
|
+
const startTime = Date.now();
|
|
467
|
+
const currentLogId = ++logId;
|
|
396
468
|
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
//
|
|
426
|
-
|
|
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
|
-
|
|
431
|
-
res.
|
|
531
|
+
// Send response to client
|
|
532
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
533
|
+
res.end(responseBody);
|
|
432
534
|
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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('
|
|
456
|
-
process.exit(1);
|
|
573
|
+
console.error('Proxy server error:', err);
|
|
457
574
|
});
|
|
458
575
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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.
|
|
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",
|