@romanmatena/browsermonitor 2.0.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.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/package.json +53 -0
- package/src/agents.llm/browser-monitor-section.md +18 -0
- package/src/cli.mjs +202 -0
- package/src/http-server.mjs +536 -0
- package/src/init.mjs +162 -0
- package/src/intro.mjs +36 -0
- package/src/logging/LogBuffer.mjs +178 -0
- package/src/logging/constants.mjs +19 -0
- package/src/logging/dump.mjs +207 -0
- package/src/logging/index.mjs +13 -0
- package/src/logging/timestamps.mjs +13 -0
- package/src/monitor/README.md +10 -0
- package/src/monitor/index.mjs +18 -0
- package/src/monitor/interactive-mode.mjs +275 -0
- package/src/monitor/join-mode.mjs +654 -0
- package/src/monitor/open-mode.mjs +889 -0
- package/src/monitor/page-monitoring.mjs +199 -0
- package/src/monitor/tab-selection.mjs +53 -0
- package/src/monitor.mjs +39 -0
- package/src/os/README.md +4 -0
- package/src/os/wsl/chrome.mjs +503 -0
- package/src/os/wsl/detect.mjs +68 -0
- package/src/os/wsl/diagnostics.mjs +729 -0
- package/src/os/wsl/index.mjs +45 -0
- package/src/os/wsl/port-proxy.mjs +190 -0
- package/src/settings.mjs +101 -0
- package/src/templates/api-help.mjs +212 -0
- package/src/templates/cli-commands.mjs +51 -0
- package/src/templates/interactive-keys.mjs +33 -0
- package/src/templates/ready-help.mjs +33 -0
- package/src/templates/section-heading.mjs +141 -0
- package/src/templates/table-helper.mjs +73 -0
- package/src/templates/wait-for-chrome.mjs +19 -0
- package/src/utils/ask.mjs +49 -0
- package/src/utils/chrome-profile-path.mjs +37 -0
- package/src/utils/colors.mjs +49 -0
- package/src/utils/env.mjs +30 -0
- package/src/utils/profile-id.mjs +23 -0
- package/src/utils/status-line.mjs +47 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join Mode - attach to existing Chrome (connect to running browser).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import puppeteer from 'puppeteer';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { C, log } from '../utils/colors.mjs';
|
|
11
|
+
import { getChromeProfileLocation } from '../utils/chrome-profile-path.mjs';
|
|
12
|
+
import {
|
|
13
|
+
getWindowsHostForWSL,
|
|
14
|
+
detectWindowsChromeCanaryPath,
|
|
15
|
+
printCanaryInstallInstructions,
|
|
16
|
+
scanChromeInstances,
|
|
17
|
+
findProjectChrome,
|
|
18
|
+
findFreeDebugPort,
|
|
19
|
+
startChromeOnWindows,
|
|
20
|
+
killPuppeteerMonitorChromes,
|
|
21
|
+
removePortProxyIfExists,
|
|
22
|
+
isPortBlocked,
|
|
23
|
+
runWslDiagnostics,
|
|
24
|
+
} from '../os/wsl/index.mjs';
|
|
25
|
+
import { LogBuffer } from '../logging/index.mjs';
|
|
26
|
+
import { createHttpServer } from '../http-server.mjs';
|
|
27
|
+
import { setupPageMonitoring as setupPageMonitoringShared } from './page-monitoring.mjs';
|
|
28
|
+
import { askUserToSelectPage } from './tab-selection.mjs';
|
|
29
|
+
import { askYesNo } from '../utils/ask.mjs';
|
|
30
|
+
import { printReadyHelp, printStatusBlock, KEYS_JOIN } from '../templates/ready-help.mjs';
|
|
31
|
+
import { printJoinConnectedBlock, printModeHeading } from '../templates/section-heading.mjs';
|
|
32
|
+
import { printApiHelpTable } from '../templates/api-help.mjs';
|
|
33
|
+
import { createTable, printTable } from '../templates/table-helper.mjs';
|
|
34
|
+
import { buildWaitForChromeContent } from '../templates/wait-for-chrome.mjs';
|
|
35
|
+
import { writeStatusLine, clearStatusLine } from '../utils/status-line.mjs';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run in Join Mode - attach to an existing Chrome browser
|
|
39
|
+
* @param {number} port - Chrome debugging port (default: 9222)
|
|
40
|
+
* @param {Object} options - Monitor options
|
|
41
|
+
*/
|
|
42
|
+
export async function runJoinMode(port, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
realtime = false,
|
|
45
|
+
outputDir = process.cwd(),
|
|
46
|
+
paths = null,
|
|
47
|
+
ignorePatterns = [],
|
|
48
|
+
hardTimeout = 0,
|
|
49
|
+
httpPort = 60001,
|
|
50
|
+
defaultUrl = '',
|
|
51
|
+
host = null, // Allow explicit host override
|
|
52
|
+
sharedHttpState = null,
|
|
53
|
+
sharedHttpServer = null,
|
|
54
|
+
skipModeHeading = false,
|
|
55
|
+
} = options;
|
|
56
|
+
|
|
57
|
+
if (!skipModeHeading) printModeHeading('Join mode');
|
|
58
|
+
const lazyMode = !realtime;
|
|
59
|
+
const connectHost = host || getWindowsHostForWSL({ quiet: true });
|
|
60
|
+
const browserURL = `http://${connectHost}:${port}`;
|
|
61
|
+
// Mutable URL that will be updated if auto-port-selection changes the port
|
|
62
|
+
let currentBrowserURL = browserURL;
|
|
63
|
+
|
|
64
|
+
// Create LogBuffer instance for centralized buffer management
|
|
65
|
+
const logBuffer = new LogBuffer({
|
|
66
|
+
outputDir,
|
|
67
|
+
paths,
|
|
68
|
+
lazyMode,
|
|
69
|
+
ignorePatterns,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let browser = null;
|
|
73
|
+
let monitoredPages = [];
|
|
74
|
+
let cleanupDone = false;
|
|
75
|
+
let httpServer = null;
|
|
76
|
+
let isSelectingTab = false; // Flag to pause main keypress handler during tab selection
|
|
77
|
+
let activePageCleanup = null;
|
|
78
|
+
let collectingPaused = false; // When true, console/network events are not recorded
|
|
79
|
+
|
|
80
|
+
// Output counter for periodic help reminder (same block as initial Ready, every HELP_INTERVAL)
|
|
81
|
+
let outputCounter = 0;
|
|
82
|
+
const HELP_INTERVAL = 5;
|
|
83
|
+
function maybeShowHelp() {
|
|
84
|
+
outputCounter++;
|
|
85
|
+
if (outputCounter % HELP_INTERVAL === 0) {
|
|
86
|
+
printReadyHelp(httpPort, KEYS_JOIN);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// In-session help (h key) – full table with session context for Claude Code
|
|
91
|
+
function printHelp() {
|
|
92
|
+
const currentUrl = monitoredPages[0]?.url?.() || defaultUrl || '';
|
|
93
|
+
const profileLoc = getChromeProfileLocation(outputDir);
|
|
94
|
+
console.log(`${C.cyan}Join mode${C.reset} Browser: ${C.brightGreen}${currentBrowserURL}${C.reset} │ Dir: ${outputDir}`);
|
|
95
|
+
printApiHelpTable({
|
|
96
|
+
port: httpPort,
|
|
97
|
+
showApi: true,
|
|
98
|
+
showInteractive: true,
|
|
99
|
+
showOutputFiles: true,
|
|
100
|
+
context: {
|
|
101
|
+
consoleLog: logBuffer.CONSOLE_LOG,
|
|
102
|
+
networkLog: logBuffer.NETWORK_LOG,
|
|
103
|
+
networkDir: logBuffer.NETWORK_DIR,
|
|
104
|
+
cookiesDir: logBuffer.COOKIES_DIR,
|
|
105
|
+
domHtml: logBuffer.DOM_HTML,
|
|
106
|
+
screenshot: logBuffer.SCREENSHOT,
|
|
107
|
+
},
|
|
108
|
+
sessionContext: {
|
|
109
|
+
currentUrl: currentUrl || undefined,
|
|
110
|
+
profilePath: profileLoc?.path,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Use shared HTTP server (started in CLI) or create our own
|
|
116
|
+
if (sharedHttpState && sharedHttpServer) {
|
|
117
|
+
httpServer = sharedHttpServer;
|
|
118
|
+
sharedHttpState.mode = 'join';
|
|
119
|
+
sharedHttpState.logBuffer = logBuffer;
|
|
120
|
+
sharedHttpState.getPages = () => monitoredPages;
|
|
121
|
+
sharedHttpState.getCollectingPaused = () => collectingPaused;
|
|
122
|
+
sharedHttpState.setCollectingPaused = (v) => { collectingPaused = !!v; };
|
|
123
|
+
sharedHttpState.getAllTabs = async () => {
|
|
124
|
+
if (!browser) return [];
|
|
125
|
+
const allPages = await browser.pages();
|
|
126
|
+
const isUserPage = (pg) => {
|
|
127
|
+
const u = pg.url();
|
|
128
|
+
return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
|
|
129
|
+
};
|
|
130
|
+
let pages = allPages.filter(isUserPage);
|
|
131
|
+
const nonBlank = pages.filter(pg => pg.url() !== 'about:blank');
|
|
132
|
+
if (nonBlank.length > 0) pages = nonBlank;
|
|
133
|
+
return pages.map((pg, i) => ({ index: i + 1, url: pg.url() }));
|
|
134
|
+
};
|
|
135
|
+
sharedHttpState.switchToTab = async (index) => {
|
|
136
|
+
if (!browser) return { success: false, error: 'Browser not connected' };
|
|
137
|
+
try {
|
|
138
|
+
const allPages = await browser.pages();
|
|
139
|
+
const isUserPage = (pg) => {
|
|
140
|
+
const u = pg.url();
|
|
141
|
+
return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
|
|
142
|
+
};
|
|
143
|
+
let pages = allPages.filter(isUserPage);
|
|
144
|
+
const nonBlank = pages.filter(pg => pg.url() !== 'about:blank');
|
|
145
|
+
if (nonBlank.length > 0) pages = nonBlank;
|
|
146
|
+
if (index < 1 || index > pages.length) return { success: false, error: `Invalid index. Use 1-${pages.length}.` };
|
|
147
|
+
const selectedPage = pages[index - 1];
|
|
148
|
+
monitoredPages = [selectedPage];
|
|
149
|
+
setupPageMonitoring(selectedPage, 'Page');
|
|
150
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
151
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
152
|
+
return { success: true, url: selectedPage.url() };
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return { success: false, error: e.message };
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
httpServer = createHttpServer({
|
|
159
|
+
port: httpPort,
|
|
160
|
+
mode: 'join',
|
|
161
|
+
logBuffer,
|
|
162
|
+
getPages: () => monitoredPages,
|
|
163
|
+
getCollectingPaused: () => collectingPaused,
|
|
164
|
+
setCollectingPaused: (v) => { collectingPaused = !!v; },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function cleanup(code = 0, closeBrowser = false) {
|
|
169
|
+
if (cleanupDone) return;
|
|
170
|
+
cleanupDone = true;
|
|
171
|
+
|
|
172
|
+
console.log('');
|
|
173
|
+
log.info(closeBrowser ? 'Disconnecting and closing Chrome...' : 'Disconnecting...');
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (httpServer) {
|
|
177
|
+
await new Promise((resolve) => httpServer.close(resolve));
|
|
178
|
+
log.dim('HTTP server closed');
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
if (browser) {
|
|
184
|
+
if (closeBrowser) {
|
|
185
|
+
await browser.close();
|
|
186
|
+
log.success('Browser closed');
|
|
187
|
+
} else {
|
|
188
|
+
browser.disconnect();
|
|
189
|
+
log.success('Disconnected from browser (Chrome still running)');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {}
|
|
193
|
+
|
|
194
|
+
process.exit(code);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.on('SIGINT', () => cleanup(0, false));
|
|
198
|
+
process.on('SIGTERM', () => cleanup(0, false));
|
|
199
|
+
process.on('uncaughtException', (e) => {
|
|
200
|
+
const msg = (e && e.message) || String(e);
|
|
201
|
+
if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
|
|
202
|
+
log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
log.error(`Uncaught exception: ${e.message}`);
|
|
206
|
+
cleanup(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (hardTimeout > 0) {
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
log.error(`HARD TIMEOUT (${hardTimeout}ms) - forcing exit`);
|
|
212
|
+
cleanup(1);
|
|
213
|
+
}, hardTimeout);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function setupKeyboardInput() {
|
|
217
|
+
readline.emitKeypressEvents(process.stdin);
|
|
218
|
+
if (process.stdin.isTTY) {
|
|
219
|
+
process.stdin.setRawMode(true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
223
|
+
// Ctrl+C always works
|
|
224
|
+
if (key.ctrl && key.name === 'c') {
|
|
225
|
+
cleanup(0);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Ignore other keys during tab selection (the selection handler will process them)
|
|
230
|
+
if (isSelectingTab) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Shortcuts only without modifiers (no Ctrl, Shift, Win)
|
|
235
|
+
if (key.ctrl || key.shift || key.meta) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (key.name === 'd') {
|
|
240
|
+
const page = monitoredPages.length > 0 ? monitoredPages[0] : null;
|
|
241
|
+
await logBuffer.dumpBuffersToFiles({
|
|
242
|
+
dumpCookies: page ? () => logBuffer.dumpCookiesFromPage(page) : null,
|
|
243
|
+
dumpDom: page ? () => logBuffer.dumpDomFromPage(page) : null,
|
|
244
|
+
dumpScreenshot: page ? () => logBuffer.dumpScreenshotFromPage(page) : null,
|
|
245
|
+
});
|
|
246
|
+
maybeShowHelp();
|
|
247
|
+
} else if (key.name === 'c') {
|
|
248
|
+
logBuffer.clearAllBuffers();
|
|
249
|
+
maybeShowHelp();
|
|
250
|
+
} else if (key.name === 'q') {
|
|
251
|
+
cleanup(0, false);
|
|
252
|
+
} else if (key.name === 'k') {
|
|
253
|
+
log.warn('Closing Chrome and exiting...');
|
|
254
|
+
cleanup(0, true);
|
|
255
|
+
} else if (key.name === 's') {
|
|
256
|
+
const stats = logBuffer.getStats();
|
|
257
|
+
const urls = monitoredPages.map(p => p.url()).join(', ');
|
|
258
|
+
printStatusBlock(stats, urls, monitoredPages.length, collectingPaused);
|
|
259
|
+
maybeShowHelp();
|
|
260
|
+
} else if (key.name === 'p') {
|
|
261
|
+
collectingPaused = !collectingPaused;
|
|
262
|
+
log.info(collectingPaused ? 'Collecting stopped (paused). Press p or curl .../start to resume.' : 'Collecting started (resumed).');
|
|
263
|
+
maybeShowHelp();
|
|
264
|
+
} else if (key.name === 't') {
|
|
265
|
+
// Switch tabs
|
|
266
|
+
await switchTabs();
|
|
267
|
+
maybeShowHelp();
|
|
268
|
+
} else if (key.name === 'h') {
|
|
269
|
+
// Show full help
|
|
270
|
+
printHelp();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function setupPageMonitoring(page, pageLabel) {
|
|
276
|
+
setupPageMonitoringShared(page, {
|
|
277
|
+
logBuffer,
|
|
278
|
+
getCollectingPaused: () => collectingPaused,
|
|
279
|
+
setActivePageCleanup: (fn) => { activePageCleanup = fn; },
|
|
280
|
+
pageLabel: pageLabel || '',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function switchTabs() {
|
|
285
|
+
if (!browser) {
|
|
286
|
+
log.error('Browser not connected');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const allPages = await browser.pages();
|
|
292
|
+
|
|
293
|
+
// Filter out internal Chrome pages
|
|
294
|
+
const isUserPage = (pg) => {
|
|
295
|
+
const pgUrl = pg.url();
|
|
296
|
+
return !pgUrl.startsWith('chrome://') &&
|
|
297
|
+
!pgUrl.startsWith('devtools://') &&
|
|
298
|
+
!pgUrl.startsWith('chrome-extension://');
|
|
299
|
+
};
|
|
300
|
+
let pages = allPages.filter(isUserPage);
|
|
301
|
+
|
|
302
|
+
// If we have pages other than about:blank, filter out about:blank
|
|
303
|
+
const nonBlankPages = pages.filter(pg => pg.url() !== 'about:blank');
|
|
304
|
+
if (nonBlankPages.length > 0) {
|
|
305
|
+
pages = nonBlankPages;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (pages.length === 0) {
|
|
309
|
+
log.warn('No user tabs found');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (pages.length === 1) {
|
|
314
|
+
log.info('Only one user tab available');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
isSelectingTab = true;
|
|
319
|
+
const selectedPage = await askUserToSelectPage(pages);
|
|
320
|
+
isSelectingTab = false;
|
|
321
|
+
|
|
322
|
+
if (selectedPage === null) {
|
|
323
|
+
log.dim('Tab switch cancelled');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clear old monitoring and setup new
|
|
328
|
+
monitoredPages = [selectedPage];
|
|
329
|
+
setupPageMonitoring(selectedPage, 'Page');
|
|
330
|
+
log.success(`Now monitoring: ${C.brightCyan}${selectedPage.url()}${C.reset}`);
|
|
331
|
+
|
|
332
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
333
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
334
|
+
|
|
335
|
+
} catch (e) {
|
|
336
|
+
log.error(`Error switching tabs: ${e.message}`);
|
|
337
|
+
isSelectingTab = false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ===== MAIN =====
|
|
342
|
+
// Detect WSL and show setup instructions proactively
|
|
343
|
+
const isWSL = (() => {
|
|
344
|
+
try {
|
|
345
|
+
const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
346
|
+
return release.includes('microsoft') || release.includes('wsl');
|
|
347
|
+
} catch { return false; }
|
|
348
|
+
})();
|
|
349
|
+
|
|
350
|
+
// Track actual port to use (may change if auto-selecting free port)
|
|
351
|
+
let actualPort = port;
|
|
352
|
+
|
|
353
|
+
if (isWSL) {
|
|
354
|
+
writeStatusLine(`${C.dim}Detecting Chrome...${C.reset}`);
|
|
355
|
+
// Chrome Canary required for launching (isolated from regular Chrome singleton)
|
|
356
|
+
const chromePath = detectWindowsChromeCanaryPath();
|
|
357
|
+
|
|
358
|
+
// Profile path (same logic as open-mode: WSL → Windows LOCALAPPDATA, native → project dir)
|
|
359
|
+
const profileLoc = getChromeProfileLocation(outputDir);
|
|
360
|
+
const projectName = path.basename(outputDir);
|
|
361
|
+
|
|
362
|
+
const { instances, chromeRunning } = scanChromeInstances();
|
|
363
|
+
const projectMatch = findProjectChrome(instances, outputDir);
|
|
364
|
+
|
|
365
|
+
// Show block only when Chrome not found / not reachable (errors)
|
|
366
|
+
const showSetupBlock = !projectMatch.found || !chromePath;
|
|
367
|
+
if (showSetupBlock) {
|
|
368
|
+
clearStatusLine();
|
|
369
|
+
console.log('');
|
|
370
|
+
log.section('Join (WSL)');
|
|
371
|
+
console.log(` ${C.cyan}Project${C.reset} ${C.brightCyan}${projectName}${C.reset} ${C.cyan}Profile${C.reset} ${C.dim}${profileLoc.path}${C.reset}`);
|
|
372
|
+
if (instances.length > 0) {
|
|
373
|
+
const line = instances.map((inst) => {
|
|
374
|
+
const isOurs = projectMatch.found && projectMatch.instance === inst;
|
|
375
|
+
const mark = isOurs ? `${C.green}*${C.reset}` : '';
|
|
376
|
+
return `port ${inst.port}${mark}`;
|
|
377
|
+
}).join(', ');
|
|
378
|
+
console.log(` ${C.cyan}Instances${C.reset} ${line}`);
|
|
379
|
+
} else if (chromeRunning) {
|
|
380
|
+
console.log(` ${C.yellow}Chrome running without debug port${C.reset}`);
|
|
381
|
+
} else {
|
|
382
|
+
console.log(` ${C.dim}Chrome not running${C.reset}`);
|
|
383
|
+
}
|
|
384
|
+
console.log('');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Decide what to do based on status
|
|
388
|
+
let shouldWaitForUser = false;
|
|
389
|
+
let shouldLaunchChrome = false;
|
|
390
|
+
let waitMessageContent = '';
|
|
391
|
+
|
|
392
|
+
if (projectMatch.found && projectMatch.instance) {
|
|
393
|
+
// Found Chrome with our project's profile - test if actually reachable
|
|
394
|
+
actualPort = projectMatch.instance.port;
|
|
395
|
+
writeStatusLine(`${C.dim}Checking connection to port ${actualPort}...${C.reset}`);
|
|
396
|
+
|
|
397
|
+
// Actually test connectivity (don't trust bindAddress from command line)
|
|
398
|
+
let isReachable = false;
|
|
399
|
+
try {
|
|
400
|
+
const testUrl = `http://${connectHost}:${actualPort}/json/version`;
|
|
401
|
+
const response = await fetch(testUrl, { signal: AbortSignal.timeout(2000) });
|
|
402
|
+
isReachable = response.ok;
|
|
403
|
+
} catch {
|
|
404
|
+
isReachable = false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (isReachable) {
|
|
408
|
+
clearStatusLine();
|
|
409
|
+
// Silent – compact block shown after connect
|
|
410
|
+
} else {
|
|
411
|
+
clearStatusLine();
|
|
412
|
+
// Chrome exists but not reachable from WSL - port proxy needed
|
|
413
|
+
shouldWaitForUser = true;
|
|
414
|
+
waitMessageContent = [
|
|
415
|
+
`${C.yellow}⚠ Chrome found but not accessible from WSL${C.reset}`,
|
|
416
|
+
`${C.dim}Port proxy required (Chrome M113+ binds to 127.0.0.1 only)${C.reset}`,
|
|
417
|
+
'',
|
|
418
|
+
`${C.yellow}Close Chrome and re-run to retry.${C.reset}`,
|
|
419
|
+
].join('\n');
|
|
420
|
+
}
|
|
421
|
+
} else if (chromePath) {
|
|
422
|
+
actualPort = findFreeDebugPort(instances, port);
|
|
423
|
+
clearStatusLine();
|
|
424
|
+
console.log(` ${C.yellow}No Chrome for this project.${C.reset} Port ${actualPort}, profile ${C.dim}${profileLoc.path}${C.reset}`);
|
|
425
|
+
shouldLaunchChrome = await askYesNo(` Launch Chrome Canary for this project?`);
|
|
426
|
+
} else {
|
|
427
|
+
// Chrome Canary not installed
|
|
428
|
+
actualPort = findFreeDebugPort(instances, port);
|
|
429
|
+
clearStatusLine();
|
|
430
|
+
printCanaryInstallInstructions();
|
|
431
|
+
log.info('Install Chrome Canary and try again.');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Launch Chrome if needed
|
|
436
|
+
if (shouldLaunchChrome && chromePath) {
|
|
437
|
+
// Check if port proxy is blocking our port and remove it
|
|
438
|
+
if (isPortBlocked(actualPort)) {
|
|
439
|
+
clearStatusLine();
|
|
440
|
+
log.info(`Port ${actualPort} is in use, checking for port proxy...`);
|
|
441
|
+
const removed = removePortProxyIfExists(actualPort);
|
|
442
|
+
if (!removed && isPortBlocked(actualPort)) {
|
|
443
|
+
// Port is still blocked - try next free port
|
|
444
|
+
clearStatusLine();
|
|
445
|
+
log.warn(`Port ${actualPort} is blocked, trying next available...`);
|
|
446
|
+
actualPort = findFreeDebugPort(instances, actualPort + 1);
|
|
447
|
+
// Check the new port too
|
|
448
|
+
if (isPortBlocked(actualPort)) {
|
|
449
|
+
removePortProxyIfExists(actualPort);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Kill any existing Chrome with browsermonitor profile to prevent singleton hijacking
|
|
455
|
+
killPuppeteerMonitorChromes();
|
|
456
|
+
|
|
457
|
+
clearStatusLine();
|
|
458
|
+
log.info(`Launching Chrome on port ${actualPort}...`);
|
|
459
|
+
const launched = startChromeOnWindows(chromePath, actualPort, profileLoc.path);
|
|
460
|
+
if (launched) {
|
|
461
|
+
writeStatusLine(`${C.dim}Waiting for Chrome to start...${C.reset}`);
|
|
462
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
463
|
+
clearStatusLine();
|
|
464
|
+
} else {
|
|
465
|
+
log.error('Failed to launch Chrome automatically');
|
|
466
|
+
shouldWaitForUser = true;
|
|
467
|
+
waitMessageContent = `${C.yellow}Failed to launch Chrome automatically.${C.reset}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Wait for user if needed
|
|
472
|
+
if (shouldWaitForUser) {
|
|
473
|
+
clearStatusLine();
|
|
474
|
+
const content = buildWaitForChromeContent(waitMessageContent);
|
|
475
|
+
const table = createTable({ colWidths: [72], tableOpts: { wordWrap: true } });
|
|
476
|
+
table.push([content]);
|
|
477
|
+
printTable(table);
|
|
478
|
+
await new Promise((resolve) => {
|
|
479
|
+
process.stdin.setRawMode(false);
|
|
480
|
+
process.stdin.resume();
|
|
481
|
+
process.stdin.once('data', () => {
|
|
482
|
+
resolve();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
console.log('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Use actual port for connection (may have been changed by auto-detection)
|
|
491
|
+
const finalBrowserURL = `http://${connectHost}:${actualPort}`;
|
|
492
|
+
currentBrowserURL = finalBrowserURL;
|
|
493
|
+
|
|
494
|
+
if (realtime) {
|
|
495
|
+
fs.writeFileSync(logBuffer.CONSOLE_LOG, '');
|
|
496
|
+
logBuffer.clearNetworkDir();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// HTTP server is already started via createHttpServer above
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
writeStatusLine(`${C.dim}Connecting to browser...${C.reset}`);
|
|
503
|
+
// Connect to existing Chrome instance via Chrome DevTools Protocol (CDP).
|
|
504
|
+
// defaultViewport: null preserves the browser's actual viewport size.
|
|
505
|
+
// Without this, Puppeteer would resize the page to its default (800x600),
|
|
506
|
+
// causing the page content to shrink unexpectedly.
|
|
507
|
+
browser = await puppeteer.connect({ browserURL: finalBrowserURL, defaultViewport: null });
|
|
508
|
+
|
|
509
|
+
writeStatusLine(`${C.dim}Loading pages...${C.reset}`);
|
|
510
|
+
const allPages = await browser.pages();
|
|
511
|
+
|
|
512
|
+
// Filter out internal/dev pages that are not useful for monitoring
|
|
513
|
+
const isUserPage = (page) => {
|
|
514
|
+
const url = page.url();
|
|
515
|
+
const title = page.title ? page.title() : '';
|
|
516
|
+
|
|
517
|
+
// Skip Chrome internal pages
|
|
518
|
+
if (url.startsWith('chrome://')) return false;
|
|
519
|
+
if (url.startsWith('chrome-extension://')) return false;
|
|
520
|
+
if (url.startsWith('devtools://')) return false;
|
|
521
|
+
if (url === 'about:blank') return false;
|
|
522
|
+
|
|
523
|
+
// Skip React/Redux DevTools and similar extensions
|
|
524
|
+
if (url.includes('react-devtools') || url.includes('redux-devtools')) return false;
|
|
525
|
+
if (url.includes('__react_devtools__')) return false;
|
|
526
|
+
|
|
527
|
+
// Skip extension pages by pattern
|
|
528
|
+
if (/^moz-extension:\/\//.test(url)) return false; // Firefox extensions
|
|
529
|
+
if (/^extension:\/\//.test(url)) return false;
|
|
530
|
+
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const userPages = allPages.filter(isUserPage);
|
|
535
|
+
const pages = userPages.length > 0 ? userPages : allPages; // Fallback to all if no user pages
|
|
536
|
+
|
|
537
|
+
if (pages.length === 0) {
|
|
538
|
+
clearStatusLine();
|
|
539
|
+
log.error('No tabs found in browser');
|
|
540
|
+
cleanup(1);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let selectedPage;
|
|
545
|
+
if (pages.length === 1) {
|
|
546
|
+
selectedPage = pages[0];
|
|
547
|
+
clearStatusLine();
|
|
548
|
+
} else {
|
|
549
|
+
clearStatusLine();
|
|
550
|
+
log.section('Tab Selection');
|
|
551
|
+
console.log(` ${C.cyan}Tabs${C.reset} ${pages.length} – select which to monitor:`);
|
|
552
|
+
selectedPage = await askUserToSelectPage(pages);
|
|
553
|
+
if (!selectedPage) {
|
|
554
|
+
log.dim('Using first tab.');
|
|
555
|
+
selectedPage = pages[0];
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
monitoredPages = [selectedPage];
|
|
560
|
+
setupPageMonitoring(selectedPage, 'Page');
|
|
561
|
+
|
|
562
|
+
if (isWSL) {
|
|
563
|
+
printJoinConnectedBlock(connectHost, selectedPage.url());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Watch for new tabs
|
|
567
|
+
browser.on('targetcreated', async (target) => {
|
|
568
|
+
if (target.type() === 'page') {
|
|
569
|
+
const newPage = await target.page();
|
|
570
|
+
if (newPage) log.dim(`New tab: ${newPage.url()}`);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
logBuffer.printConsoleSeparator('CONNECTED - Listening for console output');
|
|
575
|
+
logBuffer.printNetworkSeparator('CONNECTED - Listening for network requests');
|
|
576
|
+
|
|
577
|
+
clearStatusLine(true);
|
|
578
|
+
// Ready block from template (same as periodic reminder)
|
|
579
|
+
printReadyHelp(httpPort, KEYS_JOIN);
|
|
580
|
+
setupKeyboardInput();
|
|
581
|
+
|
|
582
|
+
await new Promise(() => {});
|
|
583
|
+
} catch (e) {
|
|
584
|
+
clearStatusLine(true);
|
|
585
|
+
if (e.message.includes('ECONNREFUSED') || e.message.includes('fetch failed') || e.message.includes('ETIMEDOUT') || e.message.includes('timeout')) {
|
|
586
|
+
console.log('');
|
|
587
|
+
log.error(`Cannot connect to Chrome at ${C.brightRed}${currentBrowserURL}${C.reset}`);
|
|
588
|
+
console.log('');
|
|
589
|
+
|
|
590
|
+
// Check if we're in WSL - if so, run diagnostics
|
|
591
|
+
const isWSL = (() => {
|
|
592
|
+
try {
|
|
593
|
+
const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
594
|
+
return release.includes('microsoft') || release.includes('wsl');
|
|
595
|
+
} catch { return false; }
|
|
596
|
+
})();
|
|
597
|
+
|
|
598
|
+
if (isWSL) {
|
|
599
|
+
// Run comprehensive WSL diagnostics
|
|
600
|
+
const diagResult = await runWslDiagnostics(port, connectHost);
|
|
601
|
+
|
|
602
|
+
// Handle port proxy conflict automatically
|
|
603
|
+
if (diagResult.hasPortProxyConflict) {
|
|
604
|
+
console.log('');
|
|
605
|
+
console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
606
|
+
console.log(`${C.bold}${C.green} AUTOMATIC FIX${C.reset}`);
|
|
607
|
+
console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
608
|
+
console.log('');
|
|
609
|
+
|
|
610
|
+
const shouldFix = await askYesNo('Do you want me to fix this automatically? (remove port proxy, restart Chrome)');
|
|
611
|
+
|
|
612
|
+
if (shouldFix) {
|
|
613
|
+
const fixPort = diagResult.actualPort || port;
|
|
614
|
+
|
|
615
|
+
console.log(`${C.cyan}[1/2]${C.reset} Removing port proxy for port ${fixPort}...`);
|
|
616
|
+
try {
|
|
617
|
+
execSync(`netsh.exe interface portproxy delete v4tov4 listenport=${fixPort} listenaddress=0.0.0.0`, { encoding: 'utf8', timeout: 5000 });
|
|
618
|
+
console.log(` ${C.green}✓${C.reset} Port proxy removed`);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.log(` ${C.yellow}!${C.reset} Could not remove port proxy (may need admin): ${err.message}`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
console.log(`${C.cyan}[2/2]${C.reset} Stopping Chrome...`);
|
|
624
|
+
try {
|
|
625
|
+
killPuppeteerMonitorChromes(true); // Only kill browsermonitor Chrome, not user's browser!
|
|
626
|
+
console.log(` ${C.green}✓${C.reset} Chrome stopped`);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.log(` ${C.yellow}!${C.reset} Could not stop Chrome: ${err.message}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
console.log('');
|
|
632
|
+
console.log(`${C.green}Fix applied!${C.reset} Please run browsermonitor again.`);
|
|
633
|
+
console.log(`${C.dim}Chrome will now bind to 0.0.0.0 correctly (no port proxy needed).${C.reset}`);
|
|
634
|
+
console.log('');
|
|
635
|
+
process.exit(0);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// Non-WSL: show basic help
|
|
640
|
+
console.log(` ${C.yellow}Make sure Chrome is running with remote debugging enabled:${C.reset}`);
|
|
641
|
+
console.log(` ${C.dim}Windows:${C.reset} ${C.cyan}chrome.exe --remote-debugging-port=${port}${C.reset}`);
|
|
642
|
+
console.log(` ${C.dim}Linux:${C.reset} ${C.cyan}google-chrome --remote-debugging-port=${port}${C.reset}`);
|
|
643
|
+
console.log(` ${C.dim}Mac:${C.reset} ${C.cyan}/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=${port}${C.reset}`);
|
|
644
|
+
console.log('');
|
|
645
|
+
console.log(` ${C.yellow}If connecting from remote server, create SSH reverse tunnel first:${C.reset}`);
|
|
646
|
+
console.log(` ${C.cyan}ssh -R ${port}:localhost:${port} user@this-server${C.reset}`);
|
|
647
|
+
console.log('');
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
log.error(e.message);
|
|
651
|
+
}
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
}
|