network-terminal 1.0.6 → 1.0.8

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,40 +1,468 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { createServer } = require('vite');
3
+ const http = require('http');
4
+ const fs = require('fs');
4
5
  const path = require('path');
5
6
 
6
7
  const args = process.argv.slice(2);
8
+
9
+ // Parse arguments
7
10
  const portIndex = args.indexOf('--port');
8
- const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3000;
9
-
10
- async function start() {
11
- const standalonePath = path.join(__dirname, '..', 'standalone');
12
-
13
- console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
14
- console.log(' Starting development server...\n');
15
-
16
- try {
17
- const server = await createServer({
18
- configFile: path.join(standalonePath, 'vite.config.js'),
19
- root: standalonePath,
20
- server: {
21
- port,
22
- open: true,
23
- },
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
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
+ };
87
+
88
+ // Intercept XHR
89
+ window.XMLHttpRequest = function() {
90
+ const xhr = new originalXHR();
91
+ const startTime = Date.now();
92
+ let method, url, requestBody;
93
+
94
+ const originalOpen = xhr.open;
95
+ xhr.open = function(m, u, ...rest) {
96
+ method = m;
97
+ url = u;
98
+ return originalOpen.apply(this, [m, u, ...rest]);
99
+ };
100
+
101
+ const originalSend = xhr.send;
102
+ xhr.send = function(body) {
103
+ requestBody = body;
104
+
105
+ xhr.addEventListener('loadend', function() {
106
+ const log = {
107
+ id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
108
+ method: (method || 'GET').toUpperCase(),
109
+ url: url,
110
+ timestamp: new Date().toISOString(),
111
+ requestBody: tryParse(requestBody),
112
+ status: xhr.status,
113
+ responseBody: tryParse(xhr.responseText),
114
+ duration: Date.now() - startTime,
115
+ };
116
+ addLog(log);
117
+ });
118
+
119
+ return originalSend.apply(this, arguments);
120
+ };
121
+
122
+ return xhr;
123
+ };
124
+
125
+ function tryParse(data) {
126
+ if (!data) return null;
127
+ if (typeof data === 'object') return data;
128
+ try {
129
+ return JSON.parse(data);
130
+ } catch (e) {
131
+ return data;
132
+ }
133
+ }
134
+
135
+ function addLog(log) {
136
+ window.__networkLogs.unshift(log);
137
+ if (window.__networkLogs.length > 100) {
138
+ window.__networkLogs.pop();
139
+ }
140
+ if (window.__networkTerminalUpdate) {
141
+ window.__networkTerminalUpdate([...window.__networkLogs]);
142
+ }
143
+ // Also log to console for CLI visibility
144
+ console.log('[Network]', log.method, log.status || '...', log.url, log.duration ? log.duration + 'ms' : '');
145
+ }
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;
158
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
159
+ font-size: 12px;
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));
168
+ }
169
+ .nt-header {
170
+ display: flex;
171
+ justify-content: space-between;
172
+ 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
+ }
187
+ .nt-btn {
188
+ background: #334155;
189
+ border: none;
190
+ color: #94a3b8;
191
+ padding: 4px 8px;
192
+ border-radius: 4px;
193
+ cursor: pointer;
194
+ font-size: 11px;
195
+ }
196
+ .nt-btn:hover {
197
+ background: #475569;
198
+ color: #e2e8f0;
199
+ }
200
+ .nt-logs {
201
+ flex: 1;
202
+ overflow-y: auto;
203
+ padding: 8px 0;
204
+ }
205
+ .nt-log {
206
+ display: flex;
207
+ align-items: flex-start;
208
+ padding: 6px 16px;
209
+ border-bottom: 1px solid #1e293b;
210
+ cursor: pointer;
211
+ }
212
+ .nt-log:hover {
213
+ background: #1e293b;
214
+ }
215
+ .nt-log.expanded {
216
+ flex-direction: column;
217
+ background: #1e293b;
218
+ }
219
+ .nt-log-main {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 12px;
223
+ width: 100%;
224
+ }
225
+ .nt-method {
226
+ 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
+ font-size: 11px;
242
+ }
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 {
247
+ flex: 1;
248
+ overflow: hidden;
249
+ text-overflow: ellipsis;
250
+ white-space: nowrap;
251
+ color: #94a3b8;
252
+ }
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 {
270
+ margin: 0;
271
+ white-space: pre-wrap;
272
+ word-break: break-all;
273
+ color: #cbd5e1;
274
+ font-size: 11px;
275
+ }
276
+ .nt-detail-label {
277
+ color: #4ade80;
278
+ font-weight: bold;
279
+ margin-top: 8px;
280
+ margin-bottom: 4px;
281
+ }
282
+ .nt-detail-label:first-child {
283
+ margin-top: 0;
284
+ }
285
+ .nt-empty {
286
+ text-align: center;
287
+ padding: 40px;
288
+ color: #64748b;
289
+ }
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>
304
+ </div>
305
+ <div class="nt-logs" id="nt-logs">
306
+ <div class="nt-empty">Waiting for network requests...</div>
307
+ </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
+ });
317
+
318
+ // Clear logs
319
+ document.getElementById('nt-clear').addEventListener('click', (e) => {
320
+ e.stopPropagation();
321
+ window.__networkLogs = [];
322
+ renderLogs([]);
24
323
  });
