@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import boxen from 'boxen';
|
|
7
7
|
import { C } from '../utils/colors.mjs';
|
|
8
|
-
import { createTable, printTable } from './table-helper.mjs';
|
|
8
|
+
import { createTable, formatTable, printTable } from './table-helper.mjs';
|
|
9
9
|
|
|
10
10
|
/** Shared boxen options – single border, dim gray, no padding, left margin for alignment with other blocks. */
|
|
11
11
|
export const BOXEN_OPTS = {
|
|
@@ -73,18 +73,21 @@ function wrapPlainText(text, maxLen) {
|
|
|
73
73
|
*/
|
|
74
74
|
const MAX_LINE_WIDTH = 71; // wrap long lines at this (prefix excluded)
|
|
75
75
|
|
|
76
|
-
export function printBulletBox(lines, indent = ' ') {
|
|
76
|
+
export function printBulletBox(lines, indent = ' ', opts = {}) {
|
|
77
77
|
if (lines.length === 0) return;
|
|
78
|
+
const dim = opts.dim || false;
|
|
78
79
|
const bullet = ' • ';
|
|
80
|
+
const bulletColor = dim ? C.dim : C.cyan;
|
|
79
81
|
const contentWidth = MAX_LINE_WIDTH;
|
|
80
82
|
const contentLines = [];
|
|
81
83
|
for (const line of lines) {
|
|
82
84
|
const plain = stripAnsi(line);
|
|
83
85
|
const wrapped = plain.length <= contentWidth ? [line] : wrapPlainText(plain, contentWidth);
|
|
84
86
|
for (let i = 0; i < wrapped.length; i++) {
|
|
85
|
-
const prefix = i === 0 ? `${
|
|
87
|
+
const prefix = i === 0 ? `${bulletColor}${bullet}${C.reset}` : ' ';
|
|
86
88
|
const chunk = wrapped[i];
|
|
87
|
-
|
|
89
|
+
let display = i === 0 && wrapped.length === 1 ? line : chunk;
|
|
90
|
+
if (dim) display = `${C.dim}${stripAnsi(display)}${C.reset}`;
|
|
88
91
|
contentLines.push(`${prefix}${display}`);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -94,7 +97,9 @@ export function printBulletBox(lines, indent = ' ') {
|
|
|
94
97
|
tableOpts: { wordWrap: true, maxWidth: 80 },
|
|
95
98
|
});
|
|
96
99
|
table.push([content]);
|
|
97
|
-
|
|
100
|
+
let output = formatTable(table, indent);
|
|
101
|
+
if (dim) output = `${C.dim}${output}${C.reset}`;
|
|
102
|
+
console.log(output);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
@@ -110,17 +115,6 @@ export function printModeHeading(mode, indent = ' ') {
|
|
|
110
115
|
console.log(renderBox(content, indent));
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
/**
|
|
114
|
-
* Print Interactive menu block – title with blue background filling full width.
|
|
115
|
-
* Options are printed outside the box by the caller.
|
|
116
|
-
* @param {string} titleLine - e.g. " Interactive Chrome not started – choose action"
|
|
117
|
-
* @param {string} [indent=' ']
|
|
118
|
-
*/
|
|
119
|
-
export function printInteractiveMenuBlock(titleLine, indent = ' ') {
|
|
120
|
-
const content = `${C.bgCyan}${C.bold}${C.white}${titleLine}${C.reset}`;
|
|
121
|
-
console.log(renderBox(content, indent));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
118
|
/**
|
|
125
119
|
* Print Join connected block – WSL title (blue bg) + monitored URL. Shown when join succeeds.
|
|
126
120
|
* Single-cell table.
|
package/src/utils/ask.mjs
CHANGED
|
@@ -1,49 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI prompt helpers
|
|
2
|
+
* CLI prompt helpers.
|
|
3
|
+
* All prompts use stdin.once('data') to keep stdin open for keypress listeners.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import readline from 'readline';
|
|
6
7
|
import { C } from './colors.mjs';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @returns {Promise<boolean>} true for y/yes, false otherwise
|
|
10
|
+
* Read one line from stdin without closing it.
|
|
11
|
+
* Handles raw mode save/restore for interactive mode compatibility.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
function readLine(prompt) {
|
|
14
14
|
return new Promise((resolve) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
16
|
+
if (wasRaw) process.stdin.setRawMode(false);
|
|
17
|
+
|
|
18
|
+
process.stdout.write(prompt);
|
|
19
|
+
process.stdin.resume();
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.once('data', (chunk) => {
|
|
22
|
+
process.stdin.pause();
|
|
23
|
+
if (wasRaw && process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
24
|
+
resolve(chunk.toString().trim().split('\n')[0].trim());
|
|
20
25
|
});
|
|
21
26
|
});
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Ask user for default URL.
|
|
31
|
+
* @param {string} [defaultValue='https://localhost:4000/']
|
|
32
|
+
* @returns {Promise<string>}
|
|
33
|
+
*/
|
|
34
|
+
async function askDefaultUrl(defaultValue = 'https://localhost:4000/') {
|
|
35
|
+
const answer = await readLine(` ${C.cyan}Default URL${C.reset} [${C.dim}${defaultValue}${C.reset}]: `);
|
|
36
|
+
return answer || defaultValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return currentValue if set, otherwise ask the user for default URL.
|
|
41
|
+
* @param {string|null|undefined} currentValue - From CLI args or config
|
|
42
|
+
* @param {string} [defaultUrl='about:blank']
|
|
43
|
+
* @returns {Promise<string>}
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveDefaultUrl(currentValue, defaultUrl = 'about:blank') {
|
|
46
|
+
if (currentValue != null) return currentValue;
|
|
47
|
+
return askDefaultUrl(defaultUrl);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ask user for HTTP API port.
|
|
52
|
+
* @param {number} [defaultPort=60001]
|
|
53
|
+
* @returns {Promise<number>}
|
|
54
|
+
*/
|
|
55
|
+
async function askHttpPort(defaultPort = 60001) {
|
|
56
|
+
const answer = await readLine(` ${C.cyan}HTTP API port${C.reset} [${C.dim}${defaultPort}${C.reset}]: `);
|
|
57
|
+
if (!answer) return defaultPort;
|
|
58
|
+
const num = parseInt(answer, 10);
|
|
59
|
+
return !Number.isNaN(num) && num >= 1 && num <= 65535 ? num : defaultPort;
|
|
60
|
+
}
|
|
25
61
|
|
|
26
62
|
/**
|
|
27
|
-
*
|
|
63
|
+
* Return currentValue if set, otherwise ask the user for HTTP port.
|
|
64
|
+
* @param {number|null|undefined} currentValue - From CLI args or config
|
|
28
65
|
* @param {number} [defaultPort=60001]
|
|
29
|
-
* @returns {Promise<number>}
|
|
66
|
+
* @returns {Promise<number>}
|
|
30
67
|
*/
|
|
31
|
-
export function
|
|
68
|
+
export async function resolveHttpPort(currentValue, defaultPort = 60001) {
|
|
69
|
+
if (currentValue != null) return currentValue;
|
|
70
|
+
return askHttpPort(defaultPort);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ask user a yes/no question.
|
|
75
|
+
* @param {string} prompt
|
|
76
|
+
* @returns {Promise<boolean>}
|
|
77
|
+
*/
|
|
78
|
+
export async function askYesNo(prompt) {
|
|
79
|
+
const answer = await readLine(`${prompt} [y/N]: `);
|
|
80
|
+
const normalized = answer.toLowerCase();
|
|
81
|
+
return normalized === 'y' || normalized === 'yes';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ask user to choose action: open, join, or quit.
|
|
86
|
+
* Single keypress – no Enter needed.
|
|
87
|
+
* @returns {Promise<'o'|'j'|'q'>}
|
|
88
|
+
*/
|
|
89
|
+
export function askMode() {
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(` ${C.green}o${C.reset} = open Chrome`);
|
|
92
|
+
console.log(` ${C.green}j${C.reset} = join running Chrome`);
|
|
93
|
+
console.log(` ${C.green}q${C.reset} = quit`);
|
|
94
|
+
console.log('');
|
|
95
|
+
process.stdout.write(` ${C.cyan}Action${C.reset} [o/j/q]: `);
|
|
96
|
+
|
|
97
|
+
readline.emitKeypressEvents(process.stdin);
|
|
98
|
+
if (process.stdin.isTTY && !process.stdin.isRaw) {
|
|
99
|
+
process.stdin.setRawMode(true);
|
|
100
|
+
}
|
|
101
|
+
process.stdin.resume();
|
|
102
|
+
|
|
32
103
|
return new Promise((resolve) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
const num = parseInt(trimmed, 10);
|
|
42
|
-
if (Number.isNaN(num) || num < 1 || num > 65535) {
|
|
43
|
-
resolve(defaultPort);
|
|
44
|
-
return;
|
|
104
|
+
const handleKey = (str, key) => {
|
|
105
|
+
if (!key) return;
|
|
106
|
+
const char = (key.name || str || '').toLowerCase();
|
|
107
|
+
if (char === 'o' || char === 'j' || char === 'q') {
|
|
108
|
+
process.stdin.removeListener('keypress', handleKey);
|
|
109
|
+
process.stdout.write(char + '\n');
|
|
110
|
+
resolve(char);
|
|
45
111
|
}
|
|
46
|
-
|
|
47
|
-
|
|
112
|
+
};
|
|
113
|
+
process.stdin.on('keypress', handleKey);
|
|
48
114
|
});
|
|
49
115
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome instance discovery and selection utilities.
|
|
3
|
+
* Used by join mode and interactive mode to find running Chrome instances.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { C, log } from './colors.mjs';
|
|
7
|
+
import { writeStatusLine, clearStatusLine } from './status-line.mjs';
|
|
8
|
+
import { isWsl, scanChromeInstances } from '../os/wsl/index.mjs';
|
|
9
|
+
import { ensureKeypressEvents } from '../monitor/tab-selection.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Collect Chrome instances with remote debugging.
|
|
13
|
+
* @returns {Promise<Array<{ port: number, label: string }>>}
|
|
14
|
+
*/
|
|
15
|
+
export async function getChromeInstances() {
|
|
16
|
+
writeStatusLine(`${C.dim}Scanning for Chrome...${C.reset}`);
|
|
17
|
+
try {
|
|
18
|
+
if (isWsl()) {
|
|
19
|
+
const { instances } = scanChromeInstances();
|
|
20
|
+
return instances.map((i) => ({ port: i.port, label: `${i.port} – ${i.profile}` }));
|
|
21
|
+
}
|
|
22
|
+
const list = [];
|
|
23
|
+
const host = '127.0.0.1';
|
|
24
|
+
for (let port = 9222; port <= 9229; port++) {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`http://${host}:${port}/json/version`, { signal: AbortSignal.timeout(800) });
|
|
27
|
+
if (res.ok) {
|
|
28
|
+
const info = await res.json();
|
|
29
|
+
const label = info.Browser ? `${port} – ${info.Browser}` : String(port);
|
|
30
|
+
list.push({ port, label });
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Port not reachable
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return list;
|
|
37
|
+
} finally {
|
|
38
|
+
clearStatusLine();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Let user pick one Chrome instance from list.
|
|
44
|
+
* @param {Array<{ port: number, label: string }>} items
|
|
45
|
+
* @returns {Promise<number|null>}
|
|
46
|
+
*/
|
|
47
|
+
export function askUserToSelectChromeInstance(items) {
|
|
48
|
+
if (items.length === 0) return Promise.resolve(null);
|
|
49
|
+
if (items.length === 1) {
|
|
50
|
+
return Promise.resolve(items[0].port);
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
items.forEach((item, index) => {
|
|
54
|
+
console.log(` ${C.brightGreen}${index + 1}${C.reset}. ${C.cyan}${item.label}${C.reset}`);
|
|
55
|
+
});
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(` ${C.red}q${C.reset}. Cancel`);
|
|
58
|
+
console.log('');
|
|
59
|
+
ensureKeypressEvents();
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
process.stdout.write(`${C.cyan}Select Chrome instance${C.reset} (${C.green}1-${items.length}${C.reset}, ${C.red}q${C.reset}=cancel): `);
|
|
62
|
+
const handleKey = (str, key) => {
|
|
63
|
+
if (!key) return;
|
|
64
|
+
process.stdin.removeListener('keypress', handleKey);
|
|
65
|
+
const char = (key.name || str).toLowerCase();
|
|
66
|
+
process.stdout.write(char + '\n');
|
|
67
|
+
if (char === 'q') {
|
|
68
|
+
resolve(null);
|
|
69
|
+
} else {
|
|
70
|
+
const num = parseInt(char, 10);
|
|
71
|
+
if (num >= 1 && num <= items.length) {
|
|
72
|
+
resolve(items[num - 1].port);
|
|
73
|
+
} else {
|
|
74
|
+
log.warn('Invalid selection');
|
|
75
|
+
resolve(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
process.stdin.once('keypress', handleKey);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where the Chrome profile is stored
|
|
3
|
-
* Depends on platform:
|
|
2
|
+
* Where the Chrome profile is stored.
|
|
3
|
+
* Depends on platform:
|
|
4
|
+
* - native Linux/Mac = .browsermonitor/.chrome-profile (matches getPaths().chromeProfileDir)
|
|
5
|
+
* - WSL /mnt/ = same dir converted to Windows drive letter
|
|
6
|
+
* - WSL (project on WSL fs) = Windows LOCALAPPDATA\browsermonitor\{profileId}
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import path from 'path';
|
|
7
10
|
import { isWsl } from './env.mjs';
|
|
8
11
|
import { getWindowsProfilePath } from '../os/wsl/chrome.mjs';
|
|
12
|
+
import { BROWSERMONITOR_DIR, CHROME_PROFILE_DIR } from '../settings.mjs';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Get Chrome profile path and a short description for the current platform.
|
|
16
|
+
* Non-WSL path matches getPaths().chromeProfileDir from settings.mjs.
|
|
12
17
|
* @param {string} projectDir - Project root (outputDir)
|
|
13
18
|
* @returns {{ path: string, where: string }}
|
|
14
19
|
*/
|
|
15
20
|
export function getChromeProfileLocation(projectDir) {
|
|
16
21
|
if (!isWsl()) {
|
|
17
22
|
return {
|
|
18
|
-
path: path.join(projectDir,
|
|
23
|
+
path: path.join(projectDir, BROWSERMONITOR_DIR, CHROME_PROFILE_DIR),
|
|
19
24
|
where: 'Project directory',
|
|
20
25
|
};
|
|
21
26
|
}
|
|
@@ -25,7 +30,7 @@ export function getChromeProfileLocation(projectDir) {
|
|
|
25
30
|
.replace(/^\/mnt\/([a-z])\//, (_, d) => `${d.toUpperCase()}:\\`)
|
|
26
31
|
.replace(/\//g, '\\');
|
|
27
32
|
return {
|
|
28
|
-
path: `${winPath}
|
|
33
|
+
path: `${winPath}\\${BROWSERMONITOR_DIR}\\${CHROME_PROFILE_DIR}`,
|
|
29
34
|
where: 'Windows (same drive as project)',
|
|
30
35
|
};
|
|
31
36
|
}
|
|
@@ -37,11 +37,3 @@ export function clearStatusLine(ensureNewline = false) {
|
|
|
37
37
|
lastLength = 0;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Finish status line with newline (keeps the message visible).
|
|
42
|
-
*/
|
|
43
|
-
export function finishStatusLine() {
|
|
44
|
-
if (!process.stdout.isTTY || lastLength === 0) return;
|
|
45
|
-
process.stdout.write('\n');
|
|
46
|
-
lastLength = 0;
|
|
47
|
-
}
|
package/src/monitor/index.mjs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Monitor submodule – re-exports and shared pieces.
|
|
3
|
-
* Public API (runJoinMode, runOpenMode, runInteractiveMode) lives in ../monitor.mjs
|
|
4
|
-
* to avoid circular deps. This index documents the monitor/ layout:
|
|
5
|
-
*
|
|
6
|
-
* - page-monitoring.mjs – shared console/network listeners for a page
|
|
7
|
-
* - tab-selection.mjs – askUserToSelectPage, ensureKeypressEvents
|
|
8
|
-
* - interactive-mode.mjs – runInteractiveMode(options, deps), getChromeInstances, askProjectDirForOpen
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export { setupPageMonitoring } from './page-monitoring.mjs';
|
|
12
|
-
export { askUserToSelectPage, ensureKeypressEvents } from './tab-selection.mjs';
|
|
13
|
-
export {
|
|
14
|
-
runInteractiveMode,
|
|
15
|
-
getChromeInstances,
|
|
16
|
-
askProjectDirForOpen,
|
|
17
|
-
askUserToSelectChromeInstance,
|
|
18
|
-
} from './interactive-mode.mjs';
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive mode: no Chrome started; user chooses open (o) or join (j).
|
|
3
|
-
* Exports runInteractiveMode(options, deps) where deps = { runOpenMode, runJoinMode }
|
|
4
|
-
* to avoid circular dependency with monitor.mjs.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import readline from 'readline';
|
|
10
|
-
import { C, log } from '../utils/colors.mjs';
|
|
11
|
-
import { getChromeProfileLocation } from '../utils/chrome-profile-path.mjs';
|
|
12
|
-
import { getLastCmdStderrAndClear, isWsl, scanChromeInstances } from '../os/wsl/index.mjs';
|
|
13
|
-
import { printBulletBox, printInteractiveMenuBlock, printModeHeading } from '../templates/section-heading.mjs';
|
|
14
|
-
import { createTable, printTable } from '../templates/table-helper.mjs';
|
|
15
|
-
import { buildWaitForChromeContent } from '../templates/wait-for-chrome.mjs';
|
|
16
|
-
import { writeStatusLine, clearStatusLine } from '../utils/status-line.mjs';
|
|
17
|
-
import { getPaths, ensureDirectories } from '../settings.mjs';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Collect Chrome instances with remote debugging (for interactive "join").
|
|
21
|
-
* @returns {Promise<Array<{ port: number, label: string }>>}
|
|
22
|
-
*/
|
|
23
|
-
export async function getChromeInstances() {
|
|
24
|
-
writeStatusLine(`${C.dim}Scanning for Chrome...${C.reset}`);
|
|
25
|
-
try {
|
|
26
|
-
if (isWsl()) {
|
|
27
|
-
const { instances } = scanChromeInstances();
|
|
28
|
-
return instances.map((i) => ({ port: i.port, label: `${i.port} – ${i.profile}` }));
|
|
29
|
-
}
|
|
30
|
-
const list = [];
|
|
31
|
-
const host = '127.0.0.1';
|
|
32
|
-
for (let port = 9222; port <= 9229; port++) {
|
|
33
|
-
try {
|
|
34
|
-
const res = await fetch(`http://${host}:${port}/json/version`, { signal: AbortSignal.timeout(800) });
|
|
35
|
-
if (res.ok) {
|
|
36
|
-
const info = await res.json();
|
|
37
|
-
const label = info.Browser ? `${port} – ${info.Browser}` : String(port);
|
|
38
|
-
list.push({ port, label });
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// Port not reachable
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return list;
|
|
45
|
-
} finally {
|
|
46
|
-
clearStatusLine();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Ask user which directory to use as project root when opening Chrome (key 'o').
|
|
52
|
-
* Reads one line via stdin.once('data') so that stdin is NOT closed (readline.close()
|
|
53
|
-
* would destroy stdin and the process would exit before the menu keypress listener runs).
|
|
54
|
-
* @param {string} currentCwd
|
|
55
|
-
* @returns {Promise<string>} Resolved absolute path to use as outputDir
|
|
56
|
-
*/
|
|
57
|
-
export function askProjectDirForOpen(currentCwd) {
|
|
58
|
-
return new Promise((resolve) => {
|
|
59
|
-
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
60
|
-
if (wasRaw) process.stdin.setRawMode(false);
|
|
61
|
-
|
|
62
|
-
console.log('');
|
|
63
|
-
process.stdout.write(` ${C.cyan}Project root${C.reset}: ${C.brightCyan}${currentCwd}${C.reset} (${C.green}Enter${C.reset} = use, or type path): `);
|
|
64
|
-
|
|
65
|
-
const onData = (chunk) => {
|
|
66
|
-
process.stdin.removeListener('data', onData);
|
|
67
|
-
process.stdin.pause();
|
|
68
|
-
if (wasRaw && process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
69
|
-
|
|
70
|
-
const trimmed = (chunk.toString().trim().split('\n')[0] || '').trim();
|
|
71
|
-
if (trimmed === '') {
|
|
72
|
-
resolve(currentCwd);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const resolved = path.resolve(currentCwd, trimmed);
|
|
76
|
-
try {
|
|
77
|
-
const stat = fs.statSync(resolved);
|
|
78
|
-
if (!stat.isDirectory()) {
|
|
79
|
-
log.warn(`Not a directory: ${resolved}, using current dir.`);
|
|
80
|
-
resolve(currentCwd);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
log.warn(`Path not found: ${resolved}, using current dir.`);
|
|
85
|
-
resolve(currentCwd);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
resolve(resolved);
|
|
89
|
-
};
|
|
90
|
-
process.stdin.resume();
|
|
91
|
-
process.stdin.setEncoding('utf8');
|
|
92
|
-
process.stdin.once('data', onData);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Let user pick one Chrome instance from list.
|
|
98
|
-
* @param {Array<{ port: number, label: string }>} items
|
|
99
|
-
* @returns {Promise<number|null>}
|
|
100
|
-
*/
|
|
101
|
-
export function askUserToSelectChromeInstance(items) {
|
|
102
|
-
if (items.length === 0) return Promise.resolve(null);
|
|
103
|
-
if (items.length === 1) {
|
|
104
|
-
return Promise.resolve(items[0].port);
|
|
105
|
-
}
|
|
106
|
-
console.log('');
|
|
107
|
-
items.forEach((item, index) => {
|
|
108
|
-
console.log(` ${C.brightGreen}${index + 1}${C.reset}. ${C.cyan}${item.label}${C.reset}`);
|
|
109
|
-
});
|
|
110
|
-
console.log('');
|
|
111
|
-
console.log(` ${C.red}q${C.reset}. Cancel`);
|
|
112
|
-
console.log('');
|
|
113
|
-
return new Promise((resolve) => {
|
|
114
|
-
process.stdout.write(`${C.cyan}Select Chrome instance${C.reset} (${C.green}1-${items.length}${C.reset}, ${C.red}q${C.reset}=cancel): `);
|
|
115
|
-
const handleKey = (str, key) => {
|
|
116
|
-
if (!key) return;
|
|
117
|
-
process.stdin.removeListener('keypress', handleKey);
|
|
118
|
-
const char = (key.name || str).toLowerCase();
|
|
119
|
-
process.stdout.write(char + '\n');
|
|
120
|
-
if (char === 'q') {
|
|
121
|
-
resolve(null);
|
|
122
|
-
} else {
|
|
123
|
-
const num = parseInt(char, 10);
|
|
124
|
-
if (num >= 1 && num <= items.length) {
|
|
125
|
-
resolve(items[num - 1].port);
|
|
126
|
-
} else {
|
|
127
|
-
log.warn('Invalid selection');
|
|
128
|
-
resolve(null);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
process.stdin.once('keypress', handleKey);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Run in Interactive Mode. Requires runOpenMode and runJoinMode to be passed to avoid circular deps.
|
|
138
|
-
* @param {Object} options
|
|
139
|
-
* @param {{ runOpenMode: Function, runJoinMode: Function }} deps
|
|
140
|
-
*/
|
|
141
|
-
export async function runInteractiveMode(options, deps) {
|
|
142
|
-
const {
|
|
143
|
-
defaultUrl = 'https://localhost:4000/',
|
|
144
|
-
realtime = false,
|
|
145
|
-
headless = false,
|
|
146
|
-
hardTimeout = 0,
|
|
147
|
-
navigationTimeout = 60_000,
|
|
148
|
-
outputDir: optionsOutputDir = process.cwd(),
|
|
149
|
-
paths: optionsPaths = null,
|
|
150
|
-
ignorePatterns = [],
|
|
151
|
-
httpPort = 60001,
|
|
152
|
-
} = options;
|
|
153
|
-
|
|
154
|
-
const { runOpenMode, runJoinMode } = deps;
|
|
155
|
-
|
|
156
|
-
// Ask for project root when interactive; otherwise use option/cwd
|
|
157
|
-
const outputDir = process.stdin.isTTY
|
|
158
|
-
? await askProjectDirForOpen(process.cwd())
|
|
159
|
-
: (optionsOutputDir || process.cwd());
|
|
160
|
-
|
|
161
|
-
// Recompute paths if project root changed from CLI's original
|
|
162
|
-
const paths = (outputDir !== optionsOutputDir && optionsPaths)
|
|
163
|
-
? getPaths(outputDir)
|
|
164
|
-
: (optionsPaths || getPaths(outputDir));
|
|
165
|
-
ensureDirectories(outputDir);
|
|
166
|
-
|
|
167
|
-
const profileLoc = getChromeProfileLocation(outputDir);
|
|
168
|
-
const cmdStderrLines = getLastCmdStderrAndClear();
|
|
169
|
-
// Merge UNC path line with CMD.EXE message (path first in stderr, then "CMD.EXE was started...")
|
|
170
|
-
let mergedStderr = [];
|
|
171
|
-
for (let i = 0; i < cmdStderrLines.length; i++) {
|
|
172
|
-
const curr = cmdStderrLines[i];
|
|
173
|
-
const next = cmdStderrLines[i + 1];
|
|
174
|
-
if (next && /^'\\\\/.test(curr) && /CMD\.EXE was started/i.test(next)) {
|
|
175
|
-
mergedStderr.push(`${next} ${curr}`);
|
|
176
|
-
i++;
|
|
177
|
-
} else {
|
|
178
|
-
mergedStderr.push(curr);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const infoLines = [
|
|
182
|
-
...mergedStderr,
|
|
183
|
-
`${C.cyan}Profile path:${C.reset} ${profileLoc.path} ${C.dim}(${profileLoc.where})${C.reset}`,
|
|
184
|
-
];
|
|
185
|
-
if (infoLines.length > 0) {
|
|
186
|
-
console.log('');
|
|
187
|
-
printBulletBox(infoLines);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
readline.emitKeypressEvents(process.stdin);
|
|
191
|
-
if (process.stdin.isTTY) {
|
|
192
|
-
process.stdin.setRawMode(true);
|
|
193
|
-
}
|
|
194
|
-
process.stdin.resume(); // ensure stdin is readable after askProjectDirForOpen (may have paused it)
|
|
195
|
-
|
|
196
|
-
const MENU_INDENT = ' ';
|
|
197
|
-
const showMenu = () => {
|
|
198
|
-
console.log('');
|
|
199
|
-
printInteractiveMenuBlock(' Interactive Chrome not started – choose action');
|
|
200
|
-
console.log(`${MENU_INDENT}${C.green}o${C.reset} = open Chrome (launch new browser → ${C.cyan}${defaultUrl}${C.reset})`);
|
|
201
|
-
console.log(`${MENU_INDENT}${C.green}j${C.reset} = join running Chrome (pick existing instance/tab)`);
|
|
202
|
-
console.log(`${MENU_INDENT}${C.green}q${C.reset} = quit`);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
showMenu();
|
|
206
|
-
|
|
207
|
-
process.stdin.on('keypress', async (str, key) => {
|
|
208
|
-
if (key?.ctrl && key?.name === 'c') {
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
211
|
-
if (key?.ctrl || key?.shift || key?.meta) return;
|
|
212
|
-
const char = (key?.name || str)?.toLowerCase();
|
|
213
|
-
if (char === 'q') {
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
|
-
if (char === 'o') {
|
|
217
|
-
process.stdin.removeAllListeners('keypress');
|
|
218
|
-
process.stdin.setRawMode?.(false);
|
|
219
|
-
await runOpenMode(defaultUrl, {
|
|
220
|
-
realtime,
|
|
221
|
-
headless,
|
|
222
|
-
outputDir,
|
|
223
|
-
paths,
|
|
224
|
-
ignorePatterns,
|
|
225
|
-
hardTimeout,
|
|
226
|
-
navigationTimeout,
|
|
227
|
-
httpPort,
|
|
228
|
-
sharedHttpState: options.sharedHttpState,
|
|
229
|
-
sharedHttpServer: options.sharedHttpServer,
|
|
230
|
-
skipProfileBlock: true,
|
|
231
|
-
});
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (char === 'j') {
|
|
235
|
-
printModeHeading('Join mode');
|
|
236
|
-
let instances = await getChromeInstances();
|
|
237
|
-
while (instances.length === 0) {
|
|
238
|
-
const hint = isWsl()
|
|
239
|
-
? 'Start Chrome on Windows with: chrome.exe --remote-debugging-port=9222'
|
|
240
|
-
: 'Start Chrome with: google-chrome --remote-debugging-port=9222';
|
|
241
|
-
const titleContent = `${C.yellow}No Chrome with remote debugging found.${C.reset}\n${C.dim}${hint}${C.reset}`;
|
|
242
|
-
const content = buildWaitForChromeContent(titleContent);
|
|
243
|
-
const table = createTable({ colWidths: [72], tableOpts: { wordWrap: true } });
|
|
244
|
-
table.push([content]);
|
|
245
|
-
printTable(table);
|
|
246
|
-
await new Promise((resolve) => {
|
|
247
|
-
process.stdin.setRawMode?.(false);
|
|
248
|
-
process.stdin.resume();
|
|
249
|
-
process.stdin.once('data', () => {
|
|
250
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
251
|
-
resolve();
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
console.log('');
|
|
255
|
-
instances = await getChromeInstances();
|
|
256
|
-
}
|
|
257
|
-
const port = await askUserToSelectChromeInstance(instances);
|
|
258
|
-
if (port == null) return;
|
|
259
|
-
process.stdin.removeAllListeners('keypress');
|
|
260
|
-
process.stdin.setRawMode?.(false);
|
|
261
|
-
await runJoinMode(port, {
|
|
262
|
-
realtime,
|
|
263
|
-
outputDir,
|
|
264
|
-
paths,
|
|
265
|
-
ignorePatterns,
|
|
266
|
-
hardTimeout,
|
|
267
|
-
defaultUrl,
|
|
268
|
-
httpPort,
|
|
269
|
-
sharedHttpState: options.sharedHttpState,
|
|
270
|
-
sharedHttpServer: options.sharedHttpServer,
|
|
271
|
-
skipModeHeading: true,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
package/src/monitor.mjs
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser Monitor - monitors browser console and network on dev server
|
|
3
|
-
*
|
|
4
|
-
* Modes:
|
|
5
|
-
* - Default (lazy): Stores logs in memory, writes to file on demand
|
|
6
|
-
* Press 'd' to dump logs to files (auto-clears buffer after)
|
|
7
|
-
* Press 'c' to clear memory buffer
|
|
8
|
-
* Press 's' to show status
|
|
9
|
-
* Press 'k' to kill Chrome and exit
|
|
10
|
-
* Press 'q' to quit
|
|
11
|
-
* - Realtime (--realtime): Writes every event immediately to files
|
|
12
|
-
*
|
|
13
|
-
* Features:
|
|
14
|
-
* - Console output -> .browsermonitor/.puppeteer/console.log (filtered, with HMR detection)
|
|
15
|
-
* - Network requests -> .browsermonitor/.puppeteer/network-log/ directory
|
|
16
|
-
* - .browsermonitor/.puppeteer/network.log - main log with request IDs
|
|
17
|
-
* - {id}.json - detailed request/response for each request
|
|
18
|
-
* - DOM dump on /dump or key 'd' -> .browsermonitor/.puppeteer/dom.html (current JS-modified HTML structure)
|
|
19
|
-
* - Console clear detection (clears buffer/log file)
|
|
20
|
-
* - GUI browser mode (user can interact)
|
|
21
|
-
* - HTTP API for LLM integration (default port 60001)
|
|
22
|
-
* - GET/POST /dump - Trigger dump of buffered logs
|
|
23
|
-
* - GET /status - Get current buffer status
|
|
24
|
-
* - Proper cleanup on exit (SIGINT, SIGTERM, uncaughtException)
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
// Re-export modes (implementations live in monitor/ to keep this file small)
|
|
28
|
-
export { runJoinMode } from './monitor/join-mode.mjs';
|
|
29
|
-
export { runOpenMode } from './monitor/open-mode.mjs';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Run in Interactive Mode – delegates to monitor/interactive-mode.mjs with runOpenMode/runJoinMode to avoid circular deps.
|
|
33
|
-
*/
|
|
34
|
-
export async function runInteractiveMode(options = {}) {
|
|
35
|
-
const { runInteractiveMode: runInteractive } = await import('./monitor/interactive-mode.mjs');
|
|
36
|
-
const { runOpenMode } = await import('./monitor/open-mode.mjs');
|
|
37
|
-
const { runJoinMode } = await import('./monitor/join-mode.mjs');
|
|
38
|
-
return runInteractive(options, { runOpenMode, runJoinMode });
|
|
39
|
-
}
|