rechrome 1.28.0 → 1.28.2
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 +1 -1
- package/daemon-manager.js +21 -2
- package/daemon-manager.ts +33 -2
- package/package.json +4 -2
- package/rechrome.js +106 -21
- package/rechrome.ts +106 -21
package/README.md
CHANGED
|
@@ -285,7 +285,7 @@ directory. Pass `-s=<name>` for a named sub-session, or `--isolate` for a throwa
|
|
|
285
285
|
```bash
|
|
286
286
|
git clone https://github.com/snomiao/rechrome.git
|
|
287
287
|
cd rechrome
|
|
288
|
-
bun install
|
|
288
|
+
bun install # `prepare` also builds vendor/ from vendor-src/, so the checkout has a working CLI
|
|
289
289
|
bun link # makes this checkout the global rechrome / rech
|
|
290
290
|
bun test ./*.test.ts ./*.spec.ts ./scripts/*.test.ts # rechrome's own tests (plain `bun test` also finds the vendored forks' suites)
|
|
291
291
|
```
|
package/daemon-manager.js
CHANGED
|
@@ -24,13 +24,14 @@ function oxmgrHasWinfix(version) {
|
|
|
24
24
|
function pickDaemonManager(opts) {
|
|
25
25
|
const override = opts.override?.toLowerCase();
|
|
26
26
|
if (override === "oxmgr" && !opts.oxmgrBin) {
|
|
27
|
-
throw new Error("RECH_DAEMON_MANAGER=oxmgr, but oxmgr is not on PATH. Install
|
|
27
|
+
throw new Error("RECH_DAEMON_MANAGER=oxmgr, but oxmgr is not on PATH. Install it with `bun i -g oxmgr` and ensure the global bin directory is on PATH.");
|
|
28
28
|
}
|
|
29
29
|
if (override === "pm2" && !opts.pm2Bin) {
|
|
30
30
|
throw new Error("RECH_DAEMON_MANAGER=pm2, but pm2 is not on PATH. Install it with `bun add -g pm2` and ensure the global bin directory is on PATH.");
|
|
31
31
|
}
|
|
32
32
|
if (!opts.oxmgrBin && !opts.pm2Bin) {
|
|
33
|
-
|
|
33
|
+
const install = opts.isWindows ? "Install oxmgr with `bun i -g oxmgr` (on Windows, pm2 via `bun add -g pm2` is preferred unless oxmgr is the +winfix build)" : "Install oxmgr with `bun i -g oxmgr`";
|
|
34
|
+
throw new Error(`No daemon process manager found on PATH (oxmgr or pm2). ${install}, ensure the global bin directory is on PATH, then rerun \`bunx rechrome setup\`.`);
|
|
34
35
|
}
|
|
35
36
|
const oxmgr = { id: "oxmgr", bin: opts.oxmgrBin ?? "oxmgr" };
|
|
36
37
|
const pm2 = { id: "pm2", bin: opts.pm2Bin ?? "pm2" };
|
|
@@ -62,9 +63,27 @@ function oxmgrInstallCommand(env) {
|
|
|
62
63
|
return ["npm", "i", "-g", "oxmgr"];
|
|
63
64
|
return ["bun", "i", "-g", "oxmgr"];
|
|
64
65
|
}
|
|
66
|
+
var PM2_DEPRECATION = "pm2 is deprecated as the rech daemon manager; install oxmgr (`bun i -g oxmgr`) and rerun `rech setup` to migrate.";
|
|
67
|
+
function isDeprecatedPm2Fallback(mgr, opts) {
|
|
68
|
+
return mgr.id === "pm2" && !opts.isWindows && !opts.override;
|
|
69
|
+
}
|
|
70
|
+
function listsProcess(id, output, name) {
|
|
71
|
+
if (id === "pm2") {
|
|
72
|
+
try {
|
|
73
|
+
const list = JSON.parse(output);
|
|
74
|
+
return Array.isArray(list) && list.some((p) => p?.name === name);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return output.split(/[\s\u2502|\u2503]+/).includes(name);
|
|
80
|
+
}
|
|
65
81
|
export {
|
|
66
82
|
OXMGR_WINFIX_BASE,
|
|
83
|
+
PM2_DEPRECATION,
|
|
67
84
|
compareVersion,
|
|
85
|
+
isDeprecatedPm2Fallback,
|
|
86
|
+
listsProcess,
|
|
68
87
|
oxmgrHasWinfix,
|
|
69
88
|
oxmgrInstallCommand,
|
|
70
89
|
pickDaemonManager
|
package/daemon-manager.ts
CHANGED
|
@@ -58,13 +58,18 @@ export function pickDaemonManager(opts: {
|
|
|
58
58
|
}): DaemonManager {
|
|
59
59
|
const override = opts.override?.toLowerCase();
|
|
60
60
|
if (override === "oxmgr" && !opts.oxmgrBin) {
|
|
61
|
-
throw new Error("RECH_DAEMON_MANAGER=oxmgr, but oxmgr is not on PATH. Install
|
|
61
|
+
throw new Error("RECH_DAEMON_MANAGER=oxmgr, but oxmgr is not on PATH. Install it with `bun i -g oxmgr` and ensure the global bin directory is on PATH.");
|
|
62
62
|
}
|
|
63
63
|
if (override === "pm2" && !opts.pm2Bin) {
|
|
64
64
|
throw new Error("RECH_DAEMON_MANAGER=pm2, but pm2 is not on PATH. Install it with `bun add -g pm2` and ensure the global bin directory is on PATH.");
|
|
65
65
|
}
|
|
66
66
|
if (!opts.oxmgrBin && !opts.pm2Bin) {
|
|
67
|
-
|
|
67
|
+
// oxmgr is the recommended manager. On Windows stock oxmgr is only used when pm2 is
|
|
68
|
+
// absent (see below), so pm2 stays a documented alternative there.
|
|
69
|
+
const install = opts.isWindows
|
|
70
|
+
? "Install oxmgr with `bun i -g oxmgr` (on Windows, pm2 via `bun add -g pm2` is preferred unless oxmgr is the +winfix build)"
|
|
71
|
+
: "Install oxmgr with `bun i -g oxmgr`";
|
|
72
|
+
throw new Error(`No daemon process manager found on PATH (oxmgr or pm2). ${install}, ensure the global bin directory is on PATH, then rerun \`bunx rechrome setup\`.`);
|
|
68
73
|
}
|
|
69
74
|
const oxmgr: DaemonManager = { id: "oxmgr", bin: opts.oxmgrBin ?? "oxmgr" };
|
|
70
75
|
const pm2: DaemonManager = { id: "pm2", bin: opts.pm2Bin ?? "pm2" };
|
|
@@ -97,3 +102,29 @@ export function oxmgrInstallCommand(env: { npm_config_user_agent?: string; npm_e
|
|
|
97
102
|
if (/(^|\/)(npm|npx)(-cli\.js|\.cmd|\.exe)?$/i.test(execPath)) return ["npm", "i", "-g", "oxmgr"];
|
|
98
103
|
return ["bun", "i", "-g", "oxmgr"];
|
|
99
104
|
}
|
|
105
|
+
|
|
106
|
+
export const PM2_DEPRECATION =
|
|
107
|
+
"pm2 is deprecated as the rech daemon manager; install oxmgr (`bun i -g oxmgr`) and rerun `rech setup` to migrate.";
|
|
108
|
+
|
|
109
|
+
// Whether `mgr` is pm2 only as a stopgap because oxmgr is missing: the case where the user
|
|
110
|
+
// should be nudged to oxmgr. Never on Windows, where pm2 is the deliberate choice over stock
|
|
111
|
+
// (non-winfix) oxmgr and installing oxmgr would not change the pick, and never for an explicit
|
|
112
|
+
// RECH_DAEMON_MANAGER=pm2.
|
|
113
|
+
export function isDeprecatedPm2Fallback(mgr: DaemonManager, opts: { isWindows: boolean; override?: string | null }): boolean {
|
|
114
|
+
return mgr.id === "pm2" && !opts.isWindows && !opts.override;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Whether a manager's process list (pm2 `jlist` JSON / oxmgr `list` table) registers exactly
|
|
118
|
+
// `name`. A substring test false-positives on other processes whose names, paths or args
|
|
119
|
+
// mention it (a pm2 app running from a `.../rechrome/...` checkout, or legacy `rechrome-serve`).
|
|
120
|
+
export function listsProcess(id: DaemonManager["id"], output: string, name: string): boolean {
|
|
121
|
+
if (id === "pm2") {
|
|
122
|
+
try {
|
|
123
|
+
const list = JSON.parse(output);
|
|
124
|
+
return Array.isArray(list) && list.some((p: { name?: unknown } | null) => p?.name === name);
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return output.split(/[\s│|┃]+/).includes(name);
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rechrome",
|
|
3
|
-
"version": "1.28.
|
|
3
|
+
"version": "1.28.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/snomiao/rechrome.git"
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"serve": "bun run rechrome.ts serve",
|
|
33
33
|
"test": "bun test",
|
|
34
34
|
"vendor-cli": "bash scripts/vendor-cli.sh",
|
|
35
|
-
"
|
|
35
|
+
"prepare": "bash scripts/vendor-cli.sh --prepare",
|
|
36
|
+
"check-pack": "bash scripts/check-pack.sh",
|
|
37
|
+
"prepublishOnly": "bash scripts/vendor-cli.sh && sed 's/\\.ts/\\.js/g' rechrome.ts > rechrome.js && sed 's/\\.ts/\\.js/g' serve.ts > serve.js && bun build extension-token.ts --target=bun --outfile=extension-token.js && bun build daemon-manager.ts --target=bun --outfile=daemon-manager.js && bun build listeners.ts --target=bun --outfile=listeners.js && bash scripts/check-pack.sh"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/bun": "latest",
|
package/rechrome.js
CHANGED
|
@@ -10,9 +10,10 @@ import { hostname, homedir, networkInterfaces } from "os";
|
|
|
10
10
|
import { isIPv4 } from "net";
|
|
11
11
|
import { join, basename, dirname } from "path";
|
|
12
12
|
import { pathToFileURL } from "url";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
13
14
|
import { spawn as cpSpawn } from "child_process";
|
|
14
15
|
import { readFile, writeFile, rename, chmod, mkdir } from "node:fs/promises";
|
|
15
|
-
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.js";
|
|
16
|
+
import { isDeprecatedPm2Fallback, listsProcess, oxmgrInstallCommand, pickDaemonManager, PM2_DEPRECATION, type DaemonManager } from "./daemon-manager.js";
|
|
16
17
|
|
|
17
18
|
export const ENV_KEY = "RECHROME_URL";
|
|
18
19
|
export const DEFAULT_PORT = 13775;
|
|
@@ -976,15 +977,45 @@ async function callServe(
|
|
|
976
977
|
console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
|
|
977
978
|
process.exit(1);
|
|
978
979
|
}
|
|
979
|
-
|
|
980
|
+
const text = await res.text();
|
|
981
|
+
try {
|
|
982
|
+
return JSON.parse(text);
|
|
983
|
+
} catch {
|
|
984
|
+
// Not the daemon answering, e.g. a reverse proxy's 404 because the URL's path prefix is wrong.
|
|
985
|
+
const detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
|
|
986
|
+
if (throwOnFailure) throw new Error(detail);
|
|
987
|
+
console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
980
990
|
}
|
|
981
991
|
|
|
992
|
+
const BOOLEAN_OPEN_FLAGS = new Set(["--headed", "--persistent", "--in-memory", "--extension"]);
|
|
993
|
+
|
|
982
994
|
export function normalizeCommandArgs(args: string[]): string[] {
|
|
983
995
|
const normalized = [...args];
|
|
984
996
|
if (normalized[0] === "tabs" || normalized[0] === "list") normalized[0] = "tab-list";
|
|
997
|
+
// `rech open hello.com`: profile-scoped listeners accept only HTTP(S)/about:blank targets, so
|
|
998
|
+
// give a bare host an https:// scheme the way a browser address bar would.
|
|
999
|
+
if (["open", "goto", "tab-new"].includes(normalized[0])) {
|
|
1000
|
+
// The target is the first positional. A token after a `--flag` without `=` is that flag's
|
|
1001
|
+
// value (`open --profile my-profile url`), unless the flag is a known boolean.
|
|
1002
|
+
const takesValue = (flag: string) => flag.startsWith("-") && !flag.includes("=") && !BOOLEAN_OPEN_FLAGS.has(flag);
|
|
1003
|
+
const i = normalized.findIndex((a, idx) => idx > 0 && !a.startsWith("-") && !takesValue(normalized[idx - 1]!));
|
|
1004
|
+
if (i > 0) normalized[i] = withDefaultScheme(normalized[i]!);
|
|
1005
|
+
}
|
|
985
1006
|
return normalized;
|
|
986
1007
|
}
|
|
987
1008
|
|
|
1009
|
+
/** `hello.com` -> `https://hello.com`, `localhost:3000` -> `http://localhost:3000`; URLs with a scheme and paths are unchanged. */
|
|
1010
|
+
export function withDefaultScheme(target: string): string {
|
|
1011
|
+
if (/^[./\\~]/.test(target) || /^[a-z]:[\\/]/i.test(target)) return target; // a file path, not a host
|
|
1012
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[^/:]+:\d+(\/|$|[?#])/.test(target)) return target;
|
|
1013
|
+
// Loopback by exact hostname: `localhost.example.com` / `127.example.com` are public hosts.
|
|
1014
|
+
const host = (target.match(/^(\[[^\]]*\]|[^/:?#]*)/)?.[1] ?? "").toLowerCase();
|
|
1015
|
+
const loopback = host === "localhost" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host);
|
|
1016
|
+
return loopback ? `http://${target}` : `https://${target}`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
988
1019
|
// Pull a global `--profile <val>` / `--profile=<val>` out of the leading flags of an argv.
|
|
989
1020
|
// Only flags before the first positional (the playwright subcommand) are rech globals — a
|
|
990
1021
|
// --profile at/after the subcommand belongs to the forwarded CLI (e.g. playwright-cli's own
|
|
@@ -1115,7 +1146,9 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
1115
1146
|
}
|
|
1116
1147
|
if (stderr) {
|
|
1117
1148
|
if (stderr.includes('Extension connection timeout')) {
|
|
1118
|
-
|
|
1149
|
+
// The daemon resolves registered profiles' bridge tokens itself and then asks for a reload;
|
|
1150
|
+
// only blame a missing install when neither side had credentials.
|
|
1151
|
+
const hasToken = !!effectiveEnv["PLAYWRIGHT_MCP_EXTENSION_TOKEN"] || /reload the .*extension/i.test(stderr);
|
|
1119
1152
|
const last = hasToken
|
|
1120
1153
|
? ` -x: extension did not connect (reload it at chrome://extensions; then verify its token) -> extension[degraded]`
|
|
1121
1154
|
: ` -> extension[not installed] (run: rech setup)`;
|
|
@@ -1404,6 +1437,8 @@ function daemonManager(): DaemonManager {
|
|
|
1404
1437
|
isWindows: IS_WINDOWS,
|
|
1405
1438
|
override: process.env.RECH_DAEMON_MANAGER,
|
|
1406
1439
|
});
|
|
1440
|
+
if (isDeprecatedPm2Fallback(_daemonMgr, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER }))
|
|
1441
|
+
console.error(`[rech] warning: using pm2 because oxmgr is not on PATH. ${PM2_DEPRECATION}`);
|
|
1407
1442
|
return _daemonMgr;
|
|
1408
1443
|
}
|
|
1409
1444
|
|
|
@@ -1440,29 +1475,47 @@ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
|
|
|
1440
1475
|
}
|
|
1441
1476
|
|
|
1442
1477
|
// Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
|
|
1443
|
-
//
|
|
1478
|
+
// Match a name in it with listsProcess, not a substring test.
|
|
1444
1479
|
async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
|
|
1445
1480
|
const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
1446
1481
|
return await new Response(proc.stdout).text();
|
|
1447
1482
|
}
|
|
1448
1483
|
|
|
1484
|
+
// A candidate playwright-cli entry is usable only if the playwright-core it requires actually
|
|
1485
|
+
// resolves FROM that entry — the wrapper does `require('playwright-core/lib/tools/cli-client/program')`,
|
|
1486
|
+
// a deep subpath reachable only through the fork's patched `exports` map. existsSync on the .js is
|
|
1487
|
+
// not the same check: an uninitialised/half-built lib/playwright-cli submodule leaves the wrapper on
|
|
1488
|
+
// disk with no resolvable core, and the failure surfaces later as MODULE_NOT_FOUND inside the daemon.
|
|
1489
|
+
export function playwrightCliIsUsable(jsEntry: string): boolean {
|
|
1490
|
+
try {
|
|
1491
|
+
createRequire(jsEntry).resolve("playwright-core/lib/tools/cli-client/program");
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1449
1498
|
// Resolve which playwright-cli the daemon runs to drive Chrome. Priority:
|
|
1450
1499
|
// 1. PLAYWRIGHT_CLI env override — explicit, already a full command string.
|
|
1451
1500
|
// 2. Vendored fork in a git checkout (lib/playwright-cli/playwright-cli.js) — the patched
|
|
1452
1501
|
// multi-tab CLI + patched playwright-core (PLAYWRIGHT_MCP_PROFILE_DIRECTORY etc.).
|
|
1453
1502
|
// 3. The fork bundled into the npm tarball (vendor/playwright-cli/playwright-cli.js, produced by
|
|
1454
|
-
// scripts/vendor-cli.sh at prepublish
|
|
1455
|
-
// `bun i -g rechrome`: self-contained, no
|
|
1503
|
+
// scripts/vendor-cli.sh at prepublish, and by `prepare` on `bun install` in a checkout). This
|
|
1504
|
+
// is the batteries-included default for `bun i -g rechrome`: self-contained, no
|
|
1505
|
+
// @playwright/cli dep, no browser-binary download.
|
|
1456
1506
|
// 4. Bare `playwright-cli-multi-tab` on PATH — legacy fallback for a pre-existing global link.
|
|
1507
|
+
// Candidates 2–3 must also pass playwrightCliIsUsable(), so a present-but-broken one falls through.
|
|
1508
|
+
// lib/ stays ahead of vendor/ on purpose: a dev who built the fork wants their patched core, not
|
|
1509
|
+
// the (possibly older) vendor-src snapshot that `prepare` unpacks into vendor/.
|
|
1457
1510
|
// A resolved .js entry is run through `node` on Windows (which can't exec a .js by shebang) and
|
|
1458
1511
|
// bare on POSIX (its `#!/usr/bin/env node` shebang runs it under node, which the relay handshake
|
|
1459
1512
|
// needs — see daemonInstall). serve splits the result on spaces into argv.
|
|
1460
|
-
export function resolvePlaywrightCli(): string {
|
|
1513
|
+
export function resolvePlaywrightCli(root: string = import.meta.dir): string {
|
|
1461
1514
|
if (process.env.PLAYWRIGHT_CLI) return process.env.PLAYWRIGHT_CLI;
|
|
1462
1515
|
const jsEntry = [
|
|
1463
|
-
join(
|
|
1464
|
-
join(
|
|
1465
|
-
].
|
|
1516
|
+
join(root, "lib/playwright-cli/playwright-cli.js"),
|
|
1517
|
+
join(root, "vendor/playwright-cli/playwright-cli.js"),
|
|
1518
|
+
].filter(existsSync).find(playwrightCliIsUsable);
|
|
1466
1519
|
if (jsEntry) return IS_WINDOWS ? `node ${jsEntry}` : jsEntry;
|
|
1467
1520
|
return "playwright-cli-multi-tab";
|
|
1468
1521
|
}
|
|
@@ -1470,18 +1523,22 @@ export function resolvePlaywrightCli(): string {
|
|
|
1470
1523
|
/**
|
|
1471
1524
|
* Make sure a daemon process manager is available, offering to install oxmgr
|
|
1472
1525
|
* (default No; --yes approves). An explicit RECH_DAEMON_MANAGER=pm2 is respected,
|
|
1473
|
-
* since installing oxmgr would not satisfy it.
|
|
1526
|
+
* since installing oxmgr would not satisfy it. When only the deprecated pm2 fallback
|
|
1527
|
+
* is available (POSIX without oxmgr), the offer is made too, but declining keeps pm2.
|
|
1474
1528
|
*/
|
|
1475
1529
|
async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<string>, yes = false): Promise<void> {
|
|
1530
|
+
let fallback: DaemonManager | undefined;
|
|
1476
1531
|
try {
|
|
1477
|
-
daemonManager();
|
|
1478
|
-
return;
|
|
1532
|
+
fallback = daemonManager();
|
|
1533
|
+
if (!isDeprecatedPm2Fallback(fallback, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER })) return;
|
|
1479
1534
|
} catch (error) {
|
|
1480
1535
|
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1481
1536
|
}
|
|
1482
1537
|
const command = oxmgrInstallCommand(process.env);
|
|
1483
|
-
const
|
|
1538
|
+
const reason = fallback ? "Only the deprecated pm2 is available" : "oxmgr is missing";
|
|
1539
|
+
const answer = yes ? "yes" : (await ask(` ${reason}. Install oxmgr globally with \`${command.join(" ")}\`? [y/N]: `)).trim();
|
|
1484
1540
|
if (!/^(y|yes)$/i.test(answer)) {
|
|
1541
|
+
if (fallback) return; // keep the working pm2 setup
|
|
1485
1542
|
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1486
1543
|
}
|
|
1487
1544
|
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
@@ -1536,6 +1593,22 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1536
1593
|
|
|
1537
1594
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
1538
1595
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1596
|
+
// Migrating from pm2 to oxmgr: a serve still registered in pm2 would hold the port and be
|
|
1597
|
+
// resurrected at login, fighting the oxmgr-managed one. Remove it from pm2 too.
|
|
1598
|
+
const pm2Bin = mgr.id === "oxmgr" ? Bun.which("pm2") : null;
|
|
1599
|
+
if (pm2Bin) {
|
|
1600
|
+
const pm2: DaemonManager = { id: "pm2", bin: pm2Bin };
|
|
1601
|
+
const listed = await pmList(pm2).catch(() => "");
|
|
1602
|
+
const stale = [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES].filter(name => listsProcess("pm2", listed, name));
|
|
1603
|
+
if (stale.length) {
|
|
1604
|
+
console.log(` Migrating from pm2: removing ${stale.join(", ")}`);
|
|
1605
|
+
for (const name of stale) await runPm(pm2, ["delete", name]);
|
|
1606
|
+
// --force: pm2 won't save an empty list otherwise, keeping the old dump that `pm2 resurrect`
|
|
1607
|
+
// would bring back at login to fight the oxmgr-managed serve over the port.
|
|
1608
|
+
if (await runPm(pm2, ["save", "--force"]) !== 0)
|
|
1609
|
+
console.warn(" pm2 save failed; run `pm2 save --force` so pm2 doesn't resurrect the old serve at login.");
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1539
1612
|
|
|
1540
1613
|
let startCode: number;
|
|
1541
1614
|
if (mgr.id === "pm2") {
|
|
@@ -1574,7 +1647,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1574
1647
|
async function daemonUninstall(): Promise<void> {
|
|
1575
1648
|
const mgr = daemonManager();
|
|
1576
1649
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1577
|
-
if (mgr.id === "pm2") await runPm(mgr, ["save"]);
|
|
1650
|
+
if (mgr.id === "pm2") await runPm(mgr, ["save", "--force"]); // an emptied list must still overwrite the dump
|
|
1578
1651
|
else await runPm(mgr, ["service", "uninstall"]);
|
|
1579
1652
|
console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
|
|
1580
1653
|
}
|
|
@@ -2478,13 +2551,16 @@ async function status(): Promise<void> {
|
|
|
2478
2551
|
// ReferenceError that took down the whole of `rech status`, so the one command
|
|
2479
2552
|
// that reports "the relay is wedged" died exactly when the relay was wedged,
|
|
2480
2553
|
// printing a stack trace instead of the restart hint.
|
|
2481
|
-
if (pingBody?.degraded)
|
|
2482
|
-
console.log(`relay: ⚠ degraded (${pingBody.consecutiveTimeouts} consecutive command timeouts) — if it persists, the daemon self-restarts; force it now with \`${daemonManager().id} restart ${PM_PROCESS_NAME}\``);
|
|
2483
2554
|
// The daemon line is about this machine; a client of a remote host has no local daemon to report.
|
|
2484
2555
|
const isHost = !!(await readListeners().catch(() => null));
|
|
2556
|
+
// No oxmgr/pm2 on PATH must not take down `rech status`: report it instead of throwing.
|
|
2557
|
+
let mgr: DaemonManager | undefined;
|
|
2558
|
+
if (isHost) try { mgr = daemonManager(); } catch { /* reported below */ }
|
|
2559
|
+
if (pingBody?.degraded)
|
|
2560
|
+
console.log(`relay: ⚠ degraded (${pingBody.consecutiveTimeouts} consecutive command timeouts) — if it persists, the daemon self-restarts; ${isHost ? `force it now with \`${mgr?.id ?? "oxmgr"} restart ${PM_PROCESS_NAME}\`` : "the daemon host can restart it"}`);
|
|
2485
2561
|
if (isHost) {
|
|
2486
|
-
const daemonRegistered = (await pmList()
|
|
2487
|
-
console.log(`daemon: ${daemonRegistered ? `${
|
|
2562
|
+
const daemonRegistered = mgr ? listsProcess(mgr.id, await pmList(mgr).catch(() => ""), PM_PROCESS_NAME) : false;
|
|
2563
|
+
console.log(`daemon: ${daemonRegistered ? `${mgr!.id} (${PM_PROCESS_NAME})` : mgr ? "not installed" : "not installed (no oxmgr or pm2 on PATH)"}`);
|
|
2488
2564
|
}
|
|
2489
2565
|
// Same resolution as a command: ?profile= in the URL, else PLAYWRIGHT_MCP_PROFILE_DIRECTORY.
|
|
2490
2566
|
const effective = resolveEffectiveProfile(parsed.profileDirectory);
|
|
@@ -2586,7 +2662,8 @@ export function rechCli(argv: string[], handlers: RechHandlers) {
|
|
|
2586
2662
|
? handlers.urlList()
|
|
2587
2663
|
: handlers.printProfileUri(a.profile, a.listener, { local: a.local, save: a.save }))
|
|
2588
2664
|
.command("connect <url>", "Use a URL from another machine in this project (checks it first)", y => y
|
|
2589
|
-
.positional("url", { type: "string", demandOption: true })
|
|
2665
|
+
.positional("url", { type: "string", demandOption: true, describe: "The URL printed by `rech url <profile>` on the machine with Chrome. Quote it: it contains #" })
|
|
2666
|
+
.example("rech connect 'https://host.example.js.net/rechrome/?profile=you%40example.com#key=…'", ""),
|
|
2590
2667
|
a => handlers.connect(a.url))
|
|
2591
2668
|
.command(["listener", "listeners"], "Control who can connect: listeners, allowed profiles, keys, public URLs", y => y
|
|
2592
2669
|
.command(["ls", "list", "$0"], "List listeners (keys hidden)", {}, () => handlers.listListeners())
|
|
@@ -2630,8 +2707,16 @@ export function rechCli(argv: string[], handlers: RechHandlers) {
|
|
|
2630
2707
|
.demandCommand(1)
|
|
2631
2708
|
.strict()
|
|
2632
2709
|
.help()
|
|
2710
|
+
.alias("help", "h")
|
|
2633
2711
|
.version(false)
|
|
2634
|
-
|
|
2712
|
+
// A parse error (missing argument, unknown option…) shows that command's help above the
|
|
2713
|
+
// error, so the fix is visible. Errors thrown by a command's own handler pass through.
|
|
2714
|
+
.fail((message, error, y) => {
|
|
2715
|
+
if (error) throw error;
|
|
2716
|
+
// Straight to stderr: Bun's console.error would paint the whole help red.
|
|
2717
|
+
y.showHelp((help: string) => process.stderr.write(`${help}\n\n`));
|
|
2718
|
+
throw new Error(`rech: ${/^Not enough non-option arguments/.test(message) ? "missing a required argument; see the usage line above" : message}`);
|
|
2719
|
+
});
|
|
2635
2720
|
}
|
|
2636
2721
|
|
|
2637
2722
|
if (import.meta.main) {
|
package/rechrome.ts
CHANGED
|
@@ -10,9 +10,10 @@ import { hostname, homedir, networkInterfaces } from "os";
|
|
|
10
10
|
import { isIPv4 } from "net";
|
|
11
11
|
import { join, basename, dirname } from "path";
|
|
12
12
|
import { pathToFileURL } from "url";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
13
14
|
import { spawn as cpSpawn } from "child_process";
|
|
14
15
|
import { readFile, writeFile, rename, chmod, mkdir } from "node:fs/promises";
|
|
15
|
-
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.ts";
|
|
16
|
+
import { isDeprecatedPm2Fallback, listsProcess, oxmgrInstallCommand, pickDaemonManager, PM2_DEPRECATION, type DaemonManager } from "./daemon-manager.ts";
|
|
16
17
|
|
|
17
18
|
export const ENV_KEY = "RECHROME_URL";
|
|
18
19
|
export const DEFAULT_PORT = 13775;
|
|
@@ -976,15 +977,45 @@ async function callServe(
|
|
|
976
977
|
console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
|
|
977
978
|
process.exit(1);
|
|
978
979
|
}
|
|
979
|
-
|
|
980
|
+
const text = await res.text();
|
|
981
|
+
try {
|
|
982
|
+
return JSON.parse(text);
|
|
983
|
+
} catch {
|
|
984
|
+
// Not the daemon answering, e.g. a reverse proxy's 404 because the URL's path prefix is wrong.
|
|
985
|
+
const detail = `HTTP ${res.status} from ${serviceUrl(url, "run")} is not a rechrome daemon response: ${text.slice(0, 200).trim()}`;
|
|
986
|
+
if (throwOnFailure) throw new Error(detail);
|
|
987
|
+
console.error(`[rech] rech-client -> ${serviceUrl(url, "run")}\n -x: ${detail}`);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
980
990
|
}
|
|
981
991
|
|
|
992
|
+
const BOOLEAN_OPEN_FLAGS = new Set(["--headed", "--persistent", "--in-memory", "--extension"]);
|
|
993
|
+
|
|
982
994
|
export function normalizeCommandArgs(args: string[]): string[] {
|
|
983
995
|
const normalized = [...args];
|
|
984
996
|
if (normalized[0] === "tabs" || normalized[0] === "list") normalized[0] = "tab-list";
|
|
997
|
+
// `rech open hello.com`: profile-scoped listeners accept only HTTP(S)/about:blank targets, so
|
|
998
|
+
// give a bare host an https:// scheme the way a browser address bar would.
|
|
999
|
+
if (["open", "goto", "tab-new"].includes(normalized[0])) {
|
|
1000
|
+
// The target is the first positional. A token after a `--flag` without `=` is that flag's
|
|
1001
|
+
// value (`open --profile my-profile url`), unless the flag is a known boolean.
|
|
1002
|
+
const takesValue = (flag: string) => flag.startsWith("-") && !flag.includes("=") && !BOOLEAN_OPEN_FLAGS.has(flag);
|
|
1003
|
+
const i = normalized.findIndex((a, idx) => idx > 0 && !a.startsWith("-") && !takesValue(normalized[idx - 1]!));
|
|
1004
|
+
if (i > 0) normalized[i] = withDefaultScheme(normalized[i]!);
|
|
1005
|
+
}
|
|
985
1006
|
return normalized;
|
|
986
1007
|
}
|
|
987
1008
|
|
|
1009
|
+
/** `hello.com` -> `https://hello.com`, `localhost:3000` -> `http://localhost:3000`; URLs with a scheme and paths are unchanged. */
|
|
1010
|
+
export function withDefaultScheme(target: string): string {
|
|
1011
|
+
if (/^[./\\~]/.test(target) || /^[a-z]:[\\/]/i.test(target)) return target; // a file path, not a host
|
|
1012
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) && !/^[^/:]+:\d+(\/|$|[?#])/.test(target)) return target;
|
|
1013
|
+
// Loopback by exact hostname: `localhost.example.com` / `127.example.com` are public hosts.
|
|
1014
|
+
const host = (target.match(/^(\[[^\]]*\]|[^/:?#]*)/)?.[1] ?? "").toLowerCase();
|
|
1015
|
+
const loopback = host === "localhost" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host);
|
|
1016
|
+
return loopback ? `http://${target}` : `https://${target}`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
988
1019
|
// Pull a global `--profile <val>` / `--profile=<val>` out of the leading flags of an argv.
|
|
989
1020
|
// Only flags before the first positional (the playwright subcommand) are rech globals — a
|
|
990
1021
|
// --profile at/after the subcommand belongs to the forwarded CLI (e.g. playwright-cli's own
|
|
@@ -1115,7 +1146,9 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
1115
1146
|
}
|
|
1116
1147
|
if (stderr) {
|
|
1117
1148
|
if (stderr.includes('Extension connection timeout')) {
|
|
1118
|
-
|
|
1149
|
+
// The daemon resolves registered profiles' bridge tokens itself and then asks for a reload;
|
|
1150
|
+
// only blame a missing install when neither side had credentials.
|
|
1151
|
+
const hasToken = !!effectiveEnv["PLAYWRIGHT_MCP_EXTENSION_TOKEN"] || /reload the .*extension/i.test(stderr);
|
|
1119
1152
|
const last = hasToken
|
|
1120
1153
|
? ` -x: extension did not connect (reload it at chrome://extensions; then verify its token) -> extension[degraded]`
|
|
1121
1154
|
: ` -> extension[not installed] (run: rech setup)`;
|
|
@@ -1404,6 +1437,8 @@ function daemonManager(): DaemonManager {
|
|
|
1404
1437
|
isWindows: IS_WINDOWS,
|
|
1405
1438
|
override: process.env.RECH_DAEMON_MANAGER,
|
|
1406
1439
|
});
|
|
1440
|
+
if (isDeprecatedPm2Fallback(_daemonMgr, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER }))
|
|
1441
|
+
console.error(`[rech] warning: using pm2 because oxmgr is not on PATH. ${PM2_DEPRECATION}`);
|
|
1407
1442
|
return _daemonMgr;
|
|
1408
1443
|
}
|
|
1409
1444
|
|
|
@@ -1440,29 +1475,47 @@ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
|
|
|
1440
1475
|
}
|
|
1441
1476
|
|
|
1442
1477
|
// Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
|
|
1443
|
-
//
|
|
1478
|
+
// Match a name in it with listsProcess, not a substring test.
|
|
1444
1479
|
async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
|
|
1445
1480
|
const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
1446
1481
|
return await new Response(proc.stdout).text();
|
|
1447
1482
|
}
|
|
1448
1483
|
|
|
1484
|
+
// A candidate playwright-cli entry is usable only if the playwright-core it requires actually
|
|
1485
|
+
// resolves FROM that entry — the wrapper does `require('playwright-core/lib/tools/cli-client/program')`,
|
|
1486
|
+
// a deep subpath reachable only through the fork's patched `exports` map. existsSync on the .js is
|
|
1487
|
+
// not the same check: an uninitialised/half-built lib/playwright-cli submodule leaves the wrapper on
|
|
1488
|
+
// disk with no resolvable core, and the failure surfaces later as MODULE_NOT_FOUND inside the daemon.
|
|
1489
|
+
export function playwrightCliIsUsable(jsEntry: string): boolean {
|
|
1490
|
+
try {
|
|
1491
|
+
createRequire(jsEntry).resolve("playwright-core/lib/tools/cli-client/program");
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1449
1498
|
// Resolve which playwright-cli the daemon runs to drive Chrome. Priority:
|
|
1450
1499
|
// 1. PLAYWRIGHT_CLI env override — explicit, already a full command string.
|
|
1451
1500
|
// 2. Vendored fork in a git checkout (lib/playwright-cli/playwright-cli.js) — the patched
|
|
1452
1501
|
// multi-tab CLI + patched playwright-core (PLAYWRIGHT_MCP_PROFILE_DIRECTORY etc.).
|
|
1453
1502
|
// 3. The fork bundled into the npm tarball (vendor/playwright-cli/playwright-cli.js, produced by
|
|
1454
|
-
// scripts/vendor-cli.sh at prepublish
|
|
1455
|
-
// `bun i -g rechrome`: self-contained, no
|
|
1503
|
+
// scripts/vendor-cli.sh at prepublish, and by `prepare` on `bun install` in a checkout). This
|
|
1504
|
+
// is the batteries-included default for `bun i -g rechrome`: self-contained, no
|
|
1505
|
+
// @playwright/cli dep, no browser-binary download.
|
|
1456
1506
|
// 4. Bare `playwright-cli-multi-tab` on PATH — legacy fallback for a pre-existing global link.
|
|
1507
|
+
// Candidates 2–3 must also pass playwrightCliIsUsable(), so a present-but-broken one falls through.
|
|
1508
|
+
// lib/ stays ahead of vendor/ on purpose: a dev who built the fork wants their patched core, not
|
|
1509
|
+
// the (possibly older) vendor-src snapshot that `prepare` unpacks into vendor/.
|
|
1457
1510
|
// A resolved .js entry is run through `node` on Windows (which can't exec a .js by shebang) and
|
|
1458
1511
|
// bare on POSIX (its `#!/usr/bin/env node` shebang runs it under node, which the relay handshake
|
|
1459
1512
|
// needs — see daemonInstall). serve splits the result on spaces into argv.
|
|
1460
|
-
export function resolvePlaywrightCli(): string {
|
|
1513
|
+
export function resolvePlaywrightCli(root: string = import.meta.dir): string {
|
|
1461
1514
|
if (process.env.PLAYWRIGHT_CLI) return process.env.PLAYWRIGHT_CLI;
|
|
1462
1515
|
const jsEntry = [
|
|
1463
|
-
join(
|
|
1464
|
-
join(
|
|
1465
|
-
].
|
|
1516
|
+
join(root, "lib/playwright-cli/playwright-cli.js"),
|
|
1517
|
+
join(root, "vendor/playwright-cli/playwright-cli.js"),
|
|
1518
|
+
].filter(existsSync).find(playwrightCliIsUsable);
|
|
1466
1519
|
if (jsEntry) return IS_WINDOWS ? `node ${jsEntry}` : jsEntry;
|
|
1467
1520
|
return "playwright-cli-multi-tab";
|
|
1468
1521
|
}
|
|
@@ -1470,18 +1523,22 @@ export function resolvePlaywrightCli(): string {
|
|
|
1470
1523
|
/**
|
|
1471
1524
|
* Make sure a daemon process manager is available, offering to install oxmgr
|
|
1472
1525
|
* (default No; --yes approves). An explicit RECH_DAEMON_MANAGER=pm2 is respected,
|
|
1473
|
-
* since installing oxmgr would not satisfy it.
|
|
1526
|
+
* since installing oxmgr would not satisfy it. When only the deprecated pm2 fallback
|
|
1527
|
+
* is available (POSIX without oxmgr), the offer is made too, but declining keeps pm2.
|
|
1474
1528
|
*/
|
|
1475
1529
|
async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<string>, yes = false): Promise<void> {
|
|
1530
|
+
let fallback: DaemonManager | undefined;
|
|
1476
1531
|
try {
|
|
1477
|
-
daemonManager();
|
|
1478
|
-
return;
|
|
1532
|
+
fallback = daemonManager();
|
|
1533
|
+
if (!isDeprecatedPm2Fallback(fallback, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER })) return;
|
|
1479
1534
|
} catch (error) {
|
|
1480
1535
|
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1481
1536
|
}
|
|
1482
1537
|
const command = oxmgrInstallCommand(process.env);
|
|
1483
|
-
const
|
|
1538
|
+
const reason = fallback ? "Only the deprecated pm2 is available" : "oxmgr is missing";
|
|
1539
|
+
const answer = yes ? "yes" : (await ask(` ${reason}. Install oxmgr globally with \`${command.join(" ")}\`? [y/N]: `)).trim();
|
|
1484
1540
|
if (!/^(y|yes)$/i.test(answer)) {
|
|
1541
|
+
if (fallback) return; // keep the working pm2 setup
|
|
1485
1542
|
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1486
1543
|
}
|
|
1487
1544
|
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
@@ -1536,6 +1593,22 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1536
1593
|
|
|
1537
1594
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
1538
1595
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1596
|
+
// Migrating from pm2 to oxmgr: a serve still registered in pm2 would hold the port and be
|
|
1597
|
+
// resurrected at login, fighting the oxmgr-managed one. Remove it from pm2 too.
|
|
1598
|
+
const pm2Bin = mgr.id === "oxmgr" ? Bun.which("pm2") : null;
|
|
1599
|
+
if (pm2Bin) {
|
|
1600
|
+
const pm2: DaemonManager = { id: "pm2", bin: pm2Bin };
|
|
1601
|
+
const listed = await pmList(pm2).catch(() => "");
|
|
1602
|
+
const stale = [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES].filter(name => listsProcess("pm2", listed, name));
|
|
1603
|
+
if (stale.length) {
|
|
1604
|
+
console.log(` Migrating from pm2: removing ${stale.join(", ")}`);
|
|
1605
|
+
for (const name of stale) await runPm(pm2, ["delete", name]);
|
|
1606
|
+
// --force: pm2 won't save an empty list otherwise, keeping the old dump that `pm2 resurrect`
|
|
1607
|
+
// would bring back at login to fight the oxmgr-managed serve over the port.
|
|
1608
|
+
if (await runPm(pm2, ["save", "--force"]) !== 0)
|
|
1609
|
+
console.warn(" pm2 save failed; run `pm2 save --force` so pm2 doesn't resurrect the old serve at login.");
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1539
1612
|
|
|
1540
1613
|
let startCode: number;
|
|
1541
1614
|
if (mgr.id === "pm2") {
|
|
@@ -1574,7 +1647,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1574
1647
|
async function daemonUninstall(): Promise<void> {
|
|
1575
1648
|
const mgr = daemonManager();
|
|
1576
1649
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1577
|
-
if (mgr.id === "pm2") await runPm(mgr, ["save"]);
|
|
1650
|
+
if (mgr.id === "pm2") await runPm(mgr, ["save", "--force"]); // an emptied list must still overwrite the dump
|
|
1578
1651
|
else await runPm(mgr, ["service", "uninstall"]);
|
|
1579
1652
|
console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
|
|
1580
1653
|
}
|
|
@@ -2478,13 +2551,16 @@ async function status(): Promise<void> {
|
|
|
2478
2551
|
// ReferenceError that took down the whole of `rech status`, so the one command
|
|
2479
2552
|
// that reports "the relay is wedged" died exactly when the relay was wedged,
|
|
2480
2553
|
// printing a stack trace instead of the restart hint.
|
|
2481
|
-
if (pingBody?.degraded)
|
|
2482
|
-
console.log(`relay: ⚠ degraded (${pingBody.consecutiveTimeouts} consecutive command timeouts) — if it persists, the daemon self-restarts; force it now with \`${daemonManager().id} restart ${PM_PROCESS_NAME}\``);
|
|
2483
2554
|
// The daemon line is about this machine; a client of a remote host has no local daemon to report.
|
|
2484
2555
|
const isHost = !!(await readListeners().catch(() => null));
|
|
2556
|
+
// No oxmgr/pm2 on PATH must not take down `rech status`: report it instead of throwing.
|
|
2557
|
+
let mgr: DaemonManager | undefined;
|
|
2558
|
+
if (isHost) try { mgr = daemonManager(); } catch { /* reported below */ }
|
|
2559
|
+
if (pingBody?.degraded)
|
|
2560
|
+
console.log(`relay: ⚠ degraded (${pingBody.consecutiveTimeouts} consecutive command timeouts) — if it persists, the daemon self-restarts; ${isHost ? `force it now with \`${mgr?.id ?? "oxmgr"} restart ${PM_PROCESS_NAME}\`` : "the daemon host can restart it"}`);
|
|
2485
2561
|
if (isHost) {
|
|
2486
|
-
const daemonRegistered = (await pmList()
|
|
2487
|
-
console.log(`daemon: ${daemonRegistered ? `${
|
|
2562
|
+
const daemonRegistered = mgr ? listsProcess(mgr.id, await pmList(mgr).catch(() => ""), PM_PROCESS_NAME) : false;
|
|
2563
|
+
console.log(`daemon: ${daemonRegistered ? `${mgr!.id} (${PM_PROCESS_NAME})` : mgr ? "not installed" : "not installed (no oxmgr or pm2 on PATH)"}`);
|
|
2488
2564
|
}
|
|
2489
2565
|
// Same resolution as a command: ?profile= in the URL, else PLAYWRIGHT_MCP_PROFILE_DIRECTORY.
|
|
2490
2566
|
const effective = resolveEffectiveProfile(parsed.profileDirectory);
|
|
@@ -2586,7 +2662,8 @@ export function rechCli(argv: string[], handlers: RechHandlers) {
|
|
|
2586
2662
|
? handlers.urlList()
|
|
2587
2663
|
: handlers.printProfileUri(a.profile, a.listener, { local: a.local, save: a.save }))
|
|
2588
2664
|
.command("connect <url>", "Use a URL from another machine in this project (checks it first)", y => y
|
|
2589
|
-
.positional("url", { type: "string", demandOption: true })
|
|
2665
|
+
.positional("url", { type: "string", demandOption: true, describe: "The URL printed by `rech url <profile>` on the machine with Chrome. Quote it: it contains #" })
|
|
2666
|
+
.example("rech connect 'https://host.example.ts.net/rechrome/?profile=you%40example.com#key=…'", ""),
|
|
2590
2667
|
a => handlers.connect(a.url))
|
|
2591
2668
|
.command(["listener", "listeners"], "Control who can connect: listeners, allowed profiles, keys, public URLs", y => y
|
|
2592
2669
|
.command(["ls", "list", "$0"], "List listeners (keys hidden)", {}, () => handlers.listListeners())
|
|
@@ -2630,8 +2707,16 @@ export function rechCli(argv: string[], handlers: RechHandlers) {
|
|
|
2630
2707
|
.demandCommand(1)
|
|
2631
2708
|
.strict()
|
|
2632
2709
|
.help()
|
|
2710
|
+
.alias("help", "h")
|
|
2633
2711
|
.version(false)
|
|
2634
|
-
|
|
2712
|
+
// A parse error (missing argument, unknown option…) shows that command's help above the
|
|
2713
|
+
// error, so the fix is visible. Errors thrown by a command's own handler pass through.
|
|
2714
|
+
.fail((message, error, y) => {
|
|
2715
|
+
if (error) throw error;
|
|
2716
|
+
// Straight to stderr: Bun's console.error would paint the whole help red.
|
|
2717
|
+
y.showHelp((help: string) => process.stderr.write(`${help}\n\n`));
|
|
2718
|
+
throw new Error(`rech: ${/^Not enough non-option arguments/.test(message) ? "missing a required argument; see the usage line above" : message}`);
|
|
2719
|
+
});
|
|
2635
2720
|
}
|
|
2636
2721
|
|
|
2637
2722
|
if (import.meta.main) {
|