localpov 0.1.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/bin/localpov-mcp.js +15 -0
  4. package/bin/localpov.js +599 -0
  5. package/dashboard/index.html +909 -0
  6. package/dist/collectors/browser-capture.d.ts +124 -0
  7. package/dist/collectors/browser-capture.js +327 -0
  8. package/dist/collectors/browser-capture.js.map +1 -0
  9. package/dist/collectors/build-parser.d.ts +18 -0
  10. package/dist/collectors/build-parser.js +192 -0
  11. package/dist/collectors/build-parser.js.map +1 -0
  12. package/dist/collectors/docker-watcher.d.ts +42 -0
  13. package/dist/collectors/docker-watcher.js +101 -0
  14. package/dist/collectors/docker-watcher.js.map +1 -0
  15. package/dist/collectors/terminal.d.ts +42 -0
  16. package/dist/collectors/terminal.js +128 -0
  17. package/dist/collectors/terminal.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +6 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/mcp-server.d.ts +1 -0
  22. package/dist/mcp-server.js +466 -0
  23. package/dist/mcp-server.js.map +1 -0
  24. package/dist/utils/inject.d.ts +11 -0
  25. package/dist/utils/inject.js +241 -0
  26. package/dist/utils/inject.js.map +1 -0
  27. package/dist/utils/network.d.ts +7 -0
  28. package/dist/utils/network.js +42 -0
  29. package/dist/utils/network.js.map +1 -0
  30. package/dist/utils/proxy.d.ts +24 -0
  31. package/dist/utils/proxy.js +363 -0
  32. package/dist/utils/proxy.js.map +1 -0
  33. package/dist/utils/scanner.d.ts +9 -0
  34. package/dist/utils/scanner.js +96 -0
  35. package/dist/utils/scanner.js.map +1 -0
  36. package/dist/utils/session-manager.d.ts +109 -0
  37. package/dist/utils/session-manager.js +488 -0
  38. package/dist/utils/session-manager.js.map +1 -0
  39. package/dist/utils/shell-init.d.ts +26 -0
  40. package/dist/utils/shell-init.js +422 -0
  41. package/dist/utils/shell-init.js.map +1 -0
  42. package/dist/utils/system-info.d.ts +51 -0
  43. package/dist/utils/system-info.js +170 -0
  44. package/dist/utils/system-info.js.map +1 -0
  45. package/package.json +64 -0
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getInjectScript = getInjectScript;
4
+ /**
5
+ * Returns the JavaScript snippet to inject into HTML pages.
6
+ * This script captures:
7
+ * - Console output (error, warn, log, info)
8
+ * - Network requests (fetch + XHR) with status, timing, response bodies for errors
9
+ * - Unhandled errors and promise rejections
10
+ * - Screenshots on demand (via html2canvas or canvas capture)
11
+ *
12
+ * Sends everything to the LocalPOV server via WebSocket.
13
+ */
14
+ function getInjectScript(wsUrl) {
15
+ // Minified-ish but readable. Runs in the user's browser.
16
+ return `
17
+ <script data-localpov-inject>
18
+ (function() {
19
+ if (window.__localpov_injected) return;
20
+ window.__localpov_injected = true;
21
+
22
+ var WS_URL = ${JSON.stringify(wsUrl)};
23
+ var ws = null;
24
+ var queue = [];
25
+ var MAX_QUEUE = 100;
26
+
27
+ function connect() {
28
+ try {
29
+ ws = new WebSocket(WS_URL);
30
+ ws.onopen = function() {
31
+ while (queue.length) ws.send(queue.shift());
32
+ };
33
+ ws.onclose = function() {
34
+ ws = null;
35
+ setTimeout(connect, 3000);
36
+ };
37
+ ws.onerror = function() { ws = null; };
38
+ } catch(e) {}
39
+ }
40
+ connect();
41
+
42
+ function send(data) {
43
+ var msg = JSON.stringify(data);
44
+ if (ws && ws.readyState === 1) {
45
+ ws.send(msg);
46
+ } else if (queue.length < MAX_QUEUE) {
47
+ queue.push(msg);
48
+ }
49
+ }
50
+
51
+ // ── Console capture ──
52
+ var origConsole = {};
53
+ ['error','warn','log','info','debug'].forEach(function(level) {
54
+ origConsole[level] = console[level];
55
+ console[level] = function() {
56
+ origConsole[level].apply(console, arguments);
57
+ var args = Array.prototype.slice.call(arguments);
58
+ var message = args.map(function(a) {
59
+ if (a instanceof Error) return a.message + '\\n' + (a.stack || '');
60
+ if (typeof a === 'object') {
61
+ try { return JSON.stringify(a, null, 2).slice(0, 1000); } catch(e) { return String(a); }
62
+ }
63
+ return String(a);
64
+ }).join(' ');
65
+ send({
66
+ type: 'console',
67
+ level: level,
68
+ message: message.slice(0, 2000),
69
+ url: location.href,
70
+ ts: Date.now()
71
+ });
72
+ };
73
+ });
74
+
75
+ // ── Unhandled errors ──
76
+ window.addEventListener('error', function(e) {
77
+ send({
78
+ type: 'error',
79
+ message: e.message || String(e),
80
+ source: (e.filename || '') + ':' + (e.lineno || 0) + ':' + (e.colno || 0),
81
+ stack: e.error && e.error.stack ? e.error.stack : null,
82
+ url: location.href,
83
+ ts: Date.now()
84
+ });
85
+ });
86
+
87
+ window.addEventListener('unhandledrejection', function(e) {
88
+ var msg = e.reason instanceof Error ? e.reason.message : String(e.reason || 'Unhandled promise rejection');
89
+ var stack = e.reason instanceof Error ? e.reason.stack : null;
90
+ send({
91
+ type: 'error',
92
+ message: msg,
93
+ stack: stack,
94
+ source: 'unhandledrejection',
95
+ url: location.href,
96
+ ts: Date.now()
97
+ });
98
+ });
99
+
100
+ // ── Fetch capture ──
101
+ var origFetch = window.fetch;
102
+ if (origFetch) {
103
+ window.fetch = function(input, init) {
104
+ var url = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
105
+ var method = (init && init.method) || (input && input.method) || 'GET';
106
+ var startTime = Date.now();
107
+
108
+ // Skip localpov's own requests
109
+ if (url.indexOf('__localpov__') !== -1) return origFetch.apply(this, arguments);
110
+
111
+ return origFetch.apply(this, arguments).then(function(response) {
112
+ var entry = {
113
+ type: 'network',
114
+ method: method.toUpperCase(),
115
+ url: url.slice(0, 500),
116
+ status: response.status,
117
+ statusText: response.statusText,
118
+ duration: Date.now() - startTime,
119
+ ts: Date.now()
120
+ };
121
+
122
+ // Capture response body for errors
123
+ if (response.status >= 400) {
124
+ response.clone().text().then(function(body) {
125
+ entry.responseBody = body.slice(0, 5000);
126
+ send(entry);
127
+ }).catch(function() { send(entry); });
128
+ } else {
129
+ send(entry);
130
+ }
131
+ return response;
132
+ }).catch(function(err) {
133
+ send({
134
+ type: 'network',
135
+ method: method.toUpperCase(),
136
+ url: url.slice(0, 500),
137
+ status: 0,
138
+ error: err.message || 'Network error',
139
+ duration: Date.now() - startTime,
140
+ ts: Date.now()
141
+ });
142
+ throw err;
143
+ });
144
+ };
145
+ }
146
+
147
+ // ── XHR capture ──
148
+ var origXHROpen = XMLHttpRequest.prototype.open;
149
+ var origXHRSend = XMLHttpRequest.prototype.send;
150
+
151
+ XMLHttpRequest.prototype.open = function(method, url) {
152
+ this.__lpov = { method: method, url: String(url).slice(0, 500), startTime: 0 };
153
+ return origXHROpen.apply(this, arguments);
154
+ };
155
+
156
+ XMLHttpRequest.prototype.send = function() {
157
+ var xhr = this;
158
+ if (xhr.__lpov) {
159
+ // Skip localpov's own requests
160
+ if (xhr.__lpov.url.indexOf('__localpov__') !== -1) {
161
+ return origXHRSend.apply(this, arguments);
162
+ }
163
+ xhr.__lpov.startTime = Date.now();
164
+ xhr.addEventListener('loadend', function() {
165
+ var entry = {
166
+ type: 'network',
167
+ method: (xhr.__lpov.method || 'GET').toUpperCase(),
168
+ url: xhr.__lpov.url,
169
+ status: xhr.status,
170
+ statusText: xhr.statusText,
171
+ duration: Date.now() - xhr.__lpov.startTime,
172
+ ts: Date.now()
173
+ };
174
+ if (xhr.status >= 400) {
175
+ entry.responseBody = String(xhr.responseText || '').slice(0, 5000);
176
+ }
177
+ send(entry);
178
+ });
179
+ xhr.addEventListener('error', function() {
180
+ send({
181
+ type: 'network',
182
+ method: (xhr.__lpov.method || 'GET').toUpperCase(),
183
+ url: xhr.__lpov.url,
184
+ status: 0,
185
+ error: 'XHR network error',
186
+ duration: Date.now() - xhr.__lpov.startTime,
187
+ ts: Date.now()
188
+ });
189
+ });
190
+ }
191
+ return origXHRSend.apply(this, arguments);
192
+ };
193
+
194
+ // ── Screenshot on demand ──
195
+ // Server can request a screenshot via WS message { type: 'take-screenshot' }
196
+ function takeScreenshot() {
197
+ // Try html2canvas if loaded, otherwise use basic canvas capture
198
+ if (window.html2canvas) {
199
+ window.html2canvas(document.body, { useCORS: true, logging: false, scale: 0.5 }).then(function(canvas) {
200
+ send({ type: 'screenshot', data: canvas.toDataURL('image/jpeg', 0.6), ts: Date.now() });
201
+ }).catch(function() {
202
+ fallbackScreenshot();
203
+ });
204
+ } else {
205
+ fallbackScreenshot();
206
+ }
207
+ }
208
+
209
+ function fallbackScreenshot() {
210
+ // Inject html2canvas dynamically on first screenshot request
211
+ var script = document.createElement('script');
212
+ script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
213
+ script.onload = function() {
214
+ window.html2canvas(document.body, { useCORS: true, logging: false, scale: 0.5 }).then(function(canvas) {
215
+ send({ type: 'screenshot', data: canvas.toDataURL('image/jpeg', 0.6), ts: Date.now() });
216
+ }).catch(function(e) {
217
+ send({ type: 'console', level: 'warn', message: 'LocalPOV: screenshot failed: ' + e.message, ts: Date.now() });
218
+ });
219
+ };
220
+ script.onerror = function() {
221
+ send({ type: 'console', level: 'warn', message: 'LocalPOV: could not load html2canvas for screenshots', ts: Date.now() });
222
+ };
223
+ document.head.appendChild(script);
224
+ }
225
+
226
+ // Listen for server commands
227
+ function setupWsListener() {
228
+ if (!ws) { setTimeout(setupWsListener, 1000); return; }
229
+ ws.addEventListener('message', function(e) {
230
+ try {
231
+ var cmd = JSON.parse(e.data);
232
+ if (cmd.type === 'take-screenshot') takeScreenshot();
233
+ } catch(err) {}
234
+ });
235
+ }
236
+ setupWsListener();
237
+
238
+ })();
239
+ </script>`;
240
+ }
241
+ //# sourceMappingURL=inject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject.js","sourceRoot":"","sources":["../../src/utils/inject.ts"],"names":[],"mappings":";;AAUA,0CAkOC;AA5OD;;;;;;;;;GASG;AACH,SAAgB,eAAe,CAAC,KAAa;IAC3C,yDAAyD;IACzD,OAAO;;;;;;iBAMQ,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAyN5B,CAAC;AACX,CAAC"}
@@ -0,0 +1,7 @@
1
+ interface IPResult {
2
+ name: string;
3
+ address: string;
4
+ }
5
+ export declare function getLocalIP(): string;
6
+ export declare function getAllIPs(): IPResult[];
7
+ export {};
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getLocalIP = getLocalIP;
7
+ exports.getAllIPs = getAllIPs;
8
+ const os_1 = __importDefault(require("os"));
9
+ function getLocalIP() {
10
+ const interfaces = os_1.default.networkInterfaces();
11
+ const candidates = [];
12
+ for (const [name, addrs] of Object.entries(interfaces)) {
13
+ if (!addrs)
14
+ continue;
15
+ for (const addr of addrs) {
16
+ if (addr.family === 'IPv4' && !addr.internal) {
17
+ candidates.push({
18
+ name,
19
+ address: addr.address,
20
+ priority: name.match(/^(Wi-Fi|Ethernet|en0|wlan0|eth0|wlp)/i) ? 0 : 1,
21
+ });
22
+ }
23
+ }
24
+ }
25
+ candidates.sort((a, b) => a.priority - b.priority);
26
+ return candidates.length > 0 ? candidates[0].address : '127.0.0.1';
27
+ }
28
+ function getAllIPs() {
29
+ const interfaces = os_1.default.networkInterfaces();
30
+ const results = [];
31
+ for (const [name, addrs] of Object.entries(interfaces)) {
32
+ if (!addrs)
33
+ continue;
34
+ for (const addr of addrs) {
35
+ if (addr.family === 'IPv4' && !addr.internal) {
36
+ results.push({ name, address: addr.address });
37
+ }
38
+ }
39
+ }
40
+ return results;
41
+ }
42
+ //# sourceMappingURL=network.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/utils/network.ts"],"names":[],"mappings":";;;;;AAaA,gCAmBC;AAED,8BAcC;AAhDD,4CAAoB;AAapB,SAAgB,UAAU;IACxB,MAAM,UAAU,GAAG,YAAE,CAAC,iBAAiB,EAAE,CAAC;IAC1C,MAAM,UAAU,GAAkB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC7C,UAAU,CAAC,IAAI,CAAC;oBACd,IAAI;oBACJ,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;iBACtE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IACnD,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;AACrE,CAAC;AAED,SAAgB,SAAS;IACvB,MAAM,UAAU,GAAG,YAAE,CAAC,iBAAiB,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAe,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC7C,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import http from 'http';
2
+ import { TerminalCapture } from '../collectors/terminal';
3
+ import { BrowserCapture } from '../collectors/browser-capture';
4
+ interface AppInfo {
5
+ port: number;
6
+ framework: string;
7
+ }
8
+ interface CreateServerOptions {
9
+ targetPort: number;
10
+ listenPort: number;
11
+ getApps?: () => AppInfo[];
12
+ onLog?: (level: string, message: string | number) => void;
13
+ onReady?: () => void;
14
+ terminal?: TerminalCapture | null;
15
+ browserCapture?: BrowserCapture | null;
16
+ }
17
+ interface ProxyServer {
18
+ server: http.Server;
19
+ readonly currentTarget: number;
20
+ setTarget(port: number): void;
21
+ close(): void;
22
+ }
23
+ export declare function createServer({ targetPort, listenPort, getApps, onLog, onReady, terminal, browserCapture }: CreateServerOptions): ProxyServer;
24
+ export {};
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createServer = createServer;
7
+ const http_1 = __importDefault(require("http"));
8
+ const http_proxy_1 = __importDefault(require("http-proxy"));
9
+ const ws_1 = __importDefault(require("ws"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const inject_1 = require("./inject");
14
+ const DASHBOARD_PREFIX = '/__localpov__';
15
+ const DASHBOARD_DIR = path_1.default.join(__dirname, '..', '..', 'dashboard');
16
+ const MIME = {
17
+ '.html': 'text/html; charset=utf-8',
18
+ '.js': 'application/javascript',
19
+ '.css': 'text/css',
20
+ '.svg': 'image/svg+xml',
21
+ '.png': 'image/png',
22
+ '.json': 'application/json',
23
+ };
24
+ const IFRAME_BLOCKED = ['x-frame-options', 'content-security-policy', 'content-security-policy-report-only'];
25
+ function parseCookies(str) {
26
+ const out = {};
27
+ for (const part of (str || '').split(';')) {
28
+ const idx = part.indexOf('=');
29
+ if (idx < 0)
30
+ continue;
31
+ const k = part.slice(0, idx).trim();
32
+ const v = part.slice(idx + 1).trim();
33
+ try {
34
+ out[k] = decodeURIComponent(v);
35
+ }
36
+ catch {
37
+ out[k] = v;
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ function createServer({ targetPort, listenPort, getApps, onLog, onReady, terminal, browserCapture }) {
43
+ let defaultTarget = targetPort;
44
+ const proxy = http_proxy_1.default.createProxyServer({ ws: true, xfwd: true, changeOrigin: true });
45
+ const injectWsUrl = `ws://\${req_host}:${listenPort}/__localpov__/ws/browser`;
46
+ const injectSnippet = (0, inject_1.getInjectScript)(injectWsUrl.replace('${req_host}', '"+location.hostname+"'));
47
+ proxy.on('proxyRes', (proxyRes, req, res) => {
48
+ for (const h of IFRAME_BLOCKED)
49
+ delete proxyRes.headers[h];
50
+ const ct = proxyRes.headers['content-type'] || '';
51
+ if (!ct.includes('text/html'))
52
+ return;
53
+ const origWrite = res.write;
54
+ const origEnd = res.end;
55
+ const chunks = [];
56
+ delete proxyRes.headers['content-length'];
57
+ res.write = function (chunk) {
58
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
59
+ return true;
60
+ };
61
+ res.end = function (chunk) {
62
+ if (chunk)
63
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
64
+ let body = Buffer.concat(chunks).toString('utf8');
65
+ if (body.includes('</head>')) {
66
+ body = body.replace('</head>', injectSnippet + '</head>');
67
+ }
68
+ else if (body.includes('</body>')) {
69
+ body = body.replace('</body>', injectSnippet + '</body>');
70
+ }
71
+ else if (body.includes('<html') || body.includes('<!DOCTYPE') || body.includes('<!doctype')) {
72
+ body += injectSnippet;
73
+ }
74
+ origWrite.call(res, body, 'utf8');
75
+ origEnd.call(res);
76
+ return res;
77
+ };
78
+ });
79
+ let _lastProxyError = '';
80
+ let _lastProxyErrorTime = 0;
81
+ proxy.on('error', (err, req, res) => {
82
+ // Suppress repeated identical errors (e.g. ECONNREFUSED spam when target is down)
83
+ const now = Date.now();
84
+ if (err.message === _lastProxyError && now - _lastProxyErrorTime < 5000) {
85
+ // Skip logging, still serve error page
86
+ }
87
+ else {
88
+ _lastProxyError = err.message;
89
+ _lastProxyErrorTime = now;
90
+ if (onLog)
91
+ onLog('error', `Proxy error: ${err.message}`);
92
+ }
93
+ if (res && 'writeHead' in res && !('headersSent' in res && res.headersSent)) {
94
+ res.writeHead(502, { 'Content-Type': 'text/html; charset=utf-8' });
95
+ res.end(errorPage(defaultTarget));
96
+ }
97
+ });
98
+ function resolvePort(req) {
99
+ const urlObj = new URL(req.url || '/', 'http://localhost');
100
+ const cookies = parseCookies(req.headers.cookie);
101
+ const _port = urlObj.searchParams.get('_port');
102
+ if (_port) {
103
+ const p = parseInt(_port, 10);
104
+ if (p > 0 && p < 65536)
105
+ return p;
106
+ }
107
+ if (cookies.lpov_port) {
108
+ const p = parseInt(cookies.lpov_port, 10);
109
+ if (p > 0 && p < 65536)
110
+ return p;
111
+ }
112
+ return defaultTarget;
113
+ }
114
+ function setPortCookie(res, port, extraHeaders) {
115
+ const headers = Object.assign({}, extraHeaders || {});
116
+ headers['Set-Cookie'] = `lpov_port=${port}; Path=/; SameSite=Strict`;
117
+ return headers;
118
+ }
119
+ const termClients = new Set();
120
+ const browserClients = new Set();
121
+ const server = http_1.default.createServer((req, res) => {
122
+ const urlObj = new URL(req.url || '/', 'http://localhost');
123
+ if (urlObj.pathname === '/__localpov__/api/apps') {
124
+ const cookies = parseCookies(req.headers.cookie);
125
+ const sessionPort = parseInt(cookies.lpov_port, 10) || defaultTarget;
126
+ return json(res, { apps: getApps ? getApps() : [], currentTarget: sessionPort });
127
+ }
128
+ if (urlObj.pathname === '/__localpov__/api/switch') {
129
+ const port = parseInt(urlObj.searchParams.get('port') || '', 10);
130
+ if (port > 0 && port < 65536) {
131
+ if (onLog)
132
+ onLog('switch', port);
133
+ res.writeHead(200, setPortCookie(res, port, {
134
+ 'Content-Type': 'application/json',
135
+ 'Cache-Control': 'no-cache',
136
+ }));
137
+ return res.end(JSON.stringify({ ok: true, target: port }));
138
+ }
139
+ return json(res, { error: 'Invalid port' }, 400);
140
+ }
141
+ if (urlObj.pathname === '/__localpov__/api/ping') {
142
+ return json(res, { ok: true, uptime: process.uptime() | 0 });
143
+ }
144
+ if (urlObj.pathname === '/__localpov__/api/terminal') {
145
+ if (terminal) {
146
+ return json(res, terminal.getStatus());
147
+ }
148
+ return json(res, { running: false, command: null });
149
+ }
150
+ if (urlObj.pathname === '/__localpov__/api/browser') {
151
+ if (!browserCapture)
152
+ return json(res, { console: [], network: [], summary: null });
153
+ const source = urlObj.searchParams.get('source') || 'summary';
154
+ if (source === 'console') {
155
+ return json(res, { entries: browserCapture.getConsoleEntries({ limit: 100 }) });
156
+ }
157
+ if (source === 'network') {
158
+ return json(res, { entries: browserCapture.getNetworkEntries({ limit: 100 }) });
159
+ }
160
+ return json(res, browserCapture.getSummary());
161
+ }
162
+ if (urlObj.pathname === '/__localpov__/api/health') {
163
+ const mem = process.memoryUsage();
164
+ return json(res, {
165
+ memory: Math.round((1 - os_1.default.freemem() / os_1.default.totalmem()) * 100),
166
+ heapMB: Math.round(mem.heapUsed / 1024 / 1024),
167
+ uptime: Math.floor(process.uptime()),
168
+ platform: process.platform,
169
+ node: process.version,
170
+ wsClients: { browser: browserClients.size, terminal: termClients.size },
171
+ });
172
+ }
173
+ if (urlObj.pathname === '/__localpov__/api/debug') {
174
+ if (process.env.NODE_ENV === 'production' && !process.env.LOCALPOV_DEBUG) {
175
+ return json(res, { error: 'Not found' }, 404);
176
+ }
177
+ return json(res, {
178
+ defaultTarget,
179
+ dashboardReady: fs_1.default.existsSync(path_1.default.join(DASHBOARD_DIR, 'index.html')),
180
+ platform: process.platform,
181
+ nodeVersion: process.version,
182
+ apps: getApps ? getApps() : [],
183
+ });
184
+ }
185
+ if (urlObj.pathname.startsWith(DASHBOARD_PREFIX)) {
186
+ return serveDashboard(urlObj.pathname, res);
187
+ }
188
+ // Serve an empty favicon to prevent 502 spam when target is down
189
+ if (urlObj.pathname === '/favicon.ico') {
190
+ res.writeHead(204);
191
+ return res.end();
192
+ }
193
+ const port = resolvePort(req);
194
+ if (urlObj.searchParams.get('_port')) {
195
+ const cleanUrl = (req.url || '/').replace(/[?&]_port=\d+/, '').replace(/\?$/, '') || '/';
196
+ res.writeHead(302, setPortCookie(res, port, { Location: cleanUrl }));
197
+ return res.end();
198
+ }
199
+ proxy.web(req, res, { target: `http://127.0.0.1:${port}` });
200
+ });
201
+ const termWss = new ws_1.default.Server({ noServer: true });
202
+ const MAX_WS_CLIENTS = 50;
203
+ const browserWss = new ws_1.default.Server({ noServer: true });
204
+ if (terminal) {
205
+ terminal.on('data', (data) => {
206
+ const msg = JSON.stringify({ type: 'data', stream: data.type, text: data.text, ts: data.ts });
207
+ for (const ws of termClients) {
208
+ if (ws.readyState === ws_1.default.OPEN)
209
+ ws.send(msg);
210
+ }
211
+ });
212
+ }
213
+ server.on('upgrade', (req, socket, head) => {
214
+ const upgradeUrl = new URL(req.url || '/', 'http://localhost');
215
+ if (upgradeUrl.pathname === '/__localpov__/ws/browser') {
216
+ if (browserClients.size >= MAX_WS_CLIENTS) {
217
+ socket.write('HTTP/1.1 429 Too Many Connections\r\n\r\n');
218
+ socket.destroy();
219
+ return;
220
+ }
221
+ browserWss.handleUpgrade(req, socket, head, (ws) => {
222
+ browserClients.add(ws);
223
+ ws.on('message', (data) => {
224
+ if (browserCapture) {
225
+ try {
226
+ const str = data.toString();
227
+ if (str.length > 65536)
228
+ return;
229
+ browserCapture.handleMessage(str);
230
+ }
231
+ catch (e) {
232
+ const msg = e instanceof Error ? e.message : String(e);
233
+ if (onLog)
234
+ onLog('warn', `Browser WS message error: ${msg}`);
235
+ }
236
+ }
237
+ });
238
+ ws.on('close', () => browserClients.delete(ws));
239
+ ws.on('error', (e) => {
240
+ if (onLog)
241
+ onLog('warn', `Browser WS error: ${e.message}`);
242
+ browserClients.delete(ws);
243
+ });
244
+ });
245
+ return;
246
+ }
247
+ if (upgradeUrl.pathname === '/__localpov__/ws/terminal') {
248
+ if (termClients.size >= MAX_WS_CLIENTS) {
249
+ socket.write('HTTP/1.1 429 Too Many Connections\r\n\r\n');
250
+ socket.destroy();
251
+ return;
252
+ }
253
+ termWss.handleUpgrade(req, socket, head, (ws) => {
254
+ termClients.add(ws);
255
+ if (terminal) {
256
+ const history = terminal.getBuffer();
257
+ ws.send(JSON.stringify({ type: 'history', lines: history }));
258
+ }
259
+ else {
260
+ ws.send(JSON.stringify({ type: 'status', running: false }));
261
+ }
262
+ ws.on('message', (data) => {
263
+ if (!terminal || !terminal.interactive)
264
+ return;
265
+ try {
266
+ const str = data.toString();
267
+ if (str.length > 4096)
268
+ return;
269
+ const msg = JSON.parse(str);
270
+ if (msg.type === 'input' && typeof msg.text === 'string') {
271
+ terminal.write(msg.text.slice(0, 1024));
272
+ }
273
+ }
274
+ catch (e) {
275
+ const eMsg = e instanceof Error ? e.message : String(e);
276
+ if (onLog)
277
+ onLog('warn', `Terminal WS message error: ${eMsg}`);
278
+ }
279
+ });
280
+ ws.on('close', () => termClients.delete(ws));
281
+ ws.on('error', (e) => {
282
+ if (onLog)
283
+ onLog('warn', `Terminal WS error: ${e.message}`);
284
+ termClients.delete(ws);
285
+ });
286
+ });
287
+ return;
288
+ }
289
+ if (req.url && req.url.startsWith(DASHBOARD_PREFIX))
290
+ return;
291
+ const port = resolvePort(req);
292
+ proxy.ws(req, socket, head, { target: `http://127.0.0.1:${port}` });
293
+ });
294
+ server.on('error', (err) => {
295
+ if (err.code === 'EADDRINUSE') {
296
+ if (onLog)
297
+ onLog('fatal', `Port ${listenPort} already in use. Is another instance running?`);
298
+ }
299
+ if (onLog)
300
+ onLog('error', `Server error: ${err.message}`);
301
+ });
302
+ server.listen(listenPort, '0.0.0.0', () => { if (onReady)
303
+ onReady(); });
304
+ function serveDashboard(pathname, res) {
305
+ let filePath = pathname.replace(DASHBOARD_PREFIX, '') || '/';
306
+ if (filePath === '' || filePath === '/')
307
+ filePath = '/index.html';
308
+ const fullPath = path_1.default.join(DASHBOARD_DIR, filePath);
309
+ if (!fullPath.startsWith(DASHBOARD_DIR)) {
310
+ res.writeHead(403);
311
+ res.end();
312
+ return;
313
+ }
314
+ fs_1.default.readFile(fullPath, (err, data) => {
315
+ if (err) {
316
+ res.writeHead(404);
317
+ res.end('Not found');
318
+ return;
319
+ }
320
+ const ext = path_1.default.extname(filePath);
321
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-cache' });
322
+ res.end(data);
323
+ });
324
+ }
325
+ function json(res, data, status = 200) {
326
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
327
+ res.end(JSON.stringify(data));
328
+ }
329
+ return {
330
+ server,
331
+ get currentTarget() { return defaultTarget; },
332
+ setTarget(port) { defaultTarget = port; },
333
+ close() {
334
+ for (const ws of browserClients) {
335
+ try {
336
+ ws.close(1001, 'Server shutting down');
337
+ }
338
+ catch { }
339
+ }
340
+ for (const ws of termClients) {
341
+ try {
342
+ ws.close(1001, 'Server shutting down');
343
+ }
344
+ catch { }
345
+ }
346
+ browserClients.clear();
347
+ termClients.clear();
348
+ server.close();
349
+ proxy.close();
350
+ },
351
+ };
352
+ }
353
+ function errorPage(port) {
354
+ return `<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width,initial-scale=1">
355
+ <style>*{margin:0;box-sizing:border-box}body{font-family:system-ui;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
356
+ .b{text-align:center;max-width:320px}h2{font-size:18px;margin-bottom:8px}p{font-size:14px;line-height:1.5;margin-bottom:6px;opacity:.7}
357
+ code{background:#f0f0f0;padding:2px 8px;border-radius:4px;font-size:13px}
358
+ button{margin-top:16px;padding:10px 28px;font-size:14px;border-radius:8px;border:1px solid #ddd;background:#fff;cursor:pointer;font-family:inherit}
359
+ @media(prefers-color-scheme:dark){body{background:#0a0a0a;color:#eee}code{background:#1a1a1a}button{background:#1a1a1a;border-color:#333;color:#ccc}}</style>
360
+ </head><body><div class="b"><h2>App not responding</h2><p><code>localhost:${port}</code></p><p>Is your dev server running?</p>
361
+ <button onclick="location.reload()">Retry</button><script>setTimeout(()=>location.reload(),5000)</script></div></body></html>`;
362
+ }
363
+ //# sourceMappingURL=proxy.js.map