@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,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Mode - launch new Chrome and monitor.
|
|
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 {
|
|
12
|
+
getWindowsHostForWSL,
|
|
13
|
+
getLastCmdStderrAndClear,
|
|
14
|
+
getWindowsLocalAppData,
|
|
15
|
+
detectWindowsChromeCanaryPath,
|
|
16
|
+
printCanaryInstallInstructions,
|
|
17
|
+
killPuppeteerMonitorChromes,
|
|
18
|
+
checkChromeRunning,
|
|
19
|
+
runWslDiagnostics,
|
|
20
|
+
} from '../os/wsl/index.mjs';
|
|
21
|
+
import { LogBuffer, getTimestamp, getFullTimestamp } from '../logging/index.mjs';
|
|
22
|
+
import { createHttpServer } from '../http-server.mjs';
|
|
23
|
+
import { setupPageMonitoring as setupPageMonitoringShared } from './page-monitoring.mjs';
|
|
24
|
+
import { askUserToSelectPage } from './tab-selection.mjs';
|
|
25
|
+
import { askYesNo } from '../utils/ask.mjs';
|
|
26
|
+
import { printReadyHelp, printStatusBlock, KEYS_OPEN } from '../templates/ready-help.mjs';
|
|
27
|
+
import { printApiHelpTable } from '../templates/api-help.mjs';
|
|
28
|
+
import { printModeHeading, printBulletBox } from '../templates/section-heading.mjs';
|
|
29
|
+
import { createTable, printTable } from '../templates/table-helper.mjs';
|
|
30
|
+
import { writeStatusLine, clearStatusLine } from '../utils/status-line.mjs';
|
|
31
|
+
import { getProfileIdFromProjectDir } from '../utils/profile-id.mjs';
|
|
32
|
+
|
|
33
|
+
// Browser and page in module scope for cleanup
|
|
34
|
+
let browser = null;
|
|
35
|
+
let page = null;
|
|
36
|
+
let cleanupDone = false;
|
|
37
|
+
let launchedOnWindows = false;
|
|
38
|
+
let windowsDebugPort = 0;
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run in Open Mode - launch new Chrome and monitor
|
|
43
|
+
* @param {string} url - URL to monitor
|
|
44
|
+
* @param {Object} options - Monitor options
|
|
45
|
+
* @param {boolean} options.realtime - Enable realtime mode (default: false = lazy mode)
|
|
46
|
+
* @param {boolean} options.headless - Run in headless mode (default: false = GUI mode)
|
|
47
|
+
* @param {string} options.outputDir - Output directory (default: process.cwd())
|
|
48
|
+
* @param {string[]} options.ignorePatterns - Additional patterns to ignore in console
|
|
49
|
+
* @param {number} options.hardTimeout - Hard timeout in ms (default: 0 = disabled)
|
|
50
|
+
* @param {number} options.defaultTimeout - Default page timeout in ms (default: 30000)
|
|
51
|
+
* @param {number} options.navigationTimeout - Navigation timeout in ms (default: 60000)
|
|
52
|
+
* @param {number} options.httpPort - HTTP server port for dump endpoint (default: 60001, 0 = disabled)
|
|
53
|
+
*/
|
|
54
|
+
export async function runOpenMode(url, options = {}) {
|
|
55
|
+
const {
|
|
56
|
+
realtime = false,
|
|
57
|
+
headless = false,
|
|
58
|
+
outputDir = process.cwd(),
|
|
59
|
+
paths = null,
|
|
60
|
+
ignorePatterns = [],
|
|
61
|
+
hardTimeout = 0,
|
|
62
|
+
defaultTimeout = 30_000,
|
|
63
|
+
navigationTimeout = 60_000,
|
|
64
|
+
httpPort = 60001,
|
|
65
|
+
sharedHttpState = null,
|
|
66
|
+
sharedHttpServer = null,
|
|
67
|
+
skipProfileBlock = false,
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
const lazyMode = !realtime;
|
|
71
|
+
|
|
72
|
+
// Create LogBuffer instance for centralized buffer management
|
|
73
|
+
const logBuffer = new LogBuffer({
|
|
74
|
+
outputDir,
|
|
75
|
+
paths,
|
|
76
|
+
lazyMode,
|
|
77
|
+
ignorePatterns,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Chrome profile: stays in project dir or Windows LOCALAPPDATA (not inside .browsermonitor)
|
|
81
|
+
const USER_DATA_DIR = path.join(outputDir, '.browsermonitor-profile');
|
|
82
|
+
// PID file goes into .browsermonitor/ when paths available
|
|
83
|
+
const PID_FILE = paths ? paths.pidFile : path.join(outputDir, '.browsermonitor', 'browsermonitor.pid');
|
|
84
|
+
|
|
85
|
+
// HTTP server for LLM dump endpoint
|
|
86
|
+
let httpServer = null;
|
|
87
|
+
|
|
88
|
+
// ===== CLEANUP FUNCTION =====
|
|
89
|
+
// closeBrowser: true = k (close Chrome and exit), false = q / Ctrl+C (exit only, Chrome keeps running)
|
|
90
|
+
async function cleanup(code = 0, closeBrowser = false) {
|
|
91
|
+
if (cleanupDone) return;
|
|
92
|
+
cleanupDone = true;
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
log.info(closeBrowser ? 'Cleaning up and closing Chrome...' : 'Cleaning up (Chrome will stay open)...');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (httpServer) {
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
httpServer.close(resolve);
|
|
101
|
+
});
|
|
102
|
+
log.dim('HTTP server closed');
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
log.error(`Error closing HTTP server: ${e.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (browser) {
|
|
110
|
+
if (closeBrowser) {
|
|
111
|
+
if (launchedOnWindows) {
|
|
112
|
+
browser.disconnect();
|
|
113
|
+
try {
|
|
114
|
+
const killed = killPuppeteerMonitorChromes(true);
|
|
115
|
+
if (killed > 0) {
|
|
116
|
+
log.success('Chrome closed');
|
|
117
|
+
} else {
|
|
118
|
+
log.dim('Chrome may have already closed');
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
log.dim('Chrome may have already closed');
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
await browser.close();
|
|
125
|
+
log.success('Browser closed');
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
browser.disconnect();
|
|
129
|
+
log.success('Disconnected (Chrome still running)');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
log.error(`Error closing browser: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
fs.unlinkSync(PID_FILE);
|
|
138
|
+
} catch {}
|
|
139
|
+
|
|
140
|
+
process.exit(code);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===== SIGNAL HANDLERS =====
|
|
144
|
+
process.on('SIGINT', () => {
|
|
145
|
+
console.log('');
|
|
146
|
+
log.dim('Received SIGINT (Ctrl+C)');
|
|
147
|
+
cleanup(0, false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
process.on('SIGTERM', () => {
|
|
151
|
+
console.log('');
|
|
152
|
+
log.dim('Received SIGTERM');
|
|
153
|
+
cleanup(0, false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
process.on('uncaughtException', (e) => {
|
|
157
|
+
const msg = (e && e.message) || String(e);
|
|
158
|
+
if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
|
|
159
|
+
log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
log.error(`Uncaught exception: ${e.message}`);
|
|
163
|
+
console.error(e.stack);
|
|
164
|
+
cleanup(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
process.on('unhandledRejection', (e) => {
|
|
168
|
+
const msg = (e && e.message) || String(e);
|
|
169
|
+
if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
|
|
170
|
+
log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
log.error(`Unhandled rejection: ${e}`);
|
|
174
|
+
cleanup(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ===== HARD TIMEOUT (safety net) =====
|
|
178
|
+
if (hardTimeout > 0) {
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
log.error(`HARD TIMEOUT (${hardTimeout}ms) - forcing exit`);
|
|
181
|
+
cleanup(1);
|
|
182
|
+
}, hardTimeout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Track monitored pages for tab switching
|
|
186
|
+
let monitoredPages = [];
|
|
187
|
+
let isSelectingTab = false;
|
|
188
|
+
let currentProfilePath = null;
|
|
189
|
+
let activePageCleanup = null;
|
|
190
|
+
let collectingPaused = false;
|
|
191
|
+
|
|
192
|
+
// Output counter for periodic help reminder (same block as initial Ready, every HELP_INTERVAL)
|
|
193
|
+
let outputCounter = 0;
|
|
194
|
+
const HELP_INTERVAL = 5;
|
|
195
|
+
function maybeShowHelp() {
|
|
196
|
+
outputCounter++;
|
|
197
|
+
if (outputCounter % HELP_INTERVAL === 0) {
|
|
198
|
+
printReadyHelp(httpPort, KEYS_OPEN);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// In-session help (h key) – full table with session context for Claude Code
|
|
203
|
+
function printHelp() {
|
|
204
|
+
const currentUrl = page?.url?.() || url;
|
|
205
|
+
console.log(`${C.cyan}Launch mode${C.reset} URL: ${C.brightGreen}${currentUrl}${C.reset} │ Dir: ${outputDir}`);
|
|
206
|
+
printApiHelpTable({
|
|
207
|
+
port: httpPort,
|
|
208
|
+
showApi: true,
|
|
209
|
+
showInteractive: true,
|
|
210
|
+
showOutputFiles: true,
|
|
211
|
+
context: {
|
|
212
|
+
consoleLog: logBuffer.CONSOLE_LOG,
|
|
213
|
+
networkLog: logBuffer.NETWORK_LOG,
|
|
214
|
+
networkDir: logBuffer.NETWORK_DIR,
|
|
215
|
+
cookiesDir: logBuffer.COOKIES_DIR,
|
|
216
|
+
domHtml: logBuffer.DOM_HTML,
|
|
217
|
+
screenshot: logBuffer.SCREENSHOT,
|
|
218
|
+
},
|
|
219
|
+
sessionContext: {
|
|
220
|
+
currentUrl,
|
|
221
|
+
profilePath: currentProfilePath,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Use shared HTTP server (started in CLI) or create our own
|
|
227
|
+
if (sharedHttpState && sharedHttpServer) {
|
|
228
|
+
httpServer = sharedHttpServer;
|
|
229
|
+
sharedHttpState.mode = 'launch';
|
|
230
|
+
sharedHttpState.logBuffer = logBuffer;
|
|
231
|
+
sharedHttpState.getPages = () => monitoredPages;
|
|
232
|
+
sharedHttpState.getCollectingPaused = () => collectingPaused;
|
|
233
|
+
sharedHttpState.setCollectingPaused = (v) => { collectingPaused = !!v; };
|
|
234
|
+
sharedHttpState.getAllTabs = async () => {
|
|
235
|
+
if (!browser) return [];
|
|
236
|
+
const allPages = await browser.pages();
|
|
237
|
+
const isUserPage = (p) => {
|
|
238
|
+
const u = p.url();
|
|
239
|
+
return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
|
|
240
|
+
};
|
|
241
|
+
let pages = allPages.filter(isUserPage);
|
|
242
|
+
const nonBlank = pages.filter(p => p.url() !== 'about:blank');
|
|
243
|
+
if (nonBlank.length > 0) pages = nonBlank;
|
|
244
|
+
return pages.map((p, i) => ({ index: i + 1, url: p.url() }));
|
|
245
|
+
};
|
|
246
|
+
sharedHttpState.switchToTab = async (index) => {
|
|
247
|
+
if (!browser) return { success: false, error: 'Browser not ready' };
|
|
248
|
+
try {
|
|
249
|
+
const allPages = await browser.pages();
|
|
250
|
+
const isUserPage = (p) => {
|
|
251
|
+
const u = p.url();
|
|
252
|
+
return !u.startsWith('chrome://') && !u.startsWith('devtools://') && !u.startsWith('chrome-extension://');
|
|
253
|
+
};
|
|
254
|
+
let pages = allPages.filter(isUserPage);
|
|
255
|
+
const nonBlank = pages.filter(p => p.url() !== 'about:blank');
|
|
256
|
+
if (nonBlank.length > 0) pages = nonBlank;
|
|
257
|
+
if (index < 1 || index > pages.length) return { success: false, error: `Invalid index. Use 1-${pages.length}.` };
|
|
258
|
+
const selectedPage = pages[index - 1];
|
|
259
|
+
page = selectedPage;
|
|
260
|
+
monitoredPages = [selectedPage];
|
|
261
|
+
setupPageMonitoring(selectedPage);
|
|
262
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
263
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
264
|
+
return { success: true, url: selectedPage.url() };
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return { success: false, error: e.message };
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
} else {
|
|
270
|
+
httpServer = createHttpServer({
|
|
271
|
+
port: httpPort,
|
|
272
|
+
mode: 'launch',
|
|
273
|
+
logBuffer,
|
|
274
|
+
getPages: () => monitoredPages,
|
|
275
|
+
getCollectingPaused: () => collectingPaused,
|
|
276
|
+
setCollectingPaused: (v) => { collectingPaused = !!v; },
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Setup page monitoring (console, request, response events) – shared implementation
|
|
281
|
+
function setupPageMonitoring(targetPage) {
|
|
282
|
+
setupPageMonitoringShared(targetPage, {
|
|
283
|
+
logBuffer,
|
|
284
|
+
getCollectingPaused: () => collectingPaused,
|
|
285
|
+
setActivePageCleanup: (fn) => { activePageCleanup = fn; },
|
|
286
|
+
pageLabel: '',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Switch tabs in Launch mode
|
|
291
|
+
async function switchTabs() {
|
|
292
|
+
if (!browser) {
|
|
293
|
+
log.error('Browser not ready');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const allPages = await browser.pages();
|
|
299
|
+
|
|
300
|
+
// Filter out internal Chrome pages (but keep about:blank if it's the only one with content)
|
|
301
|
+
const isUserPage = (p) => {
|
|
302
|
+
const pageUrl = p.url();
|
|
303
|
+
return !pageUrl.startsWith('chrome://') &&
|
|
304
|
+
!pageUrl.startsWith('devtools://') &&
|
|
305
|
+
!pageUrl.startsWith('chrome-extension://');
|
|
306
|
+
};
|
|
307
|
+
let pages = allPages.filter(isUserPage);
|
|
308
|
+
|
|
309
|
+
// If we have pages other than about:blank, filter out about:blank
|
|
310
|
+
const nonBlankPages = pages.filter(p => p.url() !== 'about:blank');
|
|
311
|
+
if (nonBlankPages.length > 0) {
|
|
312
|
+
pages = nonBlankPages;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (pages.length === 0) {
|
|
316
|
+
log.warn('No user tabs found');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (pages.length === 1) {
|
|
321
|
+
log.info('Only one user tab available');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
isSelectingTab = true;
|
|
326
|
+
const selectedPage = await askUserToSelectPage(pages);
|
|
327
|
+
isSelectingTab = false;
|
|
328
|
+
|
|
329
|
+
if (selectedPage === null) {
|
|
330
|
+
log.dim('Tab switch cancelled');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Switch to new page and setup monitoring
|
|
335
|
+
page = selectedPage;
|
|
336
|
+
monitoredPages = [selectedPage];
|
|
337
|
+
setupPageMonitoring(selectedPage);
|
|
338
|
+
log.success(`Now monitoring: ${C.brightCyan}${selectedPage.url()}${C.reset}`);
|
|
339
|
+
|
|
340
|
+
// Add separators to indicate tab switch in logs
|
|
341
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
342
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
343
|
+
|
|
344
|
+
} catch (e) {
|
|
345
|
+
log.error(`Error switching tabs: ${e.message}`);
|
|
346
|
+
isSelectingTab = false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Setup keyboard input for lazy mode
|
|
351
|
+
function setupKeyboardInput() {
|
|
352
|
+
readline.emitKeypressEvents(process.stdin);
|
|
353
|
+
if (process.stdin.isTTY) {
|
|
354
|
+
process.stdin.setRawMode(true);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
358
|
+
if (key.ctrl && key.name === 'c') {
|
|
359
|
+
cleanup(0);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Ignore keys during tab selection
|
|
364
|
+
if (isSelectingTab) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Shortcuts only without modifiers (no Ctrl, Shift, Win)
|
|
369
|
+
if (key.ctrl || key.shift || key.meta) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (key.name === 'd') {
|
|
374
|
+
await logBuffer.dumpBuffersToFiles({
|
|
375
|
+
dumpCookies: page ? () => logBuffer.dumpCookiesFromPage(page) : null,
|
|
376
|
+
dumpDom: page ? () => logBuffer.dumpDomFromPage(page) : null,
|
|
377
|
+
dumpScreenshot: page ? () => logBuffer.dumpScreenshotFromPage(page) : null,
|
|
378
|
+
});
|
|
379
|
+
maybeShowHelp();
|
|
380
|
+
} else if (key.name === 'c') {
|
|
381
|
+
logBuffer.clearAllBuffers();
|
|
382
|
+
maybeShowHelp();
|
|
383
|
+
} else if (key.name === 'q') {
|
|
384
|
+
cleanup(0, false);
|
|
385
|
+
} else if (key.name === 's') {
|
|
386
|
+
const stats = logBuffer.getStats();
|
|
387
|
+
const currentUrl = page ? page.url() : 'N/A';
|
|
388
|
+
const tabCount = browser ? (await browser.pages()).length : 0;
|
|
389
|
+
printStatusBlock(stats, currentUrl, tabCount, collectingPaused);
|
|
390
|
+
maybeShowHelp();
|
|
391
|
+
} else if (key.name === 'p') {
|
|
392
|
+
collectingPaused = !collectingPaused;
|
|
393
|
+
log.info(collectingPaused ? 'Collecting stopped (paused). Press p or curl .../start to resume.' : 'Collecting started (resumed).');
|
|
394
|
+
maybeShowHelp();
|
|
395
|
+
} else if (key.name === 'k') {
|
|
396
|
+
log.warn('Closing Chrome and exiting...');
|
|
397
|
+
cleanup(0, true);
|
|
398
|
+
} else if (key.name === 't') {
|
|
399
|
+
await switchTabs();
|
|
400
|
+
maybeShowHelp();
|
|
401
|
+
} else if (key.name === 'h') {
|
|
402
|
+
// Show full help
|
|
403
|
+
printHelp();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Initialize
|
|
409
|
+
printModeHeading('Open mode');
|
|
410
|
+
if (realtime) {
|
|
411
|
+
fs.writeFileSync(logBuffer.CONSOLE_LOG, '');
|
|
412
|
+
logBuffer.clearNetworkDir();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
writeStatusLine(`${C.dim}Launching browser for ${url}...${C.reset}`);
|
|
416
|
+
const configLines = [
|
|
417
|
+
`${C.cyan}Configuration${C.reset}`,
|
|
418
|
+
` Mode: ${lazyMode ? `${C.green}LAZY${C.reset} (buffered)` : `${C.yellow}REALTIME${C.reset} (immediate write)`}`,
|
|
419
|
+
` Browser: ${headless ? `${C.dim}HEADLESS${C.reset}` : `${C.green}GUI${C.reset}`}`,
|
|
420
|
+
].join('\n');
|
|
421
|
+
clearStatusLine(true);
|
|
422
|
+
const configTable = createTable({ colWidths: [95], tableOpts: { wordWrap: true } });
|
|
423
|
+
configTable.push([configLines]);
|
|
424
|
+
printTable(configTable);
|
|
425
|
+
|
|
426
|
+
if (realtime) {
|
|
427
|
+
logBuffer.printNetworkSeparator('BROWSERMONITOR STARTED');
|
|
428
|
+
logBuffer.logNetwork(`URL: ${url}`);
|
|
429
|
+
logBuffer.logNetwork('');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// HTTP server is already started via createHttpServer above
|
|
433
|
+
|
|
434
|
+
// ===== DETECT WSL =====
|
|
435
|
+
const isWSL = (() => {
|
|
436
|
+
try {
|
|
437
|
+
const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
438
|
+
return release.includes('microsoft') || release.includes('wsl');
|
|
439
|
+
} catch { return false; }
|
|
440
|
+
})();
|
|
441
|
+
|
|
442
|
+
// ===== CONFIGURE CHROME PROFILE (only when Chrome will use USER_DATA_DIR) =====
|
|
443
|
+
// On WSL with project on WSL fs (/srv/...), Chrome uses Windows LOCALAPPDATA - skip creating Linux path
|
|
444
|
+
const chromeUsesUserDataDir = !isWSL || outputDir.startsWith('/mnt/');
|
|
445
|
+
if (chromeUsesUserDataDir) {
|
|
446
|
+
const prefsDir = path.join(USER_DATA_DIR, 'Default');
|
|
447
|
+
const prefsFile = path.join(prefsDir, 'Preferences');
|
|
448
|
+
try {
|
|
449
|
+
fs.mkdirSync(prefsDir, { recursive: true });
|
|
450
|
+
let prefs = {};
|
|
451
|
+
if (fs.existsSync(prefsFile)) {
|
|
452
|
+
try {
|
|
453
|
+
prefs = JSON.parse(fs.readFileSync(prefsFile, 'utf8'));
|
|
454
|
+
} catch { /* ignore parse errors, start fresh */ }
|
|
455
|
+
}
|
|
456
|
+
prefs.session = prefs.session || {};
|
|
457
|
+
prefs.session.restore_on_startup = 5;
|
|
458
|
+
prefs.profile = prefs.profile || {};
|
|
459
|
+
prefs.profile.exit_type = 'Normal';
|
|
460
|
+
fs.writeFileSync(prefsFile, JSON.stringify(prefs, null, 2));
|
|
461
|
+
} catch (e) {
|
|
462
|
+
log.dim(`Could not configure profile preferences: ${e.message}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ===== MAIN TRY/FINALLY BLOCK =====
|
|
467
|
+
try {
|
|
468
|
+
if (isWSL) {
|
|
469
|
+
// === WSL MODE: Launch Chrome on Windows via PowerShell ===
|
|
470
|
+
writeStatusLine(`${C.dim}Detecting Chrome...${C.reset}`);
|
|
471
|
+
|
|
472
|
+
// For launch mode we use only Chrome Canary (isolated from user's regular Chrome)
|
|
473
|
+
const chromePath = detectWindowsChromeCanaryPath();
|
|
474
|
+
if (!chromePath) {
|
|
475
|
+
clearStatusLine();
|
|
476
|
+
printCanaryInstallInstructions();
|
|
477
|
+
log.info('Install Chrome Canary and try again.');
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// In launch mode, kill any existing browsermonitor Chrome processes
|
|
482
|
+
clearStatusLine(true);
|
|
483
|
+
const killed = killPuppeteerMonitorChromes();
|
|
484
|
+
if (killed > 0) {
|
|
485
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check if Chrome is running - with or without debug port
|
|
489
|
+
const chromeStatus = checkChromeRunning();
|
|
490
|
+
let chromeAlreadyRunning = false;
|
|
491
|
+
let useExistingChrome = false;
|
|
492
|
+
let existingDebugPort = null;
|
|
493
|
+
|
|
494
|
+
if (chromeStatus.running) {
|
|
495
|
+
if (chromeStatus.withDebugPort && chromeStatus.debugPort) {
|
|
496
|
+
// Chrome is running WITH debug port - we can try to connect to it!
|
|
497
|
+
existingDebugPort = chromeStatus.debugPort;
|
|
498
|
+
|
|
499
|
+
// Check if it's accessible from WSL or needs port proxy
|
|
500
|
+
const connectHost = getWindowsHostForWSL();
|
|
501
|
+
try {
|
|
502
|
+
const testResponse = await fetch(`http://${connectHost}:${existingDebugPort}/json/version`, {
|
|
503
|
+
signal: AbortSignal.timeout(2000)
|
|
504
|
+
});
|
|
505
|
+
if (testResponse.ok) {
|
|
506
|
+
useExistingChrome = true;
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// Not accessible - check if port proxy would help
|
|
510
|
+
|
|
511
|
+
// Check if port proxy is already set up
|
|
512
|
+
try {
|
|
513
|
+
const portProxyList = execSync('netsh.exe interface portproxy show v4tov4 2>nul', { encoding: 'utf8', timeout: 5000 });
|
|
514
|
+
if (portProxyList.includes(String(existingDebugPort))) {
|
|
515
|
+
} else {
|
|
516
|
+
// Offer to set up port proxy
|
|
517
|
+
console.log('');
|
|
518
|
+
console.log(` ${C.yellow}Port proxy needed:${C.reset} Chrome is not accessible from WSL.`);
|
|
519
|
+
console.log(` ${C.dim}Run this in PowerShell (Admin) to fix:${C.reset}`);
|
|
520
|
+
console.log(` ${C.cyan}netsh interface portproxy add v4tov4 listenport=${existingDebugPort} listenaddress=0.0.0.0 connectport=${existingDebugPort} connectaddress=127.0.0.1${C.reset}`);
|
|
521
|
+
console.log('');
|
|
522
|
+
|
|
523
|
+
const shouldSetup = await askYesNo(` ${C.bold}Try to set up port proxy now? (requires admin)${C.reset}`);
|
|
524
|
+
if (shouldSetup) {
|
|
525
|
+
try {
|
|
526
|
+
// Try to run netsh (might need elevation)
|
|
527
|
+
execSync(`netsh.exe interface portproxy add v4tov4 listenport=${existingDebugPort} listenaddress=0.0.0.0 connectport=${existingDebugPort} connectaddress=127.0.0.1`, { encoding: 'utf8', timeout: 5000 });
|
|
528
|
+
// Test again
|
|
529
|
+
await new Promise(r => setTimeout(r, 500));
|
|
530
|
+
const retryResponse = await fetch(`http://${connectHost}:${existingDebugPort}/json/version`, {
|
|
531
|
+
signal: AbortSignal.timeout(2000)
|
|
532
|
+
});
|
|
533
|
+
if (retryResponse.ok) {
|
|
534
|
+
useExistingChrome = true;
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
log.warn('Could not set up port proxy (run PowerShell as Admin)');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
// netsh failed
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (useExistingChrome) {
|
|
547
|
+
windowsDebugPort = existingDebugPort;
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
// Chrome running WITHOUT debug port - singleton problem
|
|
551
|
+
chromeAlreadyRunning = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Skip launch if using existing Chrome
|
|
556
|
+
let windowsUserDataDir = 'existing';
|
|
557
|
+
|
|
558
|
+
if (!useExistingChrome) {
|
|
559
|
+
// Find available port starting from 9222
|
|
560
|
+
// Check if port is already in use on Windows
|
|
561
|
+
const findAvailablePort = () => {
|
|
562
|
+
const START_PORT = 9222;
|
|
563
|
+
const MAX_PORT = 9299;
|
|
564
|
+
for (let port = START_PORT; port <= MAX_PORT; port++) {
|
|
565
|
+
try {
|
|
566
|
+
// Check if port is listening on Windows
|
|
567
|
+
const checkCmd = `powershell.exe -NoProfile -Command "(Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue).Count"`;
|
|
568
|
+
const result = execSync(checkCmd, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
569
|
+
const count = parseInt(result, 10) || 0;
|
|
570
|
+
if (count === 0) {
|
|
571
|
+
return port; // Port is free
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
// Get-NetTCPConnection failed = port is likely free
|
|
575
|
+
return port;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// All ports in use, fallback to random
|
|
579
|
+
return START_PORT + Math.floor(Math.random() * (MAX_PORT - START_PORT));
|
|
580
|
+
};
|
|
581
|
+
windowsDebugPort = findAvailablePort();
|
|
582
|
+
|
|
583
|
+
// Get Windows user data dir
|
|
584
|
+
// Chrome has issues with \\wsl$\ paths for singleton lock detection
|
|
585
|
+
// So we store profiles on Windows filesystem with project-specific hash
|
|
586
|
+
if (USER_DATA_DIR.startsWith('/mnt/')) {
|
|
587
|
+
// Path is on Windows drive, convert it directly
|
|
588
|
+
windowsUserDataDir = USER_DATA_DIR.replace(/^\/mnt\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, '\\');
|
|
589
|
+
} else {
|
|
590
|
+
// Path is on WSL filesystem - use Windows LOCALAPPDATA with project-specific profile ID
|
|
591
|
+
const { profileId } = getProfileIdFromProjectDir(outputDir);
|
|
592
|
+
const localAppData = getWindowsLocalAppData();
|
|
593
|
+
windowsUserDataDir = `${localAppData}\\browsermonitor\\${profileId}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// CMD.EXE stderr + Profile/Project in bullet box
|
|
597
|
+
const cmdStderrLines = getLastCmdStderrAndClear();
|
|
598
|
+
const mergedStderr = [];
|
|
599
|
+
for (let i = 0; i < cmdStderrLines.length; i++) {
|
|
600
|
+
const curr = cmdStderrLines[i];
|
|
601
|
+
const next = cmdStderrLines[i + 1];
|
|
602
|
+
if (next && /^'\\\\/.test(curr) && /CMD\.EXE was started/i.test(next)) {
|
|
603
|
+
mergedStderr.push(`${next} ${curr}`);
|
|
604
|
+
i++;
|
|
605
|
+
} else {
|
|
606
|
+
mergedStderr.push(curr);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!skipProfileBlock) {
|
|
610
|
+
const infoLines = [
|
|
611
|
+
...mergedStderr,
|
|
612
|
+
`${C.cyan}Profile:${C.reset} ${windowsUserDataDir}`,
|
|
613
|
+
`${C.cyan}Project:${C.reset} ${outputDir}`,
|
|
614
|
+
];
|
|
615
|
+
if (infoLines.length > 0) {
|
|
616
|
+
clearStatusLine();
|
|
617
|
+
console.log('');
|
|
618
|
+
printBulletBox(infoLines);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
writeStatusLine(`${C.dim}Launching Chrome on Windows (port ${windowsDebugPort})...${C.reset}`);
|
|
623
|
+
|
|
624
|
+
// Launch Chrome on Windows with user data dir
|
|
625
|
+
// Note: Chrome M113+ always binds to 127.0.0.1 only for security
|
|
626
|
+
// See: https://issues.chromium.org/issues/40261787
|
|
627
|
+
// Port proxy is required for WSL access (set up after Chrome starts)
|
|
628
|
+
const psCommand = `Start-Process -FilePath '${chromePath}' -ArgumentList '--remote-debugging-port=${windowsDebugPort}','--user-data-dir=${windowsUserDataDir}','--disable-session-crashed-bubble','--start-maximized','${url}'`;
|
|
629
|
+
try {
|
|
630
|
+
execSync(`powershell.exe -NoProfile -Command "${psCommand}"`, { encoding: 'utf8', timeout: 10000 });
|
|
631
|
+
launchedOnWindows = true;
|
|
632
|
+
|
|
633
|
+
// Set up port proxy AFTER Chrome starts
|
|
634
|
+
clearStatusLine();
|
|
635
|
+
writeStatusLine(`${C.dim}Setting up port proxy for WSL access...${C.reset}`);
|
|
636
|
+
|
|
637
|
+
// Wait for Chrome to bind and detect address
|
|
638
|
+
let chromeBindAddr = null;
|
|
639
|
+
for (let i = 0; i < 10; i++) {
|
|
640
|
+
try { execSync('powershell.exe -NoProfile -Command "Start-Sleep -Milliseconds 500"', { timeout: 2000 }); } catch {}
|
|
641
|
+
try {
|
|
642
|
+
const netstat = execSync(`netstat.exe -ano`, { encoding: 'utf8', timeout: 5000 });
|
|
643
|
+
const lines = netstat.split('\n').filter(l => l.includes(':' + windowsDebugPort) && l.includes('LISTEN'));
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
if (line.includes('127.0.0.1:' + windowsDebugPort)) { chromeBindAddr = '127.0.0.1'; break; }
|
|
646
|
+
else if (line.includes('[::1]:' + windowsDebugPort)) { chromeBindAddr = '::1'; break; }
|
|
647
|
+
}
|
|
648
|
+
if (chromeBindAddr) break;
|
|
649
|
+
} catch {}
|
|
650
|
+
}
|
|
651
|
+
if (!chromeBindAddr) chromeBindAddr = '127.0.0.1';
|
|
652
|
+
|
|
653
|
+
const proxyType = chromeBindAddr === '::1' ? 'v4tov6' : 'v4tov4';
|
|
654
|
+
|
|
655
|
+
// Remove old proxies first
|
|
656
|
+
try {
|
|
657
|
+
execSync(`powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov4 listenport=${windowsDebugPort} listenaddress=0.0.0.0 2>\\$null; netsh interface portproxy delete v4tov6 listenport=${windowsDebugPort} listenaddress=0.0.0.0 2>\\$null"`, { encoding: 'utf8', timeout: 5000 });
|
|
658
|
+
} catch {}
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
execSync(
|
|
662
|
+
`powershell.exe -NoProfile -Command "netsh interface portproxy add ${proxyType} listenport=${windowsDebugPort} listenaddress=0.0.0.0 connectport=${windowsDebugPort} connectaddress=${chromeBindAddr}"`,
|
|
663
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
664
|
+
);
|
|
665
|
+
// Port proxy configured
|
|
666
|
+
} catch (proxyErr) {
|
|
667
|
+
log.warn(`Could not set up port proxy (may need admin): ${proxyErr.message}`);
|
|
668
|
+
}
|
|
669
|
+
} catch (e) {
|
|
670
|
+
log.error(`Failed to launch Chrome on Windows: ${e.message}`);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Wait for Chrome to start and connect with retry
|
|
676
|
+
clearStatusLine(true);
|
|
677
|
+
const connectHost = getWindowsHostForWSL({ quiet: true });
|
|
678
|
+
const browserURL = `http://${connectHost}:${windowsDebugPort}`;
|
|
679
|
+
const wslSetupLines = [
|
|
680
|
+
`${C.cyan}Open (WSL)${C.reset}`,
|
|
681
|
+
` ${C.yellow}WSL detected${C.reset} – Chrome on Windows (GPU/WebGL)`,
|
|
682
|
+
` Chrome Canary (isolated from regular Chrome)`,
|
|
683
|
+
chromeAlreadyRunning ? ` ${C.yellow}Chrome already running${C.reset} – new window joins existing process` : '',
|
|
684
|
+
` Port: ${windowsDebugPort} │ Connection: ${browserURL}`,
|
|
685
|
+
].filter(Boolean).join('\n');
|
|
686
|
+
const wslTable = createTable({ colWidths: [95], tableOpts: { wordWrap: true } });
|
|
687
|
+
wslTable.push([wslSetupLines]);
|
|
688
|
+
printTable(wslTable);
|
|
689
|
+
|
|
690
|
+
writeStatusLine(`${C.dim}Connecting to Chrome...${C.reset}`);
|
|
691
|
+
|
|
692
|
+
// Retry connection up to 5 times (total ~7.5 seconds)
|
|
693
|
+
// If it fails, diagnostics will show the exact problem
|
|
694
|
+
const MAX_RETRIES = 5;
|
|
695
|
+
const RETRY_DELAY = 1500;
|
|
696
|
+
let lastError = null;
|
|
697
|
+
|
|
698
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
699
|
+
try {
|
|
700
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY));
|
|
701
|
+
writeStatusLine(`${C.dim}Connecting to Chrome (attempt ${attempt}/${MAX_RETRIES})...${C.reset}`);
|
|
702
|
+
|
|
703
|
+
// First check if Chrome debug endpoint is reachable
|
|
704
|
+
try {
|
|
705
|
+
const versionUrl = `${browserURL}/json/version`;
|
|
706
|
+
const response = await fetch(versionUrl, { signal: AbortSignal.timeout(3000) });
|
|
707
|
+
if (response.ok) {
|
|
708
|
+
const info = await response.json();
|
|
709
|
+
writeStatusLine(`${C.dim}Chrome responding: ${info.Browser || 'unknown'}${C.reset}`);
|
|
710
|
+
}
|
|
711
|
+
} catch (fetchErr) {
|
|
712
|
+
throw new Error(`Cannot reach Chrome debug endpoint: ${fetchErr.message}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
browser = await puppeteer.connect({ browserURL, defaultViewport: null });
|
|
716
|
+
clearStatusLine();
|
|
717
|
+
lastError = null;
|
|
718
|
+
break;
|
|
719
|
+
} catch (e) {
|
|
720
|
+
lastError = e;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (lastError) {
|
|
725
|
+
log.error(`Failed to connect to Chrome after ${MAX_RETRIES} attempts: ${lastError.message}`);
|
|
726
|
+
|
|
727
|
+
// Run comprehensive WSL diagnostics
|
|
728
|
+
const diagResult = await runWslDiagnostics(windowsDebugPort, connectHost);
|
|
729
|
+
|
|
730
|
+
// Handle port proxy conflict automatically
|
|
731
|
+
if (diagResult.hasPortProxyConflict) {
|
|
732
|
+
console.log('');
|
|
733
|
+
console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
734
|
+
console.log(`${C.bold}${C.green} AUTOMATIC FIX${C.reset}`);
|
|
735
|
+
console.log(`${C.bold}${C.green}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
736
|
+
console.log('');
|
|
737
|
+
|
|
738
|
+
const shouldFix = await askYesNo('Do you want me to fix this automatically? (remove port proxy, restart Chrome)');
|
|
739
|
+
|
|
740
|
+
if (shouldFix) {
|
|
741
|
+
const fixPort = diagResult.actualPort || windowsDebugPort;
|
|
742
|
+
|
|
743
|
+
console.log(`${C.cyan}[1/2]${C.reset} Removing port proxy for port ${fixPort}...`);
|
|
744
|
+
try {
|
|
745
|
+
execSync(`netsh.exe interface portproxy delete v4tov4 listenport=${fixPort} listenaddress=0.0.0.0`, { encoding: 'utf8', timeout: 5000 });
|
|
746
|
+
console.log(` ${C.green}✓${C.reset} Port proxy removed`);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
console.log(` ${C.yellow}!${C.reset} Could not remove port proxy (may need admin): ${e.message}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
console.log(`${C.cyan}[2/2]${C.reset} Stopping Chrome...`);
|
|
752
|
+
try {
|
|
753
|
+
killPuppeteerMonitorChromes(true); // Only kill browsermonitor Chrome, not user's browser!
|
|
754
|
+
console.log(` ${C.green}✓${C.reset} Chrome stopped`);
|
|
755
|
+
} catch (e) {
|
|
756
|
+
console.log(` ${C.yellow}!${C.reset} Could not stop Chrome: ${e.message}`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
console.log('');
|
|
760
|
+
console.log(`${C.green}Fix applied!${C.reset} Please run browsermonitor again.`);
|
|
761
|
+
console.log(`${C.dim}Chrome will now bind to 0.0.0.0 correctly (no port proxy needed).${C.reset}`);
|
|
762
|
+
console.log('');
|
|
763
|
+
process.exit(0);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Additional context for Chrome singleton issue
|
|
768
|
+
if (chromeAlreadyRunning && !diagResult.hasPortProxyConflict) {
|
|
769
|
+
console.log(`${C.yellow}Note:${C.reset} Chrome was already running when we tried to launch.`);
|
|
770
|
+
console.log(' The new window joined the existing process without debug port.');
|
|
771
|
+
console.log('');
|
|
772
|
+
console.log(`${C.bold}Solution:${C.reset} Close ALL Chrome windows and try again.`);
|
|
773
|
+
console.log('');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const connectedLines = [
|
|
780
|
+
`${C.green}Connected to Chrome on Windows${C.reset}`,
|
|
781
|
+
` Chrome: Windows native (port ${windowsDebugPort})`,
|
|
782
|
+
`${C.dim}Note: Separate Chrome profile – you may need to log in to websites.${C.reset}`,
|
|
783
|
+
].join('\n');
|
|
784
|
+
const connectedTable = createTable({ colWidths: [95], tableOpts: { wordWrap: true } });
|
|
785
|
+
connectedTable.push([connectedLines]);
|
|
786
|
+
printTable(connectedTable);
|
|
787
|
+
currentProfilePath = windowsUserDataDir === 'existing' ? null : windowsUserDataDir;
|
|
788
|
+
|
|
789
|
+
} else {
|
|
790
|
+
// === NATIVE MODE: Standard Puppeteer launch ===
|
|
791
|
+
browser = await puppeteer.launch({
|
|
792
|
+
headless: headless ? 'new' : false,
|
|
793
|
+
userDataDir: USER_DATA_DIR,
|
|
794
|
+
args: [
|
|
795
|
+
'--no-sandbox',
|
|
796
|
+
'--disable-setuid-sandbox',
|
|
797
|
+
'--ignore-certificate-errors',
|
|
798
|
+
'--disable-gpu',
|
|
799
|
+
'--disable-software-rasterizer',
|
|
800
|
+
'--remote-debugging-port=0',
|
|
801
|
+
'--disable-session-crashed-bubble',
|
|
802
|
+
...(headless ? [] : ['--start-maximized']),
|
|
803
|
+
],
|
|
804
|
+
defaultViewport: headless ? { width: 1920, height: 1080 } : null,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Save Chrome PID to file for recovery
|
|
808
|
+
const browserProcess = browser.process();
|
|
809
|
+
const chromePid = browserProcess ? browserProcess.pid : null;
|
|
810
|
+
|
|
811
|
+
if (chromePid) {
|
|
812
|
+
fs.writeFileSync(PID_FILE, String(chromePid));
|
|
813
|
+
}
|
|
814
|
+
const nativeLines = [
|
|
815
|
+
`${C.green}Connected to Chrome${C.reset}`,
|
|
816
|
+
` Browser profile: ${USER_DATA_DIR}`,
|
|
817
|
+
chromePid ? ` PID: ${chromePid} │ PID file: ${PID_FILE}` : '',
|
|
818
|
+
chromePid ? `${C.dim}If stuck: kill -9 $(cat ${PID_FILE})${C.reset}` : '',
|
|
819
|
+
].filter(Boolean).join('\n');
|
|
820
|
+
const nativeTable = createTable({ colWidths: [95], tableOpts: { wordWrap: true } });
|
|
821
|
+
nativeTable.push([nativeLines]);
|
|
822
|
+
printTable(nativeTable);
|
|
823
|
+
currentProfilePath = USER_DATA_DIR;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Use one tab for our URL: open it first, then close any others (avoids closing the only window)
|
|
827
|
+
const initialPages = await browser.pages();
|
|
828
|
+
page = initialPages.find((p) => p.url() === 'about:blank') || initialPages[0] || await browser.newPage();
|
|
829
|
+
monitoredPages = [page];
|
|
830
|
+
|
|
831
|
+
// ===== SET TIMEOUTS =====
|
|
832
|
+
page.setDefaultTimeout(defaultTimeout);
|
|
833
|
+
page.setDefaultNavigationTimeout(navigationTimeout);
|
|
834
|
+
|
|
835
|
+
// ===== SETUP PAGE MONITORING (console, network events) =====
|
|
836
|
+
setupPageMonitoring(page);
|
|
837
|
+
|
|
838
|
+
writeStatusLine(`${C.dim}Navigating to ${url}...${C.reset}`);
|
|
839
|
+
if (realtime) {
|
|
840
|
+
logBuffer.logConsole(`[Monitor] Navigating to ${url}...`);
|
|
841
|
+
}
|
|
842
|
+
logBuffer.printNetworkSeparator('NAVIGATION STARTED');
|
|
843
|
+
|
|
844
|
+
await page.goto(url, {
|
|
845
|
+
waitUntil: 'domcontentloaded',
|
|
846
|
+
timeout: navigationTimeout,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
clearStatusLine();
|
|
850
|
+
logBuffer.printConsoleSeparator('PAGE LOADED - Listening for console output');
|
|
851
|
+
logBuffer.printNetworkSeparator('PAGE LOADED - Listening for network requests');
|
|
852
|
+
|
|
853
|
+
// Close other tabs only after our page is open (so we never close the tab we need)
|
|
854
|
+
const allPages = await browser.pages();
|
|
855
|
+
for (const p of allPages) {
|
|
856
|
+
if (p !== page && !p.isClosed()) {
|
|
857
|
+
await p.close().catch(() => {});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (realtime) {
|
|
862
|
+
logBuffer.logConsole(`[Monitor] URL: ${url}`);
|
|
863
|
+
logBuffer.logConsole(`[Monitor] Press Ctrl+C to stop.`);
|
|
864
|
+
logBuffer.logConsole(`[Monitor] Type console.clear() in browser to reset console log.`);
|
|
865
|
+
logBuffer.logConsole('');
|
|
866
|
+
} else {
|
|
867
|
+
// Lazy mode: Ready block from template (same as periodic reminder)
|
|
868
|
+
printReadyHelp(httpPort, KEYS_OPEN);
|
|
869
|
+
setupKeyboardInput();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Keep process running until signal
|
|
873
|
+
await new Promise(() => {});
|
|
874
|
+
} finally {
|
|
875
|
+
// Ensure browser is closed even if something goes wrong
|
|
876
|
+
if (browser && !cleanupDone) {
|
|
877
|
+
try {
|
|
878
|
+
await browser.close();
|
|
879
|
+
log.dim('Browser closed in finally block');
|
|
880
|
+
} catch (e) {
|
|
881
|
+
log.error(`Error closing browser in finally: ${e.message}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
fs.unlinkSync(PID_FILE);
|
|
887
|
+
} catch {}
|
|
888
|
+
}
|
|
889
|
+
}
|