25
324
 
26
- await server.listen();
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
+ }
331
+
332
+ let expandedId = null;
333
+
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;
339
+ }
27
340
 
28
- const actualPort = server.config.server.port;
341
+ container.innerHTML = logs.map(log => {
342
+ const isExpanded = expandedId === log.id;
343
+ const statusClass = !log.status ? 'pending' : log.status < 400 ? 'success' : 'error';
29
344
 
30
- console.log('\x1b[32m%s\x1b[0m', ` Server running at:\n`);
31
- console.log(` > Local: \x1b[36mhttp://localhost:${actualPort}\x1b[0m`);
32
- console.log('\n Press \x1b[33mCtrl+C\x1b[0m to stop\n');
345
+ return \`
346
+ <div class="nt-log \${isExpanded ? 'expanded' : ''}" data-id="\${log.id}">
347
+ <div class="nt-log-main">
348
+ <span class="nt-method \${log.method}">\${log.method}</span>
349
+ <span class="nt-status \${statusClass}">\${log.status || '...'}</span>
350
+ <span class="nt-url">\${log.url}</span>
351
+ <span class="nt-duration">\${log.duration ? log.duration + 'ms' : ''}</span>
352
+ </div>
353
+ \${isExpanded ? \`
354
+ <div class="nt-details">
355
+ \${log.requestBody ? \`<div class="nt-detail-label">Request Body:</div><pre>\${formatJson(log.requestBody)}</pre>\` : ''}
356
+ \${log.responseBody ? \`<div class="nt-detail-label">Response:</div><pre>\${formatJson(log.responseBody)}</pre>\` : ''}
357
+ </div>
358
+ \` : ''}
359
+ </div>
360
+ \`;
361
+ }).join('');
33
362
 
34
- } catch (err) {
35
- console.error('\x1b[31mError starting server:\x1b[0m', err.message);
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
+ }
372
+
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));
379
+ }
380
+ }
381
+
382
+ function escapeHtml(str) {
383
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
384
+ }
385
+
386
+ // Initialize
387
+ createUI();
388
+ window.__networkTerminalUpdate = renderLogs;
389
+ })();
390
+ </script>
391
+ `;
392
+
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');
396
+
397
+ 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
+ };
405
+
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>');
421
+ } else {
422
+ modified = body + injectionScript;
423
+ }
424
+
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
429
+
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
+ });
439
+
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
+ });
445
+
446
+ req.pipe(proxyReq);
447
+ });
448
+
449
+ server.on('error', (err) => {
450
+ if (err.code === 'EADDRINUSE') {
451
+ 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}`);
36
453
  process.exit(1);
37
454
  }
38
- }
455
+ 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');
39
464
 
40
- start();
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "network-terminal",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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",
@@ -23,11 +23,11 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "bin",
26
- "standalone",
27
- "src"
26
+ "standalone-dist"
28
27
  ],
29
28
  "scripts": {
30
- "build": "tsup",
29
+ "build": "tsup && npm run build:standalone",
30
+ "build:standalone": "vite build --config standalone/vite.config.js",
31
31
  "dev": "tsup --watch",
32
32
  "prepublishOnly": "yarn build",
33
33
  "typecheck": "tsc --noEmit"