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 +442 -344
- package/dist/index.js +492 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +499 -59
- package/dist/index.mjs.map +1 -1
- package/dist/vite-plugin.d.mts +4 -8
- package/dist/vite-plugin.d.ts +4 -8
- package/dist/vite-plugin.js +492 -59
- package/dist/vite-plugin.js.map +1 -1
- package/dist/vite-plugin.mjs +499 -59
- package/dist/vite-plugin.mjs.map +1 -1
- package/package.json +2 -1
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
31
|
-
const
|
|
32
|
-
<script type="module">
|
|
33
|
-
// Network Terminal Injection
|
|
22
|
+
// Monitor script that users add to their app
|
|
23
|
+
const monitorScript = `
|
|
34
24
|
(function() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
if (window.__networkTerminalActive) return;
|
|
26
|
+
window.__networkTerminalActive = true;
|
|
27
|
+
|
|
28
|
+
const DASHBOARD_URL = 'http://localhost:${port}';
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
+
var log = {
|
|
55
59
|
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
|
|
56
|
-
method: method
|
|
57
|
-
url:
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
+
sendLog(log);
|
|
79
|
+
}).catch(function() {
|
|
80
|
+
sendLog(log);
|
|
81
|
+
});
|
|
76
82
|
|
|
77
|
-
addLog(log);
|
|
78
83
|
return response;
|
|
79
|
-
}
|
|
84
|
+
}).catch(function(error) {
|
|
80
85
|
log.status = 0;
|
|
81
86
|
log.duration = Date.now() - startTime;
|
|
82
87
|
log.error = error.message;
|
|
83
|
-
|
|
88
|
+
sendLog(log);
|
|
84
89
|
throw error;
|
|
85
|
-
}
|
|
90
|
+
});
|
|
86
91
|
};
|
|
87
92
|
|
|
88
|
-
// Intercept
|
|
93
|
+
// Intercept XMLHttpRequest
|
|
94
|
+
var OriginalXHR = window.XMLHttpRequest;
|
|
89
95
|
window.XMLHttpRequest = function() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let method, url, requestBody;
|
|
96
|
+
var xhr = new OriginalXHR();
|
|
97
|
+
var method, url, requestBody, startTime, logId;
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
xhr.open = function(m, u
|
|
99
|
+
var originalOpen = xhr.open;
|
|
100
|
+
xhr.open = function(m, u) {
|
|
96
101
|
method = m;
|
|
97
102
|
url = u;
|
|
98
|
-
return originalOpen.apply(this,
|
|
103
|
+
return originalOpen.apply(this, arguments);
|
|
99
104
|
};
|
|
100
105
|
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
159
|
+
background: #0a0e17;
|
|
160
160
|
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));
|
|
161
|
+
height: 100vh;
|
|
162
|
+
overflow: hidden;
|
|
168
163
|
}
|
|
169
|
-
.
|
|
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
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
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:
|
|
192
|
-
border-radius:
|
|
179
|
+
padding: 8px 16px;
|
|
180
|
+
border-radius: 6px;
|
|
193
181
|
cursor: pointer;
|
|
194
|
-
font-size:
|
|
195
|
-
|
|
196
|
-
.nt-btn:hover {
|
|
197
|
-
background: #475569;
|
|
198
|
-
color: #e2e8f0;
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-family: inherit;
|
|
199
184
|
}
|
|
200
|
-
.
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
198
|
+
display: flex;
|
|
199
|
+
justify-content: space-between;
|
|
200
|
+
align-items: center;
|
|
211
201
|
}
|
|
212
|
-
.
|
|
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
|
-
.
|
|
216
|
-
|
|
217
|
-
background: #
|
|
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
|
-
.
|
|
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
|
-
|
|
228
|
+
padding: 10px 12px;
|
|
229
|
+
background: #1e293b;
|
|
224
230
|
}
|
|
225
|
-
.
|
|
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
|
-
.
|
|
244
|
-
.
|
|
245
|
-
.
|
|
246
|
-
.
|
|
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
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
262
|
+
line-height: 1.5;
|
|
275
263
|
}
|
|
276
|
-
.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
.
|
|
283
|
-
|
|
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
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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: <script src="http://localhost:${port}/monitor.js"></script></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="
|
|
306
|
-
<div class="
|
|
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"><script src="http://localhost:${port}/monitor.js"></script></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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
330
|
-
}
|
|
355
|
+
function escapeHtml(str) {
|
|
356
|
+
if (typeof str !== 'string') str = String(str);
|
|
357
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
358
|
+
}
|
|
331
359
|
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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"><script src="http://localhost:${port}/monitor.js"></script></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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
439
|
+
function clearLogs() {
|
|
440
|
+
logs.clear();
|
|
441
|
+
selectedId = null;
|
|
442
|
+
renderLogs();
|
|
443
|
+
fetch('/clear', { method: 'POST' });
|
|
444
|
+
}
|
|
385
445
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
});
|