network-terminal 1.0.7 → 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.
Files changed (2) hide show
  1. package/bin/cli.js +445 -63
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -5,82 +5,464 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const args = process.argv.slice(2);
8
+
9
+ // Parse arguments
8
10
  const portIndex = args.indexOf('--port');
9
- let port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3000;
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;
10
14
 
11
15
  const distPath = path.join(__dirname, '..', 'standalone-dist');
12
16
 
13
- const mimeTypes = {
14
- '.html': 'text/html',
15
- '.js': 'application/javascript',
16
- '.css': 'text/css',
17
- '.json': 'application/json',
18
- '.png': 'image/png',
19
- '.svg': 'image/svg+xml',
20
- '.ico': 'image/x-icon',
21
- };
22
-
23
- function serve(filePath, res) {
24
- const ext = path.extname(filePath);
25
- const contentType = mimeTypes[ext] || 'application/octet-stream';
26
-
27
- fs.readFile(filePath, (err, content) => {
28
- if (err) {
29
- if (err.code === 'ENOENT') {
30
- // Serve index.html for SPA routing
31
- fs.readFile(path.join(distPath, 'index.html'), (err2, html) => {
32
- if (err2) {
33
- res.writeHead(500);
34
- res.end('Error loading page');
35
- return;
36
- }
37
- res.writeHead(200, { 'Content-Type': 'text/html' });
38
- res.end(html);
39
- });
40
- } else {
41
- res.writeHead(500);
42
- res.end('Server error');
43
- }
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([]);
323
+ });
324
+
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>';
44
338
  return;
45
339
  }
46
- res.writeHead(200, { 'Content-Type': contentType });
47
- res.end(content);
48
- });
49
- }
50
340
 
51
- function startServer(port) {
52
- const server = http.createServer((req, res) => {
53
- let filePath = path.join(distPath, req.url === '/' ? 'index.html' : req.url);
54
- serve(filePath, res);
55
- });
341
+ container.innerHTML = logs.map(log => {
342
+ const isExpanded = expandedId === log.id;
343
+ const statusClass = !log.status ? 'pending' : log.status < 400 ? 'success' : 'error';
344
+
345
+ return \`
346
+ <div class="nt-log \${isExpanded ? 'expanded' : ''}" data-id="\${log.id}">
347
+ <div class="nt-log-main">
348
+ <span class="nt-method \${log.method}">\${log.method}</span>
349
+ <span class="nt-status \${statusClass}">\${log.status || '...'}</span>
350
+ <span class="nt-url">\${log.url}</span>
351
+ <span class="nt-duration">\${log.duration ? log.duration + 'ms' : ''}</span>
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('');
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
+ }
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
+ };
56
405
 
57
- server.on('error', (err) => {
58
- if (err.code === 'EADDRINUSE') {
59
- console.log(` Port ${port} is in use, trying ${port + 1}...`);
60
- startServer(port + 1);
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
+ });
61
433
  } else {
62
- console.error('Server error:', err);
63
- process.exit(1);
434
+ // Pass through non-HTML responses
435
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
436
+ proxyRes.pipe(res);
64
437
  }
65
438
  });
66
439
 
67
- server.listen(port, () => {
68
- console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
69
- console.log('\x1b[32m%s\x1b[0m', ` Server running at:\n`);
70
- console.log(` > Local: \x1b[36mhttp://localhost:${port}\x1b[0m`);
71
- console.log('\n Press \x1b[33mCtrl+C\x1b[0m to stop\n');
72
-
73
- // Open browser
74
- const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
75
- require('child_process').exec(`${open} http://localhost:${port}`);
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}`);
76
444
  });
77
- }
78
445
 
79
- // Check if standalone-dist exists
80
- if (!fs.existsSync(distPath)) {
81
- console.error('\x1b[31mError: standalone-dist folder not found.\x1b[0m');
82
- console.error('Please rebuild the package with: npm run build');
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}`);
453
+ process.exit(1);
454
+ }
455
+ console.error('Server error:', err);
83
456
  process.exit(1);
84
- }
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');
85
464
 
86
- startServer(port);
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.7",
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",