@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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared section heading – box-style title (HTTP API, CLI, Interactive).
|
|
3
|
+
* All boxes use boxen for correct border alignment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import { C } from '../utils/colors.mjs';
|
|
8
|
+
import { createTable, printTable } from './table-helper.mjs';
|
|
9
|
+
|
|
10
|
+
/** Shared boxen options – single border, dim gray, no padding, left margin for alignment with other blocks. */
|
|
11
|
+
export const BOXEN_OPTS = {
|
|
12
|
+
borderStyle: 'single',
|
|
13
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
14
|
+
margin: { top: 0, right: 0, bottom: 0, left: 2 },
|
|
15
|
+
borderColor: 'gray',
|
|
16
|
+
dimBorder: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wrap content in boxen and return indented string.
|
|
21
|
+
* @param {string} content - Text (may contain ANSI)
|
|
22
|
+
* @param {string} [indent='']
|
|
23
|
+
*/
|
|
24
|
+
export function renderBox(content, indent = '') {
|
|
25
|
+
const box = boxen(content, BOXEN_OPTS);
|
|
26
|
+
return box.split('\n').map((l) => indent + l).join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Strip ANSI escape codes for display length calculation. */
|
|
30
|
+
function stripAnsi(s) {
|
|
31
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print a section heading in a small box.
|
|
36
|
+
* @param {string} title - e.g. "HTTP API", "CLI", "Interactive", or "HTTP API URL: http://..."
|
|
37
|
+
* @param {string} [indent='']
|
|
38
|
+
*/
|
|
39
|
+
export function printSectionHeading(title, indent = '') {
|
|
40
|
+
const content = ` ${C.bold}${C.cyan}${title}${C.reset} `;
|
|
41
|
+
console.log(renderBox(content, indent));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap text to max width; returns array of lines.
|
|
46
|
+
* Works on plain text (ANSI stripped) – use for paths/messages that have no/simple colors.
|
|
47
|
+
*/
|
|
48
|
+
function wrapPlainText(text, maxLen) {
|
|
49
|
+
const plain = stripAnsi(text);
|
|
50
|
+
if (plain.length <= maxLen) return [text];
|
|
51
|
+
const lines = [];
|
|
52
|
+
let remaining = plain;
|
|
53
|
+
while (remaining.length > 0) {
|
|
54
|
+
if (remaining.length <= maxLen) {
|
|
55
|
+
lines.push(remaining);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
let breakAt = maxLen;
|
|
59
|
+
const chunk = remaining.slice(0, maxLen);
|
|
60
|
+
const lastSpace = chunk.lastIndexOf(' ');
|
|
61
|
+
if (lastSpace > maxLen * 0.5) breakAt = lastSpace + 1;
|
|
62
|
+
lines.push(remaining.slice(0, breakAt));
|
|
63
|
+
remaining = remaining.slice(breakAt).replace(/^\s+/, '');
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Print a bullet box – info lines (UNC/CMD, Profile path) as a single-cell table.
|
|
70
|
+
* Long lines wrap to stay within BULLET_BOX_MAX_WIDTH.
|
|
71
|
+
* @param {string[]} lines - Raw lines (no bullets; we add • )
|
|
72
|
+
* @param {string} [indent=' ']
|
|
73
|
+
*/
|
|
74
|
+
const MAX_LINE_WIDTH = 71; // wrap long lines at this (prefix excluded)
|
|
75
|
+
|
|
76
|
+
export function printBulletBox(lines, indent = ' ') {
|
|
77
|
+
if (lines.length === 0) return;
|
|
78
|
+
const bullet = ' • ';
|
|
79
|
+
const contentWidth = MAX_LINE_WIDTH;
|
|
80
|
+
const contentLines = [];
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const plain = stripAnsi(line);
|
|
83
|
+
const wrapped = plain.length <= contentWidth ? [line] : wrapPlainText(plain, contentWidth);
|
|
84
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
85
|
+
const prefix = i === 0 ? `${C.cyan}${bullet}${C.reset}` : ' ';
|
|
86
|
+
const chunk = wrapped[i];
|
|
87
|
+
const display = i === 0 && wrapped.length === 1 ? line : chunk;
|
|
88
|
+
contentLines.push(`${prefix}${display}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const content = contentLines.join('\n');
|
|
92
|
+
const table = createTable({
|
|
93
|
+
colWidths: [74],
|
|
94
|
+
tableOpts: { wordWrap: true, maxWidth: 80 },
|
|
95
|
+
});
|
|
96
|
+
table.push([content]);
|
|
97
|
+
printTable(table, indent);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Print mode heading – Open mode / Join mode with blue background.
|
|
102
|
+
* Call as soon as mode is chosen (o or j), before any connection logic.
|
|
103
|
+
* @param {'Open mode' | 'Join mode'} mode
|
|
104
|
+
* @param {string} [indent=' ']
|
|
105
|
+
*/
|
|
106
|
+
export function printModeHeading(mode, indent = ' ') {
|
|
107
|
+
const titleLine = ` ${mode} `;
|
|
108
|
+
const content = `${C.bgCyan}${C.bold}${C.white}${titleLine}${C.reset}`;
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(renderBox(content, indent));
|
|
111
|
+
}
|
|
112
|
+
|
|
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
|
+
/**
|
|
125
|
+
* Print Join connected block – WSL title (blue bg) + monitored URL. Shown when join succeeds.
|
|
126
|
+
* Single-cell table.
|
|
127
|
+
* @param {string} windowsHostIp - e.g. "172.29.96.1"
|
|
128
|
+
* @param {string} url - Monitored page URL
|
|
129
|
+
* @param {string} [indent=' ']
|
|
130
|
+
*/
|
|
131
|
+
export function printJoinConnectedBlock(windowsHostIp, url, indent = ' ') {
|
|
132
|
+
const titleLine = ` WSL detected, using Windows host IP: ${windowsHostIp}`;
|
|
133
|
+
const content = `${C.bgCyan}${C.bold}${C.white}${titleLine}${C.reset}\n ${C.cyan}${url}${C.reset}`;
|
|
134
|
+
console.log('');
|
|
135
|
+
const table = createTable({
|
|
136
|
+
colWidths: [74],
|
|
137
|
+
tableOpts: { wordWrap: true, maxWidth: 80 },
|
|
138
|
+
});
|
|
139
|
+
table.push([content]);
|
|
140
|
+
printTable(table, indent);
|
|
141
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI table rendering via cli-table3.
|
|
3
|
+
* Supports max width, word wrap, and consistent indent.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
|
|
8
|
+
const INDENT = ' ';
|
|
9
|
+
const DEFAULT_MAX_WIDTH = 80;
|
|
10
|
+
|
|
11
|
+
/** Default table style – dim borders, no built-in header colors (we use C.xxx in content). */
|
|
12
|
+
const DEFAULT_TABLE_STYLE = {
|
|
13
|
+
head: [],
|
|
14
|
+
border: [],
|
|
15
|
+
'padding-left': 1,
|
|
16
|
+
'padding-right': 1,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compute max table width from terminal or param.
|
|
21
|
+
* @param {number} [maxWidth] - Override max width; 0 = use terminal width
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
function getMaxWidth(maxWidth) {
|
|
25
|
+
if (maxWidth != null && maxWidth > 0) return maxWidth;
|
|
26
|
+
const cols = process.stdout.columns;
|
|
27
|
+
if (cols && cols > 20) return Math.min(cols - 4, 100); // reserve space for indent
|
|
28
|
+
return DEFAULT_MAX_WIDTH;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a table with our default style.
|
|
33
|
+
* @param {Object} opts - cli-table3 options (colWidths, head, style, chars, etc.)
|
|
34
|
+
* @param {{ maxWidth?: number, wordWrap?: boolean }} [opts.tableOpts] - maxWidth, wordWrap
|
|
35
|
+
* @returns {Table}
|
|
36
|
+
*/
|
|
37
|
+
export function createTable(opts = {}) {
|
|
38
|
+
const { tableOpts = {}, colWidths, style: styleOverride, ...rest } = opts;
|
|
39
|
+
const { maxWidth, wordWrap = false } = tableOpts;
|
|
40
|
+
const style = { ...DEFAULT_TABLE_STYLE, ...styleOverride };
|
|
41
|
+
const options = { style, ...rest };
|
|
42
|
+
if (wordWrap) {
|
|
43
|
+
options.wordWrap = true;
|
|
44
|
+
const mw = getMaxWidth(maxWidth);
|
|
45
|
+
const left = colWidths?.[0] ?? 12;
|
|
46
|
+
options.colWidths = colWidths ?? [left, mw - left - 6];
|
|
47
|
+
} else if (colWidths) {
|
|
48
|
+
options.colWidths = colWidths;
|
|
49
|
+
}
|
|
50
|
+
return new Table(options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render table to string with indent applied to each line.
|
|
55
|
+
* @param {Table} table
|
|
56
|
+
* @param {string} [indent]
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function formatTable(table, indent = INDENT) {
|
|
60
|
+
const out = table.toString();
|
|
61
|
+
return out.split('\n').map((l) => indent + l).join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Print table with indent.
|
|
66
|
+
* @param {Table} table
|
|
67
|
+
* @param {string} [indent]
|
|
68
|
+
*/
|
|
69
|
+
export function printTable(table, indent = INDENT) {
|
|
70
|
+
console.log(formatTable(table, indent));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { INDENT };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified "wait for Chrome" block – variable title, common prompt.
|
|
3
|
+
* Used when Chrome is not ready and user must start it or fix config.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { C } from '../utils/colors.mjs';
|
|
7
|
+
|
|
8
|
+
/** Common prompt line shown in all wait-for-Chrome scenarios. */
|
|
9
|
+
export const WAIT_FOR_CHROME_PROMPT =
|
|
10
|
+
`${C.yellow}[Monitor]${C.reset} Press ${C.bgGreen}${C.white}${C.bold} ENTER ${C.reset} when Chrome is running, or ${C.red}Ctrl+C${C.reset} to cancel...`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build full content for wait block: title (context) + common prompt.
|
|
14
|
+
* @param {string} [titleContent] - Variable part (e.g. "Chrome found but not accessible...")
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function buildWaitForChromeContent(titleContent) {
|
|
18
|
+
return titleContent ? `${titleContent}\n\n${WAIT_FOR_CHROME_PROMPT}` : WAIT_FOR_CHROME_PROMPT;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI prompt helpers (yes/no, port, etc.).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { C } from './colors.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ask user a yes/no question.
|
|
10
|
+
* @param {string} prompt
|
|
11
|
+
* @returns {Promise<boolean>} true for y/yes, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
export function askYesNo(prompt) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
rl.question(`${prompt} [y/N]: `, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
const normalized = (answer || '').trim().toLowerCase();
|
|
19
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_HTTP_PORT = 60001;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ask user for HTTP API port; default 60001.
|
|
28
|
+
* @param {number} [defaultPort=60001]
|
|
29
|
+
* @returns {Promise<number>} port 1–65535
|
|
30
|
+
*/
|
|
31
|
+
export function askHttpPort(defaultPort = DEFAULT_HTTP_PORT) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
rl.question(` ${C.cyan}HTTP API port${C.reset} (${C.green}Enter${C.reset} = ${defaultPort}): `, (answer) => {
|
|
35
|
+
rl.close();
|
|
36
|
+
const trimmed = (answer || '').trim();
|
|
37
|
+
if (trimmed === '') {
|
|
38
|
+
resolve(defaultPort);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const num = parseInt(trimmed, 10);
|
|
42
|
+
if (Number.isNaN(num) || num < 1 || num > 65535) {
|
|
43
|
+
resolve(defaultPort);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
resolve(num);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the Chrome profile is stored for open mode.
|
|
3
|
+
* Depends on platform: native Linux/Mac = project dir; WSL = Windows path (same drive or LOCALAPPDATA).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { isWsl } from './env.mjs';
|
|
8
|
+
import { getWindowsProfilePath } from '../os/wsl/chrome.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get Chrome profile path and a short description for the current platform.
|
|
12
|
+
* @param {string} projectDir - Project root (outputDir)
|
|
13
|
+
* @returns {{ path: string, where: string }}
|
|
14
|
+
*/
|
|
15
|
+
export function getChromeProfileLocation(projectDir) {
|
|
16
|
+
if (!isWsl()) {
|
|
17
|
+
return {
|
|
18
|
+
path: path.join(projectDir, '.browsermonitor-profile'),
|
|
19
|
+
where: 'Project directory',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (projectDir.startsWith('/mnt/')) {
|
|
24
|
+
const winPath = projectDir
|
|
25
|
+
.replace(/^\/mnt\/([a-z])\//, (_, d) => `${d.toUpperCase()}:\\`)
|
|
26
|
+
.replace(/\//g, '\\');
|
|
27
|
+
return {
|
|
28
|
+
path: `${winPath}\\.browsermonitor-profile`,
|
|
29
|
+
where: 'Windows (same drive as project)',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
path: getWindowsProfilePath(projectDir),
|
|
35
|
+
where: 'Windows (LOCALAPPDATA\\browsermonitor\\)',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI color codes and logging helpers for terminal output.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ANSI color codes
|
|
6
|
+
export const C = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
// Colors
|
|
11
|
+
red: '\x1b[31m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
blue: '\x1b[34m',
|
|
15
|
+
magenta: '\x1b[35m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
white: '\x1b[37m',
|
|
18
|
+
// Bright colors
|
|
19
|
+
brightRed: '\x1b[91m',
|
|
20
|
+
brightGreen: '\x1b[92m',
|
|
21
|
+
brightYellow: '\x1b[93m',
|
|
22
|
+
brightBlue: '\x1b[94m',
|
|
23
|
+
brightMagenta: '\x1b[95m',
|
|
24
|
+
brightCyan: '\x1b[96m',
|
|
25
|
+
// Background
|
|
26
|
+
bgRed: '\x1b[41m',
|
|
27
|
+
bgGreen: '\x1b[42m',
|
|
28
|
+
bgYellow: '\x1b[43m',
|
|
29
|
+
bgBlue: '\x1b[44m',
|
|
30
|
+
bgMagenta: '\x1b[45m',
|
|
31
|
+
bgCyan: '\x1b[46m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Helper functions for colored output
|
|
35
|
+
export const log = {
|
|
36
|
+
info: (msg) => console.log(`${C.cyan}[Monitor]${C.reset} ${msg}`),
|
|
37
|
+
success: (msg) => console.log(`${C.green}[Monitor]${C.reset} ${C.green}${msg}${C.reset}`),
|
|
38
|
+
warn: (msg) => console.log(`${C.yellow}[Monitor]${C.reset} ${C.yellow}${msg}${C.reset}`),
|
|
39
|
+
error: (msg) => console.error(`${C.red}[Monitor]${C.reset} ${C.red}${msg}${C.reset}`),
|
|
40
|
+
dim: (msg) => console.log(`${C.dim}[Monitor] ${msg}${C.reset}`),
|
|
41
|
+
header: (msg) => console.log(`\n${C.bold}${C.brightCyan}═══════════════════════════════════════════════════════════════════════════════${C.reset}`),
|
|
42
|
+
title: (msg) => console.log(`${C.bold}${C.brightCyan} ${msg}${C.reset}`),
|
|
43
|
+
section: (msg) => console.log(`\n${C.bold}${C.white}> ${msg}${C.reset}`),
|
|
44
|
+
key: (key, value) => console.log(` ${C.cyan}${key}:${C.reset} ${value}`),
|
|
45
|
+
keyHighlight: (key, value) => console.log(` ${C.cyan}${key}:${C.reset} ${C.brightGreen}${value}${C.reset}`),
|
|
46
|
+
cmd: (msg) => console.log(` ${C.bgBlue}${C.white} ${msg} ${C.reset}`),
|
|
47
|
+
bullet: (msg) => console.log(` ${C.dim}-${C.reset} ${msg}`),
|
|
48
|
+
status: (label, value, color = C.white) => console.log(` ${C.dim}${label}:${C.reset} ${color}${value}${C.reset}`),
|
|
49
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform / environment detection (Windows, Linux, WSL).
|
|
3
|
+
* Single place for isWindows, isLinux, isWsl so UI and profile path logic can depend on it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
export function isWindows() {
|
|
9
|
+
return process.platform === 'win32';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isLinux() {
|
|
13
|
+
return process.platform === 'linux';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if running in WSL (Linux under Windows).
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function isWsl() {
|
|
21
|
+
if (process.platform !== 'linux') return false;
|
|
22
|
+
if (process.env.WSL_DISTRO_NAME) return true;
|
|
23
|
+
if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop')) return true;
|
|
24
|
+
try {
|
|
25
|
+
const v = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
26
|
+
return v.includes('microsoft') || v.includes('wsl');
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global helper for Chrome profile ID derivation.
|
|
3
|
+
* Profile ID = projectName_hash where hash represents the full project path.
|
|
4
|
+
* Ensures consistent profile naming across open-mode, join-mode, and WSL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute profile ID components from project directory.
|
|
12
|
+
* projectName = basename (e.g. cadcloud-ui); hash = md5 of full absolute path.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} projectDir - Project root (outputDir), any form
|
|
15
|
+
* @returns {{ projectName: string, hash: string, profileId: string }}
|
|
16
|
+
*/
|
|
17
|
+
export function getProfileIdFromProjectDir(projectDir) {
|
|
18
|
+
const absolutePath = path.resolve(projectDir);
|
|
19
|
+
const projectName = path.basename(absolutePath).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
20
|
+
const hash = crypto.createHash('md5').update(absolutePath).digest('hex').substring(0, 12);
|
|
21
|
+
const profileId = `${projectName}_${hash}`;
|
|
22
|
+
return { projectName, hash, profileId };
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-line status output – overwrites same line, no line inflation.
|
|
3
|
+
* Use during long operations (join, connect, etc.) to show progress.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Strip ANSI codes for display length. */
|
|
7
|
+
function stripAnsi(s) {
|
|
8
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let lastLength = 0;
|
|
12
|
+
const INDENT = ' ';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write status message on current line (overwrites previous).
|
|
16
|
+
* Only works when stdout is TTY.
|
|
17
|
+
* @param {string} msg - Message (may contain ANSI)
|
|
18
|
+
*/
|
|
19
|
+
export function writeStatusLine(msg) {
|
|
20
|
+
if (!process.stdout.isTTY) return;
|
|
21
|
+
const plain = stripAnsi(msg);
|
|
22
|
+
const full = INDENT + msg;
|
|
23
|
+
const pad = lastLength > plain.length + INDENT.length ? ' '.repeat(lastLength - plain.length - INDENT.length) : '';
|
|
24
|
+
lastLength = Math.max(lastLength, plain.length + INDENT.length);
|
|
25
|
+
process.stdout.write('\r' + full + pad);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clear status line. Optionally add newline so next output starts on fresh line.
|
|
30
|
+
* Call before prompts or when done.
|
|
31
|
+
* @param {boolean} [ensureNewline=false] - If true, write \\n after clearing
|
|
32
|
+
*/
|
|
33
|
+
export function clearStatusLine(ensureNewline = false) {
|
|
34
|
+
if (!process.stdout.isTTY || lastLength === 0) return;
|
|
35
|
+
process.stdout.write('\r' + ' '.repeat(lastLength) + '\r');
|
|
36
|
+
if (ensureNewline) process.stdout.write('\n');
|
|
37
|
+
lastLength = 0;
|
|
38
|
+
}
|
|
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
|
+
}
|