rechrome 1.28.1 → 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/daemon-manager.js +21 -2
- package/daemon-manager.ts +33 -2
- package/package.json +1 -1
- package/rechrome.js +70 -13
- package/rechrome.ts +70 -13
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
package/rechrome.js
CHANGED
|
@@ -13,7 +13,7 @@ import { pathToFileURL } from "url";
|
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { spawn as cpSpawn } from "child_process";
|
|
15
15
|
import { readFile, writeFile, rename, chmod, mkdir } from "node:fs/promises";
|
|
16
|
-
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.js";
|
|
16
|
+
import { isDeprecatedPm2Fallback, listsProcess, oxmgrInstallCommand, pickDaemonManager, PM2_DEPRECATION, type DaemonManager } from "./daemon-manager.js";
|
|
17
17
|
|
|
18
18
|
export const ENV_KEY = "RECHROME_URL";
|
|
19
19
|
export const DEFAULT_PORT = 13775;
|
|
@@ -977,15 +977,45 @@ async function callServe(
|
|
|
977
977
|
console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
|
|
978
978
|
process.exit(1);
|
|
979
979
|
}
|
|
980
|
-
|
|
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
|
+
}
|
|
981
990
|
}
|
|
982
991
|
|
|
992
|
+
const BOOLEAN_OPEN_FLAGS = new Set(["--headed", "--persistent", "--in-memory", "--extension"]);
|
|
993
|
+
|
|
983
994
|
export function normalizeCommandArgs(args: string[]): string[] {
|
|
984
995
|
const normalized = [...args];
|
|
985
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
|
+
}
|
|
986
1006
|
return normalized;
|
|
987
1007
|
}
|
|
988
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
|
+
|
|
989
1019
|
// Pull a global `--profile <val>` / `--profile=<val>` out of the leading flags of an argv.
|
|
990
1020
|
// Only flags before the first positional (the playwright subcommand) are rech globals — a
|
|
991
1021
|
// --profile at/after the subcommand belongs to the forwarded CLI (e.g. playwright-cli's own
|
|
@@ -1116,7 +1146,9 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
1116
1146
|
}
|
|
1117
1147
|
if (stderr) {
|
|
1118
1148
|
if (stderr.includes('Extension connection timeout')) {
|
|
1119
|
-
|
|
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);
|
|
1120
1152
|
const last = hasToken
|
|
1121
1153
|
? ` -x: extension did not connect (reload it at chrome://extensions; then verify its token) -> extension[degraded]`
|
|
1122
1154
|
: ` -> extension[not installed] (run: rech setup)`;
|
|
@@ -1405,6 +1437,8 @@ function daemonManager(): DaemonManager {
|
|
|
1405
1437
|
isWindows: IS_WINDOWS,
|
|
1406
1438
|
override: process.env.RECH_DAEMON_MANAGER,
|
|
1407
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}`);
|
|
1408
1442
|
return _daemonMgr;
|
|
1409
1443
|
}
|
|
1410
1444
|
|
|
@@ -1441,7 +1475,7 @@ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
|
|
|
1441
1475
|
}
|
|
1442
1476
|
|
|
1443
1477
|
// Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
|
|
1444
|
-
//
|
|
1478
|
+
// Match a name in it with listsProcess, not a substring test.
|
|
1445
1479
|
async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
|
|
1446
1480
|
const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
1447
1481
|
return await new Response(proc.stdout).text();
|
|
@@ -1489,18 +1523,22 @@ export function resolvePlaywrightCli(root: string = import.meta.dir): string {
|
|
|
1489
1523
|
/**
|
|
1490
1524
|
* Make sure a daemon process manager is available, offering to install oxmgr
|
|
1491
1525
|
* (default No; --yes approves). An explicit RECH_DAEMON_MANAGER=pm2 is respected,
|
|
1492
|
-
* 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.
|
|
1493
1528
|
*/
|
|
1494
1529
|
async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<string>, yes = false): Promise<void> {
|
|
1530
|
+
let fallback: DaemonManager | undefined;
|
|
1495
1531
|
try {
|
|
1496
|
-
daemonManager();
|
|
1497
|
-
return;
|
|
1532
|
+
fallback = daemonManager();
|
|
1533
|
+
if (!isDeprecatedPm2Fallback(fallback, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER })) return;
|
|
1498
1534
|
} catch (error) {
|
|
1499
1535
|
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1500
1536
|
}
|
|
1501
1537
|
const command = oxmgrInstallCommand(process.env);
|
|
1502
|
-
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();
|
|
1503
1540
|
if (!/^(y|yes)$/i.test(answer)) {
|
|
1541
|
+
if (fallback) return; // keep the working pm2 setup
|
|
1504
1542
|
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1505
1543
|
}
|
|
1506
1544
|
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
@@ -1555,6 +1593,22 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1555
1593
|
|
|
1556
1594
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
1557
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
|
+
}
|
|
1558
1612
|
|
|
1559
1613
|
let startCode: number;
|
|
1560
1614
|
if (mgr.id === "pm2") {
|
|
@@ -1593,7 +1647,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1593
1647
|
async function daemonUninstall(): Promise<void> {
|
|
1594
1648
|
const mgr = daemonManager();
|
|
1595
1649
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1596
|
-
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
|
|
1597
1651
|
else await runPm(mgr, ["service", "uninstall"]);
|
|
1598
1652
|
console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
|
|
1599
1653
|
}
|
|
@@ -2497,13 +2551,16 @@ async function status(): Promise<void> {
|
|
|
2497
2551
|
// ReferenceError that took down the whole of `rech status`, so the one command
|
|
2498
2552
|
// that reports "the relay is wedged" died exactly when the relay was wedged,
|
|
2499
2553
|
// printing a stack trace instead of the restart hint.
|
|
2500
|
-
if (pingBody?.degraded)
|
|
2501
|
-
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}\``);
|
|
2502
2554
|
// The daemon line is about this machine; a client of a remote host has no local daemon to report.
|
|
2503
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"}`);
|
|
2504
2561
|
if (isHost) {
|
|
2505
|
-
const daemonRegistered = (await pmList()
|
|
2506
|
-
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)"}`);
|
|
2507
2564
|
}
|
|
2508
2565
|
// Same resolution as a command: ?profile= in the URL, else PLAYWRIGHT_MCP_PROFILE_DIRECTORY.
|
|
2509
2566
|
const effective = resolveEffectiveProfile(parsed.profileDirectory);
|
package/rechrome.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { pathToFileURL } from "url";
|
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { spawn as cpSpawn } from "child_process";
|
|
15
15
|
import { readFile, writeFile, rename, chmod, mkdir } from "node:fs/promises";
|
|
16
|
-
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.ts";
|
|
16
|
+
import { isDeprecatedPm2Fallback, listsProcess, oxmgrInstallCommand, pickDaemonManager, PM2_DEPRECATION, type DaemonManager } from "./daemon-manager.ts";
|
|
17
17
|
|
|
18
18
|
export const ENV_KEY = "RECHROME_URL";
|
|
19
19
|
export const DEFAULT_PORT = 13775;
|
|
@@ -977,15 +977,45 @@ async function callServe(
|
|
|
977
977
|
console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
|
|
978
978
|
process.exit(1);
|
|
979
979
|
}
|
|
980
|
-
|
|
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
|
+
}
|
|
981
990
|
}
|
|
982
991
|
|
|
992
|
+
const BOOLEAN_OPEN_FLAGS = new Set(["--headed", "--persistent", "--in-memory", "--extension"]);
|
|
993
|
+
|
|
983
994
|
export function normalizeCommandArgs(args: string[]): string[] {
|
|
984
995
|
const normalized = [...args];
|
|
985
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
|
+
}
|
|
986
1006
|
return normalized;
|
|
987
1007
|
}
|
|
988
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
|
+
|
|
989
1019
|
// Pull a global `--profile <val>` / `--profile=<val>` out of the leading flags of an argv.
|
|
990
1020
|
// Only flags before the first positional (the playwright subcommand) are rech globals — a
|
|
991
1021
|
// --profile at/after the subcommand belongs to the forwarded CLI (e.g. playwright-cli's own
|
|
@@ -1116,7 +1146,9 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
1116
1146
|
}
|
|
1117
1147
|
if (stderr) {
|
|
1118
1148
|
if (stderr.includes('Extension connection timeout')) {
|
|
1119
|
-
|
|
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);
|
|
1120
1152
|
const last = hasToken
|
|
1121
1153
|
? ` -x: extension did not connect (reload it at chrome://extensions; then verify its token) -> extension[degraded]`
|
|
1122
1154
|
: ` -> extension[not installed] (run: rech setup)`;
|
|
@@ -1405,6 +1437,8 @@ function daemonManager(): DaemonManager {
|
|
|
1405
1437
|
isWindows: IS_WINDOWS,
|
|
1406
1438
|
override: process.env.RECH_DAEMON_MANAGER,
|
|
1407
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}`);
|
|
1408
1442
|
return _daemonMgr;
|
|
1409
1443
|
}
|
|
1410
1444
|
|
|
@@ -1441,7 +1475,7 @@ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
|
|
|
1441
1475
|
}
|
|
1442
1476
|
|
|
1443
1477
|
// Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
|
|
1444
|
-
//
|
|
1478
|
+
// Match a name in it with listsProcess, not a substring test.
|
|
1445
1479
|
async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
|
|
1446
1480
|
const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
1447
1481
|
return await new Response(proc.stdout).text();
|
|
@@ -1489,18 +1523,22 @@ export function resolvePlaywrightCli(root: string = import.meta.dir): string {
|
|
|
1489
1523
|
/**
|
|
1490
1524
|
* Make sure a daemon process manager is available, offering to install oxmgr
|
|
1491
1525
|
* (default No; --yes approves). An explicit RECH_DAEMON_MANAGER=pm2 is respected,
|
|
1492
|
-
* 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.
|
|
1493
1528
|
*/
|
|
1494
1529
|
async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<string>, yes = false): Promise<void> {
|
|
1530
|
+
let fallback: DaemonManager | undefined;
|
|
1495
1531
|
try {
|
|
1496
|
-
daemonManager();
|
|
1497
|
-
return;
|
|
1532
|
+
fallback = daemonManager();
|
|
1533
|
+
if (!isDeprecatedPm2Fallback(fallback, { isWindows: IS_WINDOWS, override: process.env.RECH_DAEMON_MANAGER })) return;
|
|
1498
1534
|
} catch (error) {
|
|
1499
1535
|
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1500
1536
|
}
|
|
1501
1537
|
const command = oxmgrInstallCommand(process.env);
|
|
1502
|
-
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();
|
|
1503
1540
|
if (!/^(y|yes)$/i.test(answer)) {
|
|
1541
|
+
if (fallback) return; // keep the working pm2 setup
|
|
1504
1542
|
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1505
1543
|
}
|
|
1506
1544
|
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
@@ -1555,6 +1593,22 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1555
1593
|
|
|
1556
1594
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
1557
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
|
+
}
|
|
1558
1612
|
|
|
1559
1613
|
let startCode: number;
|
|
1560
1614
|
if (mgr.id === "pm2") {
|
|
@@ -1593,7 +1647,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1593
1647
|
async function daemonUninstall(): Promise<void> {
|
|
1594
1648
|
const mgr = daemonManager();
|
|
1595
1649
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
1596
|
-
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
|
|
1597
1651
|
else await runPm(mgr, ["service", "uninstall"]);
|
|
1598
1652
|
console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
|
|
1599
1653
|
}
|
|
@@ -2497,13 +2551,16 @@ async function status(): Promise<void> {
|
|
|
2497
2551
|
// ReferenceError that took down the whole of `rech status`, so the one command
|
|
2498
2552
|
// that reports "the relay is wedged" died exactly when the relay was wedged,
|
|
2499
2553
|
// printing a stack trace instead of the restart hint.
|
|
2500
|
-
if (pingBody?.degraded)
|
|
2501
|
-
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}\``);
|
|
2502
2554
|
// The daemon line is about this machine; a client of a remote host has no local daemon to report.
|
|
2503
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"}`);
|
|
2504
2561
|
if (isHost) {
|
|
2505
|
-
const daemonRegistered = (await pmList()
|
|
2506
|
-
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)"}`);
|
|
2507
2564
|
}
|
|
2508
2565
|
// Same resolution as a command: ?profile= in the URL, else PLAYWRIGHT_MCP_PROFILE_DIRECTORY.
|
|
2509
2566
|
const effective = resolveEffectiveProfile(parsed.profileDirectory);
|