@guanzhu.me/pw-cli 0.0.1

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/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@guanzhu.me/pw-cli",
3
+ "version": "0.0.1",
4
+ "description": "Persistent Playwright browser CLI with headed defaults, profile support, queueing, and script execution",
5
+ "bin": {
6
+ "pw-cli": "./bin/pw-cli.js"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "lint": "node scripts/check-syntax.js",
13
+ "test": "node --test",
14
+ "verify": "npm run lint && npm test"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "src/",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "peerDependencies": {
26
+ "playwright": ">=1.40.0",
27
+ "@playwright/cli": ">=0.1.0"
28
+ },
29
+ "keywords": [
30
+ "playwright",
31
+ "cli",
32
+ "browser",
33
+ "automation",
34
+ "chromium",
35
+ "testing"
36
+ ],
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const fs = require('fs');
7
+ const crypto = require('crypto');
8
+ const { readState, writeState, clearState, getProfileDir } = require('./state');
9
+ const { probeCDP, findFreePort, sleep } = require('./utils');
10
+
11
+ const DAEMON_SCRIPT = path.join(__dirname, 'launch-daemon.js');
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // playwright-cli session integration
15
+ // ---------------------------------------------------------------------------
16
+ const HOME_DIR = os.homedir();
17
+ const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
18
+ const WORKSPACE_HASH = crypto.createHash('sha1')
19
+ .update(PW_CLI_DIR)
20
+ .digest('hex')
21
+ .substring(0, 16);
22
+
23
+ function getDaemonDir() {
24
+ if (process.platform === 'win32') {
25
+ return path.join(process.env.LOCALAPPDATA || path.join(HOME_DIR, 'AppData', 'Local'), 'ms-playwright', 'daemon');
26
+ } else if (process.platform === 'darwin') {
27
+ return path.join(HOME_DIR, 'Library', 'Caches', 'ms-playwright', 'daemon');
28
+ }
29
+ return path.join(process.env.XDG_CACHE_HOME || path.join(HOME_DIR, '.cache'), 'ms-playwright', 'daemon');
30
+ }
31
+
32
+ function readPlaywrightCliSession(sessionName = 'default') {
33
+ const sessionFile = path.join(getDaemonDir(), WORKSPACE_HASH, `${sessionName}.session`);
34
+ try {
35
+ return JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function getPlaywrightCliCdpPort(sessionName = 'default') {
42
+ const session = readPlaywrightCliSession(sessionName);
43
+ return session?.resolvedConfig?.browser?.launchOptions?.cdpPort || null;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Our own CDP-based browser launcher (fallback when playwright-cli not running)
48
+ // ---------------------------------------------------------------------------
49
+ async function launchBrowser({ headless = false, profile = 'default', port: preferredPort = 9223 } = {}) {
50
+ const profileDir = getProfileDir(profile);
51
+ const port = await findFreePort(preferredPort);
52
+
53
+ const daemonArgs = [
54
+ DAEMON_SCRIPT,
55
+ '--profile-dir', profileDir,
56
+ '--port', String(port),
57
+ ];
58
+ if (headless) daemonArgs.push('--headless');
59
+
60
+ return new Promise((resolve, reject) => {
61
+ const child = spawn(process.execPath, daemonArgs, {
62
+ detached: true,
63
+ stdio: ['ignore', 'pipe', 'ignore'],
64
+ });
65
+
66
+ let output = '';
67
+ const timer = setTimeout(() => {
68
+ child.stdout.destroy();
69
+ reject(new Error('Browser launch timed out (15s)'));
70
+ }, 15000);
71
+
72
+ child.stdout.on('data', chunk => {
73
+ output += chunk.toString();
74
+ const readyMatch = output.match(/READY:(\d+)/);
75
+ const errorMatch = output.match(/ERROR:(.*)/);
76
+
77
+ if (readyMatch) {
78
+ clearTimeout(timer);
79
+ const actualPort = parseInt(readyMatch[1], 10);
80
+ writeState({ port: actualPort, cdpUrl: `http://127.0.0.1:${actualPort}`, profile });
81
+ child.unref();
82
+ resolve(actualPort);
83
+ } else if (errorMatch) {
84
+ clearTimeout(timer);
85
+ reject(new Error(`Browser launch failed: ${errorMatch[1]}`));
86
+ }
87
+ });
88
+
89
+ child.on('error', err => {
90
+ clearTimeout(timer);
91
+ reject(err);
92
+ });
93
+
94
+ child.on('exit', (code) => {
95
+ if (code !== null && code !== 0) {
96
+ clearTimeout(timer);
97
+ reject(new Error(`Daemon exited with code ${code}`));
98
+ }
99
+ });
100
+ });
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // getConnection — tries playwright-cli browser first, then our own
105
+ // ---------------------------------------------------------------------------
106
+ async function getConnection({ headless = false, profile = 'default', port: preferredPort = 9223 } = {}) {
107
+ let playwright;
108
+ try {
109
+ playwright = require('playwright');
110
+ } catch {
111
+ throw new Error('playwright is not installed. Run: npm install -g playwright');
112
+ }
113
+
114
+ // 1. Try to reuse playwright-cli's browser via its CDP port
115
+ const cliCdpPort = getPlaywrightCliCdpPort();
116
+ if (cliCdpPort) {
117
+ const alive = await probeCDP(cliCdpPort, 2000);
118
+ if (alive) {
119
+ try {
120
+ const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cliCdpPort}`);
121
+ const contexts = browser.contexts();
122
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
123
+ const pages = context.pages();
124
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
125
+ return { browser, context, page, playwright };
126
+ } catch {
127
+ // fall through to own browser
128
+ }
129
+ }
130
+ }
131
+
132
+ // 2. Try our own CDP browser (state file)
133
+ let state = readState();
134
+ let cdpUrl;
135
+
136
+ if (state) {
137
+ const alive = await probeCDP(state.port, 2000);
138
+ if (alive) {
139
+ cdpUrl = state.cdpUrl;
140
+ } else {
141
+ clearState();
142
+ state = null;
143
+ }
144
+ }
145
+
146
+ if (!state) {
147
+ const port = await launchBrowser({ headless, profile, port: preferredPort });
148
+ cdpUrl = `http://127.0.0.1:${port}`;
149
+ await sleep(200);
150
+ }
151
+
152
+ const browser = await playwright.chromium.connectOverCDP(cdpUrl);
153
+ const contexts = browser.contexts();
154
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
155
+ const pages = context.pages();
156
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
157
+
158
+ return { browser, context, page, playwright };
159
+ }
160
+
161
+ async function killBrowser() {
162
+ const state = readState();
163
+ if (!state) return false;
164
+
165
+ const alive = await probeCDP(state.port, 1000);
166
+ if (alive) {
167
+ try {
168
+ const playwright = require('playwright');
169
+ const browser = await playwright.chromium.connectOverCDP(state.cdpUrl);
170
+ await browser.close();
171
+ } catch { /* ignore */ }
172
+ }
173
+
174
+ clearState();
175
+ return true;
176
+ }
177
+
178
+ module.exports = { getConnection, killBrowser };
package/src/cli.js ADDED
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const { getConnection, killBrowser } = require('./browser-manager');
4
+ const { execCode, execScript } = require('./executor');
5
+ const { readState } = require('./state');
6
+ const { readStdin, die, probeCDP } = require('./utils');
7
+
8
+ function parseArgs(argv) {
9
+ const global = { headless: false, profile: 'default', port: 9222 };
10
+ const rest = [];
11
+ let i = 0;
12
+
13
+ while (i < argv.length) {
14
+ const arg = argv[i];
15
+ if (arg === '--headless') {
16
+ global.headless = true;
17
+ } else if (arg === '--profile' && argv[i + 1]) {
18
+ global.profile = argv[++i];
19
+ } else if (arg === '--port' && argv[i + 1]) {
20
+ global.port = parseInt(argv[++i], 10);
21
+ } else {
22
+ rest.push(arg);
23
+ }
24
+ i++;
25
+ }
26
+
27
+ return { global, rest };
28
+ }
29
+
30
+ async function cmdRunCode(rest, opts) {
31
+ let code;
32
+
33
+ if (rest.length > 0) {
34
+ code = rest.join(' ');
35
+ } else if (!process.stdin.isTTY) {
36
+ code = await readStdin();
37
+ } else {
38
+ die('No code provided. Pass inline or pipe via stdin.\n\nExamples:\n pw-cli run-code "await page.goto(\'https://example.com\')"\n cat script.js | pw-cli run-code');
39
+ }
40
+
41
+ if (!code) die('Empty code provided.');
42
+
43
+ const conn = await getConnection(opts);
44
+ try {
45
+ const result = await execCode(code, conn);
46
+ if (result !== undefined) {
47
+ console.log(result);
48
+ }
49
+ } finally {
50
+ await conn.browser.close(); // disconnect only, browser keeps running
51
+ }
52
+ }
53
+
54
+ async function cmdRunScript(rest, opts) {
55
+ const [scriptPath, ...scriptArgs] = rest;
56
+ if (!scriptPath) {
57
+ die('No script path provided.\n\nUsage: pw-cli run-script <file.js> [args...]');
58
+ }
59
+
60
+ const conn = await getConnection(opts);
61
+ try {
62
+ const result = await execScript(scriptPath, scriptArgs, conn);
63
+ if (result !== undefined) {
64
+ console.log(result);
65
+ }
66
+ } finally {
67
+ await conn.browser.close();
68
+ }
69
+ }
70
+
71
+ async function cmdKill() {
72
+ const killed = await killBrowser();
73
+ console.log(killed ? 'Browser stopped.' : 'No browser running.');
74
+ }
75
+
76
+ async function cmdStatus() {
77
+ const state = readState();
78
+ if (!state) {
79
+ console.log('Status: stopped');
80
+ return;
81
+ }
82
+ const alive = await probeCDP(state.port, 2000);
83
+ if (alive) {
84
+ console.log(`Status: running`);
85
+ console.log(` CDP: ${state.cdpUrl}`);
86
+ console.log(` Profile: ${state.profile || 'default'}`);
87
+ } else {
88
+ console.log('Status: stopped (stale state file cleared)');
89
+ const { clearState } = require('./state');
90
+ clearState();
91
+ }
92
+ }
93
+
94
+ function printHelp() {
95
+ console.log(`
96
+ pw-cli — Persistent Playwright browser CLI
97
+
98
+ USAGE
99
+ pw-cli [--headless] [--profile <name>] [--port <number>] <command> [...]
100
+
101
+ GLOBAL OPTIONS
102
+ --headless Run browser headlessly (default: headed)
103
+ --profile <name> Named profile to use (default: "default")
104
+ --port <number> CDP port (default: 9222)
105
+
106
+ COMMANDS
107
+ run-code [code] Execute inline JS (reads stdin if omitted)
108
+ run-script <file> [...] Execute a .js script with optional args
109
+ kill Stop the running browser
110
+ status Show browser status
111
+
112
+ SCRIPT GLOBALS
113
+ page, context, browser, playwright, args, require, __filename, __dirname
114
+
115
+ EXAMPLES
116
+ pw-cli run-code "await page.goto('https://example.com'); console.log(await page.title())"
117
+ echo "await page.screenshot({ path: 'out.png' })" | pw-cli run-code
118
+ pw-cli run-script ./scrape.js --url https://example.com
119
+ pw-cli --headless run-code "await page.goto('https://example.com')"
120
+ pw-cli --profile work status
121
+ pw-cli kill
122
+ `.trim());
123
+ }
124
+
125
+ async function run(argv) {
126
+ const { global: opts, rest } = parseArgs(argv);
127
+ const [command, ...cmdArgs] = rest;
128
+
129
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
130
+ printHelp();
131
+ return;
132
+ }
133
+
134
+ try {
135
+ switch (command) {
136
+ case 'run-code':
137
+ await cmdRunCode(cmdArgs, opts);
138
+ break;
139
+ case 'run-script':
140
+ await cmdRunScript(cmdArgs, opts);
141
+ break;
142
+ case 'kill':
143
+ await cmdKill();
144
+ break;
145
+ case 'status':
146
+ await cmdStatus();
147
+ break;
148
+ default:
149
+ die(`Unknown command: ${command}\nRun "pw-cli help" for usage.`);
150
+ }
151
+ } catch (err) {
152
+ if (err.code === 'ENOENT' && err.message.includes('Script not found')) {
153
+ process.exit(3);
154
+ }
155
+ if (err.message && (err.message.includes('Target closed') || err.message.includes('Connection closed') || err.message.includes('Protocol error'))) {
156
+ const { clearState } = require('./state');
157
+ clearState();
158
+ process.stderr.write(`pw-cli error: Browser disconnected unexpectedly. Run the command again to relaunch.\n\nDetails: ${err.message}\n`);
159
+ process.exit(2);
160
+ }
161
+ process.stderr.write(`pw-cli error: ${err.message || err}\n`);
162
+ process.exit(1);
163
+ }
164
+ // Force exit: playwright CDP connections keep the event loop alive
165
+ process.exit(0);
166
+ }
167
+
168
+ module.exports = { run };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function isFunctionExpression(code) {
7
+ const text = code.trim();
8
+ return /^(async\s+function\b|function\b|async\s*(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/.test(text);
9
+ }
10
+
11
+ async function runFunctionExpression(code, globals) {
12
+ const factory = new Function(...Object.keys(globals), `return (${code});`);
13
+ const maybeFn = factory(...Object.values(globals));
14
+
15
+ if (typeof maybeFn === 'function') {
16
+ return maybeFn(
17
+ globals.page,
18
+ globals.context,
19
+ globals.browser,
20
+ globals.playwright,
21
+ globals.args
22
+ );
23
+ }
24
+
25
+ return maybeFn;
26
+ }
27
+
28
+ // Use AsyncFunction constructor to execute in the same V8 context as playwright objects.
29
+ // This avoids vm.runInNewContext's cross-context prototype issues.
30
+ async function runCode(code, globals) {
31
+ if (isFunctionExpression(code)) {
32
+ return runFunctionExpression(code, globals);
33
+ }
34
+
35
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
36
+ const fn = new AsyncFunction(...Object.keys(globals), code);
37
+ return fn.call(globals, ...Object.values(globals));
38
+ }
39
+
40
+ async function execCode(code, { browser, context, page, playwright }) {
41
+ const globals = {
42
+ browser,
43
+ context,
44
+ page,
45
+ playwright,
46
+ require,
47
+ console,
48
+ process,
49
+ };
50
+ return runCode(code, globals);
51
+ }
52
+
53
+ async function execScript(scriptPath, scriptArgs, { browser, context, page, playwright }) {
54
+ const absPath = path.resolve(scriptPath);
55
+ if (!fs.existsSync(absPath)) {
56
+ const err = new Error(`Script not found: ${absPath}`);
57
+ err.code = 'ENOENT';
58
+ throw err;
59
+ }
60
+
61
+ const code = fs.readFileSync(absPath, 'utf8');
62
+ const globals = {
63
+ browser,
64
+ context,
65
+ page,
66
+ playwright,
67
+ args: scriptArgs,
68
+ require,
69
+ console,
70
+ process,
71
+ __filename: absPath,
72
+ __dirname: path.dirname(absPath),
73
+ };
74
+ return runCode(code, globals);
75
+ }
76
+
77
+ module.exports = { execCode, execScript };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ // This script is spawned as a detached child process.
4
+ // It launches a persistent Chromium browser and holds it open.
5
+ // It signals readiness by writing "READY:<port>\n" to stdout.
6
+ // Args: --profile-dir <dir> --port <port> [--headless]
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ function getArg(name) {
11
+ const i = args.indexOf(name);
12
+ return i !== -1 ? args[i + 1] : undefined;
13
+ }
14
+
15
+ const profileDir = getArg('--profile-dir');
16
+ const port = parseInt(getArg('--port') || '9222', 10);
17
+ const headless = args.includes('--headless');
18
+
19
+ if (!profileDir) {
20
+ process.stderr.write('ERROR:missing --profile-dir\n');
21
+ process.exit(1);
22
+ }
23
+
24
+ (async () => {
25
+ let playwright;
26
+ try {
27
+ playwright = require('playwright');
28
+ } catch (e) {
29
+ process.stdout.write(`ERROR:playwright not found - run: npm install -g playwright\n`);
30
+ process.exit(1);
31
+ }
32
+
33
+ try {
34
+ const context = await playwright.chromium.launchPersistentContext(profileDir, {
35
+ headless,
36
+ args: [`--remote-debugging-port=${port}`],
37
+ ignoreDefaultArgs: ['--enable-automation'],
38
+ });
39
+
40
+ // Wait briefly for CDP to be ready, then signal
41
+ const { probeCDP, sleep } = require('./utils');
42
+ let ready = false;
43
+ for (let i = 0; i < 20; i++) {
44
+ if (await probeCDP(port, 1000)) { ready = true; break; }
45
+ await sleep(300);
46
+ }
47
+
48
+ if (!ready) {
49
+ process.stdout.write(`ERROR:CDP not available on port ${port} after launch\n`);
50
+ await context.close();
51
+ process.exit(1);
52
+ }
53
+
54
+ process.stdout.write(`READY:${port}\n`);
55
+
56
+ // Keep process alive until browser closes
57
+ context.on('close', () => process.exit(0));
58
+
59
+ // Prevent premature exit
60
+ process.stdin.resume();
61
+ } catch (e) {
62
+ process.stdout.write(`ERROR:${e.message}\n`);
63
+ process.exit(1);
64
+ }
65
+ })();
package/src/queue.js ADDED
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+
7
+ const HOME_DIR = os.homedir();
8
+ const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
9
+ const QUEUE_FILE = path.join(PW_CLI_DIR, 'queue.json');
10
+
11
+ function readQueue() {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(QUEUE_FILE, 'utf8'));
14
+ } catch {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function saveQueue(items) {
20
+ fs.mkdirSync(PW_CLI_DIR, { recursive: true });
21
+ const tmp = QUEUE_FILE + '.tmp';
22
+ fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf8');
23
+ fs.renameSync(tmp, QUEUE_FILE);
24
+ }
25
+
26
+ function addItem(command, args) {
27
+ const queue = readQueue();
28
+ const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
29
+ const item = { id, command, args, addedAt: new Date().toISOString() };
30
+ queue.push(item);
31
+ saveQueue(queue);
32
+ return item;
33
+ }
34
+
35
+ function removeItem(idPrefix) {
36
+ const queue = readQueue();
37
+ const idx = queue.findIndex(i => i.id === idPrefix || i.id.startsWith(idPrefix));
38
+ if (idx === -1) return null;
39
+ const [removed] = queue.splice(idx, 1);
40
+ saveQueue(queue);
41
+ return removed;
42
+ }
43
+
44
+ function clearQueue() {
45
+ saveQueue([]);
46
+ }
47
+
48
+ module.exports = { readQueue, saveQueue, addItem, removeItem, clearQueue, QUEUE_FILE };
package/src/state.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ function getPwCliDir() {
8
+ return path.join(os.homedir(), '.pw-cli');
9
+ }
10
+
11
+ function getStateFile() {
12
+ return path.join(getPwCliDir(), 'browser.json');
13
+ }
14
+
15
+ function getProfileDir(profile = 'default') {
16
+ const dir = path.join(getPwCliDir(), 'profiles', profile);
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ return dir;
19
+ }
20
+
21
+ function readState() {
22
+ try {
23
+ const raw = fs.readFileSync(getStateFile(), 'utf8');
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function writeState(state) {
31
+ const dir = getPwCliDir();
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ const file = getStateFile();
34
+ const tmp = file + '.tmp';
35
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
36
+ fs.renameSync(tmp, file);
37
+ }
38
+
39
+ function clearState() {
40
+ try {
41
+ fs.unlinkSync(getStateFile());
42
+ } catch {
43
+ // already gone
44
+ }
45
+ }
46
+
47
+ module.exports = { getPwCliDir, getProfileDir, readState, writeState, clearState };
package/src/utils.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const net = require('net');
5
+
6
+ function readStdin() {
7
+ return new Promise((resolve, reject) => {
8
+ let data = '';
9
+ process.stdin.setEncoding('utf8');
10
+ process.stdin.on('data', chunk => { data += chunk; });
11
+ process.stdin.on('end', () => resolve(data.trim()));
12
+ process.stdin.on('error', reject);
13
+ });
14
+ }
15
+
16
+ function die(msg, code = 1) {
17
+ process.stderr.write(`pw-cli error: ${msg}\n`);
18
+ process.exit(code);
19
+ }
20
+
21
+ function probeCDP(port, timeout = 3000) {
22
+ return new Promise(resolve => {
23
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, { timeout }, res => {
24
+ res.resume();
25
+ resolve(res.statusCode === 200);
26
+ });
27
+ req.on('error', () => resolve(false));
28
+ req.on('timeout', () => { req.destroy(); resolve(false); });
29
+ });
30
+ }
31
+
32
+ function findFreePort(preferred = 9222) {
33
+ return new Promise((resolve, reject) => {
34
+ const server = net.createServer();
35
+ server.listen(preferred, '127.0.0.1', () => {
36
+ const { port } = server.address();
37
+ server.close(() => resolve(port));
38
+ });
39
+ server.on('error', () => {
40
+ // preferred port busy, get a random free one
41
+ const s2 = net.createServer();
42
+ s2.listen(0, '127.0.0.1', () => {
43
+ const { port } = s2.address();
44
+ s2.close(() => resolve(port));
45
+ });
46
+ s2.on('error', reject);
47
+ });
48
+ });
49
+ }
50
+
51
+ function sleep(ms) {
52
+ return new Promise(r => setTimeout(r, ms));
53
+ }
54
+
55
+ module.exports = { readStdin, die, probeCDP, findFreePort, sleep };