@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +558 -0
  3. package/package.json +53 -0
  4. package/src/agents.llm/browser-monitor-section.md +18 -0
  5. package/src/cli.mjs +202 -0
  6. package/src/http-server.mjs +536 -0
  7. package/src/init.mjs +162 -0
  8. package/src/intro.mjs +36 -0
  9. package/src/logging/LogBuffer.mjs +178 -0
  10. package/src/logging/constants.mjs +19 -0
  11. package/src/logging/dump.mjs +207 -0
  12. package/src/logging/index.mjs +13 -0
  13. package/src/logging/timestamps.mjs +13 -0
  14. package/src/monitor/README.md +10 -0
  15. package/src/monitor/index.mjs +18 -0
  16. package/src/monitor/interactive-mode.mjs +275 -0
  17. package/src/monitor/join-mode.mjs +654 -0
  18. package/src/monitor/open-mode.mjs +889 -0
  19. package/src/monitor/page-monitoring.mjs +199 -0
  20. package/src/monitor/tab-selection.mjs +53 -0
  21. package/src/monitor.mjs +39 -0
  22. package/src/os/README.md +4 -0
  23. package/src/os/wsl/chrome.mjs +503 -0
  24. package/src/os/wsl/detect.mjs +68 -0
  25. package/src/os/wsl/diagnostics.mjs +729 -0
  26. package/src/os/wsl/index.mjs +45 -0
  27. package/src/os/wsl/port-proxy.mjs +190 -0
  28. package/src/settings.mjs +101 -0
  29. package/src/templates/api-help.mjs +212 -0
  30. package/src/templates/cli-commands.mjs +51 -0
  31. package/src/templates/interactive-keys.mjs +33 -0
  32. package/src/templates/ready-help.mjs +33 -0
  33. package/src/templates/section-heading.mjs +141 -0
  34. package/src/templates/table-helper.mjs +73 -0
  35. package/src/templates/wait-for-chrome.mjs +19 -0
  36. package/src/utils/ask.mjs +49 -0
  37. package/src/utils/chrome-profile-path.mjs +37 -0
  38. package/src/utils/colors.mjs +49 -0
  39. package/src/utils/env.mjs +30 -0
  40. package/src/utils/profile-id.mjs +23 -0
  41. package/src/utils/status-line.mjs +47 -0
@@ -0,0 +1,275 @@
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
+ }