@romanmatena/browsermonitor 2.0.0 → 2.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.
- package/README.md +9 -9
- package/package.json +1 -1
- package/src/agents.llm/browser-monitor-section.md +6 -4
- package/src/cli.mjs +103 -105
- package/src/http-server.mjs +12 -38
- package/src/init.mjs +48 -103
- package/src/intro.mjs +1 -1
- package/src/logging/LogBuffer.mjs +1 -1
- package/src/monitor/README.md +18 -5
- package/src/monitor/join-mode.mjs +127 -335
- package/src/monitor/open-mode.mjs +141 -384
- package/src/monitor/shared/cleanup.mjs +96 -0
- package/src/monitor/shared/help.mjs +64 -0
- package/src/monitor/shared/http-state-setup.mjs +55 -0
- package/src/monitor/shared/index.mjs +11 -0
- package/src/monitor/shared/keyboard-handler.mjs +79 -0
- package/src/monitor/shared/monitoring-wrapper.mjs +39 -0
- package/src/monitor/shared/tab-switching.mjs +61 -0
- package/src/monitor/shared/user-page-filter.mjs +28 -0
- package/src/os/wsl/chrome.mjs +3 -36
- package/src/os/wsl/detect.mjs +0 -12
- package/src/os/wsl/index.mjs +0 -3
- package/src/settings.mjs +12 -10
- package/src/templates/api-help.mjs +9 -11
- package/src/templates/cli-commands.mjs +1 -1
- package/src/templates/section-heading.mjs +10 -16
- package/src/utils/ask.mjs +94 -28
- package/src/utils/chrome-instances.mjs +81 -0
- package/src/utils/chrome-profile-path.mjs +9 -4
- package/src/utils/status-line.mjs +0 -8
- package/src/monitor/index.mjs +0 -18
- package/src/monitor/interactive-mode.mjs +0 -275
- package/src/monitor.mjs +0 -39
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup function and signal handler installation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { log } from '../../utils/colors.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create cleanup function and install signal/error handlers.
|
|
9
|
+
* @param {Object} deps
|
|
10
|
+
* @param {() => import('puppeteer').Browser|null} deps.getBrowser
|
|
11
|
+
* @param {() => import('http').Server|null} deps.getHttpServer
|
|
12
|
+
* @param {(browser: import('puppeteer').Browser, closeBrowser: boolean) => Promise<void>} deps.closeBrowserFn
|
|
13
|
+
* @param {() => void} [deps.afterCleanup] - e.g. PID file removal
|
|
14
|
+
* @param {number} [deps.hardTimeout=0]
|
|
15
|
+
* @returns {{ cleanup: (code?: number, closeBrowser?: boolean) => Promise<void> }}
|
|
16
|
+
*/
|
|
17
|
+
export function createCleanup({ getBrowser, getHttpServer, closeBrowserFn, afterCleanup, hardTimeout = 0 }) {
|
|
18
|
+
let cleanupDone = false;
|
|
19
|
+
|
|
20
|
+
async function cleanup(code = 0, closeBrowser = false) {
|
|
21
|
+
if (cleanupDone) return;
|
|
22
|
+
cleanupDone = true;
|
|
23
|
+
|
|
24
|
+
console.log('');
|
|
25
|
+
log.info(closeBrowser ? 'Cleaning up and closing Chrome...' : 'Cleaning up...');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const httpServer = getHttpServer();
|
|
29
|
+
if (httpServer) {
|
|
30
|
+
await new Promise((resolve) => httpServer.close(resolve));
|
|
31
|
+
log.dim('HTTP server closed');
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
log.error(`Error closing HTTP server: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const browser = getBrowser();
|
|
39
|
+
if (browser) {
|
|
40
|
+
await closeBrowserFn(browser, closeBrowser);
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
log.error(`Error closing browser: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (afterCleanup) {
|
|
47
|
+
try { afterCleanup(); } catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.exit(code);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Signal handlers
|
|
54
|
+
process.on('SIGINT', () => {
|
|
55
|
+
console.log('');
|
|
56
|
+
log.dim('Received SIGINT (Ctrl+C)');
|
|
57
|
+
cleanup(0, false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.on('SIGTERM', () => {
|
|
61
|
+
console.log('');
|
|
62
|
+
log.dim('Received SIGTERM');
|
|
63
|
+
cleanup(0, false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on('uncaughtException', (e) => {
|
|
67
|
+
const msg = (e && e.message) || String(e);
|
|
68
|
+
if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
|
|
69
|
+
log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
log.error(`Uncaught exception: ${e.message}`);
|
|
73
|
+
console.error(e.stack);
|
|
74
|
+
cleanup(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
process.on('unhandledRejection', (e) => {
|
|
78
|
+
const msg = (e && e.message) || String(e);
|
|
79
|
+
if (/Execution context was destroyed|Target closed|Protocol error/.test(msg)) {
|
|
80
|
+
log.dim(`Navigation/context closed: ${msg.slice(0, 60)}… (continuing)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
log.error(`Unhandled rejection: ${e}`);
|
|
84
|
+
cleanup(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Hard timeout
|
|
88
|
+
if (hardTimeout > 0) {
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
log.error(`HARD TIMEOUT (${hardTimeout}ms) - forcing exit`);
|
|
91
|
+
cleanup(1);
|
|
92
|
+
}, hardTimeout);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { cleanup };
|
|
96
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help display factories: periodic help reminder + full in-session help.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { C } from '../../utils/colors.mjs';
|
|
6
|
+
import { printReadyHelp } from '../../templates/ready-help.mjs';
|
|
7
|
+
import { printApiHelpTable } from '../../templates/api-help.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create help display functions bound to session state.
|
|
11
|
+
* @param {Object} deps
|
|
12
|
+
* @param {number} deps.httpPort
|
|
13
|
+
* @param {string} deps.keysVariant - KEYS_OPEN or KEYS_JOIN constant
|
|
14
|
+
* @param {Object} deps.logBuffer
|
|
15
|
+
* @param {string} deps.outputDir
|
|
16
|
+
* @param {string} deps.modeLabel - 'Launch mode' or 'Join mode'
|
|
17
|
+
* @param {() => string} deps.getCurrentUrl
|
|
18
|
+
* @param {() => string|null} deps.getProfilePath
|
|
19
|
+
* @param {() => string|null} deps.getBrowserUrl - join shows browserURL, open null
|
|
20
|
+
* @returns {{ maybeShowHelp: () => void, printHelp: () => void }}
|
|
21
|
+
*/
|
|
22
|
+
export function createHelpHandlers({
|
|
23
|
+
httpPort, keysVariant, logBuffer, outputDir,
|
|
24
|
+
modeLabel, getCurrentUrl, getProfilePath, getBrowserUrl,
|
|
25
|
+
}) {
|
|
26
|
+
let outputCounter = 0;
|
|
27
|
+
const HELP_INTERVAL = 5;
|
|
28
|
+
|
|
29
|
+
function maybeShowHelp() {
|
|
30
|
+
outputCounter++;
|
|
31
|
+
if (outputCounter % HELP_INTERVAL === 0) {
|
|
32
|
+
printReadyHelp(httpPort, keysVariant);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printHelp() {
|
|
37
|
+
const currentUrl = getCurrentUrl();
|
|
38
|
+
const browserUrl = getBrowserUrl();
|
|
39
|
+
const connInfo = browserUrl
|
|
40
|
+
? `Browser: ${C.brightGreen}${browserUrl}${C.reset} │ Dir: ${outputDir}`
|
|
41
|
+
: `URL: ${C.brightGreen}${currentUrl}${C.reset} │ Dir: ${outputDir}`;
|
|
42
|
+
console.log(`${C.cyan}${modeLabel}${C.reset} ${connInfo}`);
|
|
43
|
+
printApiHelpTable({
|
|
44
|
+
port: httpPort,
|
|
45
|
+
showApi: true,
|
|
46
|
+
showInteractive: true,
|
|
47
|
+
showOutputFiles: true,
|
|
48
|
+
context: {
|
|
49
|
+
consoleLog: logBuffer.CONSOLE_LOG,
|
|
50
|
+
networkLog: logBuffer.NETWORK_LOG,
|
|
51
|
+
networkDir: logBuffer.NETWORK_DIR,
|
|
52
|
+
cookiesDir: logBuffer.COOKIES_DIR,
|
|
53
|
+
domHtml: logBuffer.DOM_HTML,
|
|
54
|
+
screenshot: logBuffer.SCREENSHOT,
|
|
55
|
+
},
|
|
56
|
+
sessionContext: {
|
|
57
|
+
currentUrl: currentUrl || undefined,
|
|
58
|
+
profilePath: getProfilePath(),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { maybeShowHelp, printHelp };
|
|
64
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire sharedHttpState callbacks to mode-local state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { filterUserPages } from './user-page-filter.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} sharedHttpState - mutable state object from CLI
|
|
9
|
+
* @param {Object} deps
|
|
10
|
+
* @param {string} deps.mode - 'launch' | 'join'
|
|
11
|
+
* @param {Object} deps.logBuffer
|
|
12
|
+
* @param {() => import('puppeteer').Page[]} deps.getMonitoredPages
|
|
13
|
+
* @param {() => boolean} deps.getCollectingPaused
|
|
14
|
+
* @param {(v: boolean) => void} deps.setCollectingPaused
|
|
15
|
+
* @param {() => import('puppeteer').Browser|null} deps.getBrowser
|
|
16
|
+
* @param {(page: import('puppeteer').Page, label?: string) => void} deps.setupPageMonitoring
|
|
17
|
+
* @param {(page: import('puppeteer').Page) => void} deps.onTabSwitched - mode-specific side effect
|
|
18
|
+
*/
|
|
19
|
+
export function wireHttpState(sharedHttpState, {
|
|
20
|
+
mode, logBuffer, getMonitoredPages, getCollectingPaused, setCollectingPaused,
|
|
21
|
+
getBrowser, setupPageMonitoring, onTabSwitched,
|
|
22
|
+
}) {
|
|
23
|
+
sharedHttpState.mode = mode;
|
|
24
|
+
sharedHttpState.logBuffer = logBuffer;
|
|
25
|
+
sharedHttpState.getPages = getMonitoredPages;
|
|
26
|
+
sharedHttpState.getCollectingPaused = getCollectingPaused;
|
|
27
|
+
sharedHttpState.setCollectingPaused = setCollectingPaused;
|
|
28
|
+
|
|
29
|
+
sharedHttpState.getAllTabs = async () => {
|
|
30
|
+
const browser = getBrowser();
|
|
31
|
+
if (!browser) return [];
|
|
32
|
+
const allPages = await browser.pages();
|
|
33
|
+
return filterUserPages(allPages).map((p, i) => ({ index: i + 1, url: p.url() }));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
sharedHttpState.switchToTab = async (index) => {
|
|
37
|
+
const browser = getBrowser();
|
|
38
|
+
if (!browser) return { success: false, error: 'Browser not connected' };
|
|
39
|
+
try {
|
|
40
|
+
const allPages = await browser.pages();
|
|
41
|
+
const pages = filterUserPages(allPages);
|
|
42
|
+
if (index < 1 || index > pages.length) {
|
|
43
|
+
return { success: false, error: `Invalid index. Use 1-${pages.length}.` };
|
|
44
|
+
}
|
|
45
|
+
const selectedPage = pages[index - 1];
|
|
46
|
+
onTabSwitched(selectedPage);
|
|
47
|
+
setupPageMonitoring(selectedPage);
|
|
48
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
49
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
50
|
+
return { success: true, url: selectedPage.url() };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return { success: false, error: e.message };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for open-mode and join-mode.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { filterUserPages } from './user-page-filter.mjs';
|
|
6
|
+
export { createSetupPageMonitoring } from './monitoring-wrapper.mjs';
|
|
7
|
+
export { createHelpHandlers } from './help.mjs';
|
|
8
|
+
export { wireHttpState } from './http-state-setup.mjs';
|
|
9
|
+
export { createSwitchTabs } from './tab-switching.mjs';
|
|
10
|
+
export { createCleanup } from './cleanup.mjs';
|
|
11
|
+
export { setupKeyboardInput } from './keyboard-handler.mjs';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive keyboard input handler (raw mode stdin).
|
|
3
|
+
* Shared between open-mode and join-mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import { log } from '../../utils/colors.mjs';
|
|
8
|
+
import { printStatusBlock } from '../../templates/ready-help.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Setup keyboard input for lazy (buffered) mode.
|
|
12
|
+
* @param {Object} deps
|
|
13
|
+
* @param {() => import('puppeteer').Page|null} deps.getActivePage
|
|
14
|
+
* @param {Object} deps.logBuffer
|
|
15
|
+
* @param {(code?: number, closeBrowser?: boolean) => Promise<void>} deps.cleanup
|
|
16
|
+
* @param {() => Promise<void>} deps.switchTabs
|
|
17
|
+
* @param {() => void} deps.printHelp
|
|
18
|
+
* @param {() => void} deps.maybeShowHelp
|
|
19
|
+
* @param {() => boolean} deps.isSelectingTab
|
|
20
|
+
* @param {() => boolean} deps.getCollectingPaused
|
|
21
|
+
* @param {(v: boolean) => void} deps.setCollectingPaused
|
|
22
|
+
* @param {() => Promise<{ currentUrl: string, tabCount: number }>} deps.getStatusInfo
|
|
23
|
+
*/
|
|
24
|
+
export function setupKeyboardInput({
|
|
25
|
+
getActivePage, logBuffer, cleanup, switchTabs,
|
|
26
|
+
printHelp, maybeShowHelp, isSelectingTab,
|
|
27
|
+
getCollectingPaused, setCollectingPaused, getStatusInfo,
|
|
28
|
+
}) {
|
|
29
|
+
readline.emitKeypressEvents(process.stdin);
|
|
30
|
+
if (process.stdin.isTTY) {
|
|
31
|
+
process.stdin.setRawMode(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
35
|
+
if (key.ctrl && key.name === 'c') {
|
|
36
|
+
cleanup(0);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ignore keys during tab selection
|
|
41
|
+
if (isSelectingTab()) return;
|
|
42
|
+
|
|
43
|
+
// Shortcuts only without modifiers
|
|
44
|
+
if (key.ctrl || key.shift || key.meta) return;
|
|
45
|
+
|
|
46
|
+
if (key.name === 'd') {
|
|
47
|
+
const page = getActivePage();
|
|
48
|
+
await logBuffer.dumpBuffersToFiles({
|
|
49
|
+
dumpCookies: page ? () => logBuffer.dumpCookiesFromPage(page) : null,
|
|
50
|
+
dumpDom: page ? () => logBuffer.dumpDomFromPage(page) : null,
|
|
51
|
+
dumpScreenshot: page ? () => logBuffer.dumpScreenshotFromPage(page) : null,
|
|
52
|
+
});
|
|
53
|
+
maybeShowHelp();
|
|
54
|
+
} else if (key.name === 'c') {
|
|
55
|
+
logBuffer.clearAllBuffers();
|
|
56
|
+
maybeShowHelp();
|
|
57
|
+
} else if (key.name === 'q') {
|
|
58
|
+
cleanup(0, false);
|
|
59
|
+
} else if (key.name === 'k') {
|
|
60
|
+
log.warn('Closing Chrome and exiting...');
|
|
61
|
+
cleanup(0, true);
|
|
62
|
+
} else if (key.name === 's') {
|
|
63
|
+
const { currentUrl, tabCount } = await getStatusInfo();
|
|
64
|
+
const stats = logBuffer.getStats();
|
|
65
|
+
printStatusBlock(stats, currentUrl, tabCount, getCollectingPaused());
|
|
66
|
+
maybeShowHelp();
|
|
67
|
+
} else if (key.name === 'p') {
|
|
68
|
+
const paused = !getCollectingPaused();
|
|
69
|
+
setCollectingPaused(paused);
|
|
70
|
+
log.info(paused ? 'Collecting stopped (paused). Press p or curl .../start to resume.' : 'Collecting started (resumed).');
|
|
71
|
+
maybeShowHelp();
|
|
72
|
+
} else if (key.name === 't') {
|
|
73
|
+
await switchTabs();
|
|
74
|
+
maybeShowHelp();
|
|
75
|
+
} else if (key.name === 'h') {
|
|
76
|
+
printHelp();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for page monitoring setup, bound to local session state.
|
|
3
|
+
* Manages listener lifecycle: each call to setupPageMonitoring automatically
|
|
4
|
+
* cleans up listeners from the previously monitored page.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { setupPageMonitoring as setupPageMonitoringShared } from '../page-monitoring.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a setupPageMonitoring function bound to the current session's state.
|
|
11
|
+
* @param {Object} deps
|
|
12
|
+
* @param {Object} deps.logBuffer
|
|
13
|
+
* @param {() => boolean} deps.getCollectingPaused
|
|
14
|
+
* @returns {{ setupPageMonitoring: (page: import('puppeteer').Page, pageLabel?: string) => void, cleanupActivePageListeners: () => void }}
|
|
15
|
+
*/
|
|
16
|
+
export function createSetupPageMonitoring({ logBuffer, getCollectingPaused }) {
|
|
17
|
+
let activePageCleanup = null;
|
|
18
|
+
|
|
19
|
+
function cleanupActivePageListeners() {
|
|
20
|
+
if (activePageCleanup) {
|
|
21
|
+
activePageCleanup();
|
|
22
|
+
activePageCleanup = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setupPageMonitoring(targetPage, pageLabel = '') {
|
|
27
|
+
// Remove listeners from previous page before attaching to new one
|
|
28
|
+
cleanupActivePageListeners();
|
|
29
|
+
|
|
30
|
+
setupPageMonitoringShared(targetPage, {
|
|
31
|
+
logBuffer,
|
|
32
|
+
getCollectingPaused,
|
|
33
|
+
setActivePageCleanup: (fn) => { activePageCleanup = fn; },
|
|
34
|
+
pageLabel,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { setupPageMonitoring, cleanupActivePageListeners };
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive tab switching (t key).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { C, log } from '../../utils/colors.mjs';
|
|
6
|
+
import { askUserToSelectPage } from '../tab-selection.mjs';
|
|
7
|
+
import { filterUserPages } from './user-page-filter.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create interactive tab-switch function.
|
|
11
|
+
* @param {Object} deps
|
|
12
|
+
* @param {() => import('puppeteer').Browser|null} deps.getBrowser
|
|
13
|
+
* @param {(page: import('puppeteer').Page) => void} deps.onTabSwitched
|
|
14
|
+
* @param {(page: import('puppeteer').Page, label?: string) => void} deps.setupPageMonitoring
|
|
15
|
+
* @param {Object} deps.logBuffer
|
|
16
|
+
* @param {(v: boolean) => void} deps.setSelectingTab
|
|
17
|
+
* @returns {() => Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
export function createSwitchTabs({ getBrowser, onTabSwitched, setupPageMonitoring, logBuffer, setSelectingTab }) {
|
|
20
|
+
return async () => {
|
|
21
|
+
const browser = getBrowser();
|
|
22
|
+
if (!browser) {
|
|
23
|
+
log.error('Browser not connected');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const allPages = await browser.pages();
|
|
29
|
+
const pages = filterUserPages(allPages);
|
|
30
|
+
|
|
31
|
+
if (pages.length === 0) {
|
|
32
|
+
log.warn('No user tabs found');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (pages.length === 1) {
|
|
37
|
+
log.info('Only one user tab available');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setSelectingTab(true);
|
|
42
|
+
const selectedPage = await askUserToSelectPage(pages);
|
|
43
|
+
setSelectingTab(false);
|
|
44
|
+
|
|
45
|
+
if (selectedPage === null) {
|
|
46
|
+
log.dim('Tab switch cancelled');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onTabSwitched(selectedPage);
|
|
51
|
+
setupPageMonitoring(selectedPage);
|
|
52
|
+
log.success(`Now monitoring: ${C.brightCyan}${selectedPage.url()}${C.reset}`);
|
|
53
|
+
|
|
54
|
+
logBuffer.printConsoleSeparator('TAB SWITCHED');
|
|
55
|
+
logBuffer.printNetworkSeparator('TAB SWITCHED');
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log.error(`Error switching tabs: ${e.message}`);
|
|
58
|
+
setSelectingTab(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter browser internal / devtools pages from a page list.
|
|
3
|
+
* Single source of truth for "is this a user page?" across all modes.
|
|
4
|
+
*
|
|
5
|
+
* Strips: chrome://, devtools://, chrome-extension://, moz-extension://,
|
|
6
|
+
* extension://, React/Redux devtools background pages.
|
|
7
|
+
* Also strips about:blank unless it's the only remaining page.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('puppeteer').Page[]} allPages
|
|
10
|
+
* @returns {import('puppeteer').Page[]}
|
|
11
|
+
*/
|
|
12
|
+
export function filterUserPages(allPages) {
|
|
13
|
+
const isUserPage = (p) => {
|
|
14
|
+
const u = p.url();
|
|
15
|
+
if (u.startsWith('chrome://')) return false;
|
|
16
|
+
if (u.startsWith('devtools://')) return false;
|
|
17
|
+
if (u.startsWith('chrome-extension://')) return false;
|
|
18
|
+
if (u.startsWith('moz-extension://')) return false;
|
|
19
|
+
if (u.startsWith('extension://')) return false;
|
|
20
|
+
if (u.includes('react-devtools') || u.includes('redux-devtools')) return false;
|
|
21
|
+
if (u.includes('__react_devtools__')) return false;
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
let pages = allPages.filter(isUserPage);
|
|
25
|
+
const nonBlank = pages.filter(p => p.url() !== 'about:blank');
|
|
26
|
+
if (nonBlank.length > 0) pages = nonBlank;
|
|
27
|
+
return pages;
|
|
28
|
+
}
|
package/src/os/wsl/chrome.mjs
CHANGED
|
@@ -41,7 +41,6 @@ export function getLastCmdStderrAndClear() {
|
|
|
41
41
|
_lastCmdStderrLines = [];
|
|
42
42
|
return lines;
|
|
43
43
|
}
|
|
44
|
-
import { getWslDistroName } from './detect.mjs';
|
|
45
44
|
|
|
46
45
|
// Cache for LOCALAPPDATA path
|
|
47
46
|
let _cachedLocalAppData = null;
|
|
@@ -329,12 +328,9 @@ export function startChromeOnWindows(chromePath, port, profileDir) {
|
|
|
329
328
|
);
|
|
330
329
|
} catch { /* ignore */ }
|
|
331
330
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
].join("','");
|
|
336
|
-
|
|
337
|
-
const psCommand = `Start-Process -FilePath '${chromePath}' -ArgumentList '${args}'`;
|
|
331
|
+
// Escape single quotes for PowerShell literal strings (double them)
|
|
332
|
+
const psEsc = (s) => s.replace(/'/g, "''");
|
|
333
|
+
const psCommand = `Start-Process -FilePath '${psEsc(chromePath)}' -ArgumentList @('--remote-debugging-port=${port}','--user-data-dir=${psEsc(profileDir)}')`;
|
|
338
334
|
execSync(`powershell.exe -NoProfile -Command "${psCommand}"`, { encoding: 'utf8', timeout: 10000 });
|
|
339
335
|
|
|
340
336
|
log.dim('Waiting for Chrome to start...');
|
|
@@ -472,32 +468,3 @@ export function killPuppeteerMonitorChromes(usePowerShell = false) {
|
|
|
472
468
|
}
|
|
473
469
|
}
|
|
474
470
|
|
|
475
|
-
/**
|
|
476
|
-
* Legacy wrapper for backward compatibility.
|
|
477
|
-
*/
|
|
478
|
-
export function checkChromeRunning() {
|
|
479
|
-
const { instances, chromeRunning } = scanChromeInstances();
|
|
480
|
-
if (!chromeRunning) {
|
|
481
|
-
return { running: false, withDebugPort: false, debugPort: null };
|
|
482
|
-
}
|
|
483
|
-
if (instances.length === 0) {
|
|
484
|
-
return { running: true, withDebugPort: false, debugPort: null };
|
|
485
|
-
}
|
|
486
|
-
return {
|
|
487
|
-
running: true,
|
|
488
|
-
withDebugPort: true,
|
|
489
|
-
debugPort: instances[0].port,
|
|
490
|
-
instances,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Legacy wrapper for backward compatibility.
|
|
496
|
-
*/
|
|
497
|
-
export function launchChromeFromWSL(chromePath, port) {
|
|
498
|
-
const distroName = getWslDistroName();
|
|
499
|
-
const wslPath = process.cwd();
|
|
500
|
-
const winPath = wslPath.replace(/\//g, '\\');
|
|
501
|
-
const profileDir = `\\\\wsl$\\${distroName}${winPath}\\.browsermonitor-profile`;
|
|
502
|
-
return startChromeOnWindows(chromePath, port, profileDir);
|
|
503
|
-
}
|
package/src/os/wsl/detect.mjs
CHANGED
|
@@ -54,15 +54,3 @@ export function getWindowsHostForWSL(opts = {}) {
|
|
|
54
54
|
return 'localhost';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
* Convert WSL path to Windows UNC path.
|
|
59
|
-
* Note: UNC paths don't work well with Chrome profiles due to singleton detection.
|
|
60
|
-
*
|
|
61
|
-
* @param {string} wslPath - WSL path (e.g., /srv/project)
|
|
62
|
-
* @returns {string} Windows UNC path (e.g., \\wsl$\Ubuntu\srv\project)
|
|
63
|
-
*/
|
|
64
|
-
export function wslToWindowsPath(wslPath) {
|
|
65
|
-
const distroName = getWslDistroName();
|
|
66
|
-
const winPath = wslPath.replace(/\//g, '\\');
|
|
67
|
-
return `\\\\wsl$\\${distroName}${winPath}`;
|
|
68
|
-
}
|
package/src/os/wsl/index.mjs
CHANGED
|
@@ -10,7 +10,6 @@ export {
|
|
|
10
10
|
isWsl,
|
|
11
11
|
getWslDistroName,
|
|
12
12
|
getWindowsHostForWSL,
|
|
13
|
-
wslToWindowsPath,
|
|
14
13
|
} from './detect.mjs';
|
|
15
14
|
|
|
16
15
|
// Chrome detection and launch
|
|
@@ -26,8 +25,6 @@ export {
|
|
|
26
25
|
findFreeDebugPort,
|
|
27
26
|
startChromeOnWindows,
|
|
28
27
|
killPuppeteerMonitorChromes,
|
|
29
|
-
checkChromeRunning,
|
|
30
|
-
launchChromeFromWSL,
|
|
31
28
|
} from './chrome.mjs';
|
|
32
29
|
|
|
33
30
|
// Port proxy management
|
package/src/settings.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export const PID_FILE = 'browsermonitor.pid';
|
|
|
16
16
|
|
|
17
17
|
/** Default settings for new projects */
|
|
18
18
|
export const DEFAULT_SETTINGS = {
|
|
19
|
-
defaultUrl: '
|
|
19
|
+
defaultUrl: 'about:blank',
|
|
20
20
|
headless: false,
|
|
21
21
|
navigationTimeout: 60000,
|
|
22
22
|
ignorePatterns: [],
|
|
@@ -30,6 +30,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
30
30
|
* @returns {Object} All paths used by browsermonitor
|
|
31
31
|
*/
|
|
32
32
|
export function getPaths(projectRoot) {
|
|
33
|
+
projectRoot = path.resolve(projectRoot);
|
|
33
34
|
const bmDir = path.join(projectRoot, BROWSERMONITOR_DIR);
|
|
34
35
|
const puppeteerDir = path.join(bmDir, PUPPETEER_DIR);
|
|
35
36
|
return {
|
|
@@ -49,32 +50,33 @@ export function getPaths(projectRoot) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* Check if .
|
|
53
|
+
* Check if settings.json exists (first-run detection).
|
|
53
54
|
* @param {string} projectRoot
|
|
54
55
|
* @returns {boolean}
|
|
55
56
|
*/
|
|
56
57
|
export function isInitialized(projectRoot) {
|
|
57
|
-
const {
|
|
58
|
-
return fs.existsSync(
|
|
58
|
+
const { settingsFile } = getPaths(projectRoot);
|
|
59
|
+
return fs.existsSync(settingsFile);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
* Load settings from .browsermonitor/settings.json
|
|
63
|
+
* Load settings from .browsermonitor/settings.json.
|
|
64
|
+
* Returns only what is actually saved — no default merging.
|
|
65
|
+
* Missing keys mean "not configured yet" so the caller can prompt the user.
|
|
63
66
|
* @param {string} projectRoot
|
|
64
|
-
* @returns {Object}
|
|
67
|
+
* @returns {Object} Saved settings (may be empty)
|
|
65
68
|
*/
|
|
66
69
|
export function loadSettings(projectRoot) {
|
|
67
70
|
const { settingsFile } = getPaths(projectRoot);
|
|
68
71
|
try {
|
|
69
72
|
if (fs.existsSync(settingsFile)) {
|
|
70
73
|
const raw = fs.readFileSync(settingsFile, 'utf8');
|
|
71
|
-
|
|
72
|
-
return { ...DEFAULT_SETTINGS, ...saved };
|
|
74
|
+
return JSON.parse(raw);
|
|
73
75
|
}
|
|
74
76
|
} catch {
|
|
75
|
-
// Ignore parse errors, fall through to
|
|
77
|
+
// Ignore parse errors, fall through to empty
|
|
76
78
|
}
|
|
77
|
-
return {
|
|
79
|
+
return {};
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -43,6 +43,7 @@ const API_USAGE =
|
|
|
43
43
|
* @param {Object} options
|
|
44
44
|
* @param {number} [options.port=60001]
|
|
45
45
|
* @param {string} [options.host='127.0.0.1']
|
|
46
|
+
* @param {string} [options.url] - Browser URL being monitored (shown in header)
|
|
46
47
|
* @param {boolean} [options.showApi=true]
|
|
47
48
|
* @param {boolean} [options.showInteractive=false] - Interactive help (keyboard shortcuts, human interaction)
|
|
48
49
|
* @param {boolean} [options.showOutputFiles=true]
|
|
@@ -54,6 +55,7 @@ export function printApiHelpTable(options = {}) {
|
|
|
54
55
|
const {
|
|
55
56
|
port = 60001,
|
|
56
57
|
host = '127.0.0.1',
|
|
58
|
+
url = null,
|
|
57
59
|
showApi = true,
|
|
58
60
|
showInteractive = false,
|
|
59
61
|
showOutputFiles = true,
|
|
@@ -66,7 +68,8 @@ export function printApiHelpTable(options = {}) {
|
|
|
66
68
|
|
|
67
69
|
if (showApi) {
|
|
68
70
|
if (!noLeadingNewline) console.log('');
|
|
69
|
-
|
|
71
|
+
const heading = url ? `HTTP API ${C.dim}→${C.reset} ${C.brightCyan}${url}${C.reset}` : 'HTTP API';
|
|
72
|
+
printSectionHeading(heading, INDENT);
|
|
70
73
|
|
|
71
74
|
const hasSession = sessionContext && (sessionContext.currentUrl || sessionContext.profilePath);
|
|
72
75
|
const apiTable = createTable({
|
|
@@ -133,7 +136,7 @@ export function printApiHelpTable(options = {}) {
|
|
|
133
136
|
|
|
134
137
|
// Same info without tables, structured for LLM (no box-drawing / table chars; no keyboard shortcuts)
|
|
135
138
|
if (showApi || showOutputFiles) {
|
|
136
|
-
printApiHelpForLlm({ port, host, showApi, showOutputFiles, context, sessionContext });
|
|
139
|
+
printApiHelpForLlm({ port, host, url, showApi, showOutputFiles, context, sessionContext });
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
console.log('');
|
|
@@ -147,6 +150,7 @@ function printApiHelpForLlm(options = {}) {
|
|
|
147
150
|
const {
|
|
148
151
|
port = 60001,
|
|
149
152
|
host = '127.0.0.1',
|
|
153
|
+
url = null,
|
|
150
154
|
showApi = true,
|
|
151
155
|
showOutputFiles = true,
|
|
152
156
|
context = null,
|
|
@@ -168,6 +172,9 @@ function printApiHelpForLlm(options = {}) {
|
|
|
168
172
|
console.log('');
|
|
169
173
|
console.log('--- LLM reference (plain text, no tables) ---');
|
|
170
174
|
console.log('');
|
|
175
|
+
if (url) {
|
|
176
|
+
console.log('Browser URL: ' + url);
|
|
177
|
+
}
|
|
171
178
|
console.log('Base URL: ' + baseUrl);
|
|
172
179
|
if (sessionContext?.currentUrl) {
|
|
173
180
|
console.log('Monitored URL: ' + sessionContext.currentUrl);
|
|
@@ -201,12 +208,3 @@ function printApiHelpForLlm(options = {}) {
|
|
|
201
208
|
console.log('');
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
/**
|
|
205
|
-
* One-line API hint (for Ready bar and intro).
|
|
206
|
-
* @param {number} port
|
|
207
|
-
* @param {string} [host='127.0.0.1']
|
|
208
|
-
*/
|
|
209
|
-
export function apiHintOneLine(port, host = '127.0.0.1') {
|
|
210
|
-
const base = `http://${host}:${port}`;
|
|
211
|
-
return `${C.dim}curl ${base}/dump ${base}/status ${base}/stop ${base}/start${C.reset}`;
|
|
212
|
-
}
|
|
@@ -18,7 +18,7 @@ export const CLI_EXAMPLES = [
|
|
|
18
18
|
{ command: 'browsermonitor --open', description: 'Launch Chrome and monitor (URL from settings or first arg)' },
|
|
19
19
|
{ command: 'browsermonitor --open https://localhost:5173/', description: 'Open with URL' },
|
|
20
20
|
{ command: 'browsermonitor --join=9222', description: 'Attach to Chrome with remote debugging on port 9222' },
|
|
21
|
-
{ command: 'browsermonitor init', description: '
|
|
21
|
+
{ command: 'browsermonitor init', description: 'Create .browsermonitor/, settings.json with defaults, update agent files' },
|
|
22
22
|
{ command: 'browsermonitor --help', description: 'Show full help (options, API table, examples)' },
|
|
23
23
|
];
|
|
24
24
|
|