rechrome 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -103
- package/listeners.js +57 -0
- package/listeners.ts +54 -1
- package/package.json +1 -1
- package/rechrome.js +217 -24
- package/rechrome.ts +217 -24
- package/serve.js +3 -1
- package/serve.ts +3 -1
package/rechrome.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { readListeners, writeListeners, listenerAddress, isLoopback, normalizePrefix, serviceUrl, type Listener } from "./listeners.js";
|
|
2
|
+
import { readListeners, writeListeners, listenerAddress, isLoopback, normalizePrefix, serviceUrl, allowProfiles, denyProfiles, rotateListenerKey, setPublicUrl, type Listener } from "./listeners.js";
|
|
3
3
|
|
|
4
4
|
import { file } from "bun";
|
|
5
5
|
import yargs from "yargs";
|
|
@@ -395,8 +395,8 @@ export async function detectTailscaleServe(port: number, prefix: string): Promis
|
|
|
395
395
|
return { dnsName, routeUrl: findTailscaleServeRoute(dnsName, serveStatus, port, prefix) };
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
/**
|
|
399
|
-
export function
|
|
398
|
+
/** The same connection (key and profile) at another base URL, e.g. where a proxy exposes the listener. */
|
|
399
|
+
export function rebaseConnectionUrl(routeUrl: string, localUrl: string): string {
|
|
400
400
|
const local = new URL(localUrl);
|
|
401
401
|
const remote = new URL(routeUrl);
|
|
402
402
|
remote.username = parseUrl(localUrl).key;
|
|
@@ -897,16 +897,22 @@ export function profileConnectionUri(profile: string, configuredUrl: string | un
|
|
|
897
897
|
return result.toString();
|
|
898
898
|
}
|
|
899
899
|
|
|
900
|
-
async function printProfileUri(selector?: string, listener?: string): Promise<void> {
|
|
900
|
+
async function printProfileUri(selector?: string, listener?: string, opts: { local?: boolean; save?: boolean } = {}): Promise<void> {
|
|
901
901
|
const url = process.env[ENV_KEY];
|
|
902
902
|
selector ??= url ? parseUrl(url).profileDirectory : undefined;
|
|
903
|
-
if (!selector) throw new Error("Specify a profile: rech
|
|
903
|
+
if (!selector) throw new Error("Specify a profile: rech url <profile>");
|
|
904
904
|
const registry = await readTokenRegistry();
|
|
905
905
|
const cache = await readChromeProfileCache();
|
|
906
906
|
// A configured remote profile may not exist in this machine's local registry.
|
|
907
907
|
const profile = url && parseUrl(url).profileDirectory === selector && !listener
|
|
908
908
|
? selector : (await resolveGlobalProfile(registry, cache, selector)).email;
|
|
909
|
-
|
|
909
|
+
const listeners = (await readListeners())?.listeners ?? [];
|
|
910
|
+
let uri = profileConnectionUri(profile, url, listeners, listener);
|
|
911
|
+
// Prefer where a proxy exposes the listener, when it has been recorded.
|
|
912
|
+
const publicUrl = listeners.find(l => l.key === parseUrl(uri).key)?.publicUrl;
|
|
913
|
+
if (publicUrl && !opts.local) uri = rebaseConnectionUrl(publicUrl, uri);
|
|
914
|
+
console.log(uri);
|
|
915
|
+
if (opts.save) console.error(`Saved RECHROME_URL to ${await saveProjectUrl(uri)}`);
|
|
910
916
|
}
|
|
911
917
|
|
|
912
918
|
export function sandboxConnectionWarning(env: Record<string, string | undefined> = process.env): string | null {
|
|
@@ -984,6 +990,31 @@ export function normalizeCommandArgs(args: string[]): string[] {
|
|
|
984
990
|
// --profile at/after the subcommand belongs to the forwarded CLI (e.g. playwright-cli's own
|
|
985
991
|
// `open --profile <dir>`, a user-data-dir path) and must pass through untouched. Throws on a
|
|
986
992
|
// missing value; accepts multiple occurrences (last one wins).
|
|
993
|
+
/**
|
|
994
|
+
* `rech [--profile X] [--isolate] pw <args>` forwards <args> to playwright-cli verbatim, so
|
|
995
|
+
* `rech pw --version` or `rech pw status` reach playwright instead of rech. `--` works the
|
|
996
|
+
* same after a rech flag (`rech --profile X -- status`); a bare leading `--` cannot, because
|
|
997
|
+
* Bun consumes the `--` right after the script path. The separator counts only when
|
|
998
|
+
* everything before it is a rech global flag; otherwise it belongs to the playwright command
|
|
999
|
+
* (`rech open -- x`). Returns its index, or -1.
|
|
1000
|
+
*/
|
|
1001
|
+
export function rechSeparatorIndex(args: string[]): number {
|
|
1002
|
+
for (let i = 0; i < args.length; i++) {
|
|
1003
|
+
const a = args[i];
|
|
1004
|
+
if (a === "--" || a === "pw" || a === "playwright") return i;
|
|
1005
|
+
if (a === "--profile") { i++; continue; }
|
|
1006
|
+
if (a.startsWith("--profile=") || a === "--isolate" || a === "--isolated") continue;
|
|
1007
|
+
return -1;
|
|
1008
|
+
}
|
|
1009
|
+
return -1;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** rechrome's own version, from the package.json shipped next to this file. */
|
|
1013
|
+
export function rechromeVersion(): string {
|
|
1014
|
+
try { return JSON.parse(readFileSync(join(import.meta.dir, "package.json"), "utf8")).version ?? "unknown"; }
|
|
1015
|
+
catch { return "unknown"; }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
987
1018
|
export function extractGlobalProfileArg(args: string[]): { args: string[]; selector?: string } {
|
|
988
1019
|
const rest = [...args];
|
|
989
1020
|
let selector: string | undefined;
|
|
@@ -1849,17 +1880,123 @@ async function removeListener(name: string): Promise<void> {
|
|
|
1849
1880
|
console.log("Listener removed from configuration; daemon reloads automatically.");
|
|
1850
1881
|
}
|
|
1851
1882
|
|
|
1852
|
-
async function
|
|
1853
|
-
const config = await requireListeners();
|
|
1883
|
+
async function resolveProfileKeys(selectors: string[]): Promise<string[]> {
|
|
1854
1884
|
const registry = await readTokenRegistry(), cache = await readChromeProfileCache();
|
|
1855
1885
|
const profiles: string[] = [];
|
|
1856
|
-
for (const selector of
|
|
1886
|
+
for (const selector of selectors) profiles.push((await resolveGlobalProfile(registry, cache, selector)).email);
|
|
1887
|
+
return [...new Set(profiles)];
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/** Next steps after exposing a listener: proxy it, record where, share. Plain text, so any shell works. */
|
|
1891
|
+
export function listenerNextSteps(listener: Listener, profile?: string): string[] {
|
|
1892
|
+
const prefix = normalizePrefix(listener.prefix);
|
|
1893
|
+
const mount = prefix === "/" ? "" : prefix.slice(0, -1);
|
|
1894
|
+
return [
|
|
1895
|
+
`Expose it through any reverse proxy on port ${listener.port}${mount ? ` (mount ${mount}; stripping it is fine)` : ""}, e.g.:`,
|
|
1896
|
+
` tailscale serve --bg${mount ? ` --set-path=${mount}` : ""} ${listener.port}`,
|
|
1897
|
+
`Then record where it is reachable and print the URL to share:`,
|
|
1898
|
+
` rech listener set ${listener.name} --public-url https://<your-host>${prefix}`,
|
|
1899
|
+
` rech url ${profile ? JSON.stringify(profile) : "<profile>"} --listener ${listener.name}`,
|
|
1900
|
+
];
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
async function addListener(name: string, opts: { listen: string; profile: string[]; port?: number; prefix?: string }): Promise<void> {
|
|
1904
|
+
const config = await requireListeners();
|
|
1905
|
+
const profiles = await resolveProfileKeys(opts.profile);
|
|
1857
1906
|
const host = chooseListenAddress(await detectListenChoices(), opts.listen);
|
|
1858
1907
|
const port = opts.port ?? DEFAULT_PORT;
|
|
1859
|
-
if (config.listeners.some(l => l.name === name)) throw new Error(
|
|
1860
|
-
|
|
1908
|
+
if (config.listeners.some(l => l.name === name)) throw new Error(`Listener "${name}" already exists; change its profiles with rech listener allow|deny ${name} <profile>`);
|
|
1909
|
+
const listener: Listener = { name, host, port, prefix: normalizePrefix(opts.prefix), key: randomBytes(24).toString("base64url"), profiles };
|
|
1910
|
+
config.listeners.push(listener);
|
|
1861
1911
|
await writeListeners(config);
|
|
1862
1912
|
console.log(`Listener saved for ${host}:${port}; daemon reloads automatically. Credentials are in ~/.rechrome/listeners.json (keep private).`);
|
|
1913
|
+
if (isLoopback(host)) for (const line of listenerNextSteps(listener, profiles[0])) console.log(line);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function findListener(config: { listeners: Listener[] }, name?: string): Listener {
|
|
1917
|
+
if (name) {
|
|
1918
|
+
const listener = config.listeners.find(l => l.name === name);
|
|
1919
|
+
if (!listener) throw new Error(`Unknown listener "${name}". See rech listener ls.`);
|
|
1920
|
+
return listener;
|
|
1921
|
+
}
|
|
1922
|
+
const scoped = config.listeners.filter(l => l.profiles !== "*");
|
|
1923
|
+
if (scoped.length !== 1) throw new Error(`Name a listener: ${config.listeners.map(l => l.name).join(", ")}`);
|
|
1924
|
+
return scoped[0];
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
async function listenerPort(name?: string): Promise<void> {
|
|
1928
|
+
console.log(findListener(await requireListeners(), name).port);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
async function allowListener(name: string, selectors: string[]): Promise<void> {
|
|
1932
|
+
const config = await requireListeners();
|
|
1933
|
+
const added = allowProfiles(config, name, await resolveProfileKeys(selectors));
|
|
1934
|
+
await writeListeners(config);
|
|
1935
|
+
console.log(added.length ? `Allowed on ${name}: ${added.join(", ")}` : `Already allowed on ${name}; nothing changed.`);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
async function denyListener(name: string, selectors: string[]): Promise<void> {
|
|
1939
|
+
const config = await requireListeners();
|
|
1940
|
+
// Accept raw registry keys too, so a profile that no longer resolves can still be removed.
|
|
1941
|
+
const keys = [...selectors, ...await resolveProfileKeys(selectors).catch(() => [] as string[])];
|
|
1942
|
+
const removed = denyProfiles(config, name, keys);
|
|
1943
|
+
await writeListeners(config);
|
|
1944
|
+
console.log(removed.length ? `Removed from ${name}: ${removed.join(", ")}` : `Not on ${name}; nothing changed.`);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
async function rotateKey(name: string): Promise<void> {
|
|
1948
|
+
const config = await requireListeners();
|
|
1949
|
+
rotateListenerKey(config, name);
|
|
1950
|
+
await writeListeners(config);
|
|
1951
|
+
console.log(`New key for ${name}; URLs carrying the old key stop working now. Print new ones with: rech url <profile> --listener ${name}`);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
async function setListener(name: string, opts: { publicUrl?: string; clearPublicUrl?: boolean }): Promise<void> {
|
|
1955
|
+
const config = await requireListeners();
|
|
1956
|
+
const listener = setPublicUrl(config, name, opts.clearPublicUrl ? null : opts.publicUrl ?? null);
|
|
1957
|
+
await writeListeners(config);
|
|
1958
|
+
console.log(listener.publicUrl ? `${name} is reachable at ${listener.publicUrl}` : `${name}: public URL cleared`);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const hideKey = (url: string) => url.replace(/([#&?]key=)[^&]*/, "$1…");
|
|
1962
|
+
|
|
1963
|
+
async function urlList(): Promise<void> {
|
|
1964
|
+
const config = await requireListeners();
|
|
1965
|
+
const rows = [["LISTENER", "PROFILE", "LOCAL", "PUBLIC"]];
|
|
1966
|
+
for (const l of config.listeners) {
|
|
1967
|
+
const local = `http://${listenerAddress(l)}${normalizePrefix(l.prefix)}`;
|
|
1968
|
+
for (const profile of l.profiles === "*" ? ["(all profiles)"] : l.profiles) rows.push([l.name, profile, local, l.publicUrl ?? "-"]);
|
|
1969
|
+
}
|
|
1970
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map(r => r[i].length)));
|
|
1971
|
+
for (const r of rows) console.log(r.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd());
|
|
1972
|
+
console.log(`\nPrint a full URL (contains the secret key): rech url <profile> --listener <name>`);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/** Write RECHROME_URL to this project's .rechrome/.env.local (the folder git-ignores itself). */
|
|
1976
|
+
async function saveProjectUrl(url: string): Promise<string> {
|
|
1977
|
+
const { dataDir } = await getClientIdentity();
|
|
1978
|
+
mkdirSync(dataDir, { recursive: true });
|
|
1979
|
+
const gitignore = join(dataDir, ".gitignore");
|
|
1980
|
+
if (!existsSync(gitignore)) await Bun.write(gitignore, "*\n");
|
|
1981
|
+
const envPath = join(dataDir, ".env.local");
|
|
1982
|
+
const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
|
|
1983
|
+
await writeFile(envPath, [...lines, `${ENV_KEY}=${url}`, ""].join("\n"), { mode: 0o600 });
|
|
1984
|
+
await chmod(envPath, 0o600);
|
|
1985
|
+
return envPath;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function connect(url: string): Promise<void> {
|
|
1989
|
+
const parsed = parseUrl(url);
|
|
1990
|
+
if (!parsed.key) throw new Error("That URL has no key (#key=…). Ask the host for the full URL from: rech url <profile>");
|
|
1991
|
+
const response = await fetch(serviceUrl(url, "ping"), { headers: { Authorization: `Bearer ${parsed.key}` }, signal: AbortSignal.timeout(5000) })
|
|
1992
|
+
.catch(error => { throw new Error(`Could not reach ${serviceUrl(url)}: ${error instanceof Error ? error.message : error}`); });
|
|
1993
|
+
if (response.status === 401) throw new Error("The daemon rejected this key; ask the host for a fresh URL (keys change on rech listener rotate-key).");
|
|
1994
|
+
if (!response.ok) throw new Error(`${serviceUrl(url, "ping")} answered ${response.status}; is the proxy pointing at the listener?`);
|
|
1995
|
+
const body = await response.json().catch(() => ({})) as { listener?: string; profiles?: string[] | "*" };
|
|
1996
|
+
if (parsed.profileDirectory && Array.isArray(body.profiles) && !body.profiles.includes(parsed.profileDirectory))
|
|
1997
|
+
throw new Error(`Connected, but listener "${body.listener}" does not allow profile "${parsed.profileDirectory}".`);
|
|
1998
|
+
const saved = await saveProjectUrl(url);
|
|
1999
|
+
console.log(`Connected to ${serviceUrl(url)}${body.listener ? ` (listener ${body.listener})` : ""}. Saved RECHROME_URL to ${saved}`);
|
|
1863
2000
|
}
|
|
1864
2001
|
|
|
1865
2002
|
export function detectSetupAgent(env: Record<string, string | undefined> = process.env): "Codex" | "Claude Code" | null {
|
|
@@ -2179,10 +2316,10 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
|
|
|
2179
2316
|
const serve = await detectTailscaleServe(exposure.port, prefix);
|
|
2180
2317
|
if (serve.routeUrl) {
|
|
2181
2318
|
console.log(` Tailscale Serve route found: ${serve.routeUrl} → ${protocol}://${listenerAddress(exposure)}${prefix.slice(0, -1)}`);
|
|
2182
|
-
console.log(` Remote RECHROME_URL (secret; for other tailnet machines):\n ${
|
|
2319
|
+
console.log(` Remote RECHROME_URL (secret; for other tailnet machines):\n ${rebaseConnectionUrl(serve.routeUrl, rechUrl.toString())}`);
|
|
2183
2320
|
} else {
|
|
2184
2321
|
console.log(` Tailscale Serve command (run separately): tailscale serve --bg --https=443 --set-path=${prefix.slice(0, -1)} ${protocol}://${listenerAddress(exposure)}${prefix.slice(0, -1)}`);
|
|
2185
|
-
if (serve.dnsName) console.log(` Then remote clients can use:\n ${
|
|
2322
|
+
if (serve.dnsName) console.log(` Then remote clients can use:\n ${rebaseConnectionUrl(`https://${serve.dnsName}${prefix}`, rechUrl.toString())}`);
|
|
2186
2323
|
else console.log(` For remote clients, use https://<your-machine-tailnet-name>${prefix} with this listener's bearer key and profile query.`);
|
|
2187
2324
|
}
|
|
2188
2325
|
console.log(` The URL saved below is the local loopback one.`);
|
|
@@ -2266,7 +2403,6 @@ async function status(): Promise<void> {
|
|
|
2266
2403
|
console.log(`serve: not configured (run \`rech setup\`)`);
|
|
2267
2404
|
return;
|
|
2268
2405
|
}
|
|
2269
|
-
const { host, port, protocol } = parseUrl(url);
|
|
2270
2406
|
const parsed = parseUrl(url);
|
|
2271
2407
|
const ping = await fetch(serviceUrl(url), { signal: AbortSignal.timeout(2000) }).catch(() => null);
|
|
2272
2408
|
// Resolve the daemon's actual bind from its authenticated /ping (cross-platform; lsof is
|
|
@@ -2275,11 +2411,11 @@ async function status(): Promise<void> {
|
|
|
2275
2411
|
? await fetch(serviceUrl(url, "ping"), {
|
|
2276
2412
|
headers: { Authorization: `Bearer ${parsed.key}` },
|
|
2277
2413
|
signal: AbortSignal.timeout(2000),
|
|
2278
|
-
}).then(r => (r.ok ? r.json() : null)).catch(() => null) as { bind?: string; degraded?: boolean; consecutiveTimeouts?: number } | null
|
|
2414
|
+
}).then(r => (r.ok ? r.json() : null)).catch(() => null) as { bind?: string; listener?: string; degraded?: boolean; consecutiveTimeouts?: number } | null
|
|
2279
2415
|
: null;
|
|
2280
|
-
|
|
2281
|
-
const
|
|
2282
|
-
console.log(`serve: ${ping ? `running ${
|
|
2416
|
+
// Show the URL this client connects to; through a proxy, the daemon's bind is on another port.
|
|
2417
|
+
const details = [pingBody?.listener && `listener ${pingBody.listener}`, pingBody?.bind && `bind ${pingBody.bind}`].filter(Boolean).join(", ");
|
|
2418
|
+
console.log(`serve: ${ping ? `running ${serviceUrl(url)}${details ? ` (${details})` : ""}` : "not running"}`);
|
|
2283
2419
|
// daemonManager().id — there is no PM_BIN constant. Referencing one threw a
|
|
2284
2420
|
// ReferenceError that took down the whole of `rech status`, so the one command
|
|
2285
2421
|
// that reports "the relay is wedged" died exactly when the relay was wedged,
|
|
@@ -2343,12 +2479,26 @@ Usage:
|
|
|
2343
2479
|
rech uninstall Remove the serve daemon and clear config
|
|
2344
2480
|
rech serve Start the serve server manually (foreground)
|
|
2345
2481
|
rech listener [ls|add|remove] Manage daemon listener addresses and allowed profiles
|
|
2482
|
+
rech listener allow|deny <name> <profile...>
|
|
2483
|
+
Add or remove profiles on an existing listener
|
|
2484
|
+
rech listener port [name] Print a listener's port, for a reverse-proxy command
|
|
2485
|
+
rech listener set <name> --public-url <url>
|
|
2486
|
+
Record where a proxy exposes the listener (rech url uses it)
|
|
2487
|
+
rech listener rotate-key <name>
|
|
2488
|
+
New key for a listener; URLs with the old key stop working
|
|
2346
2489
|
rech profile [ls|list]
|
|
2347
2490
|
List Chrome + managed test profiles and connection status
|
|
2348
|
-
rech
|
|
2349
|
-
Print a connection
|
|
2350
|
-
|
|
2491
|
+
rech url [profile] [--listener <name>] [--local] [--save]
|
|
2492
|
+
Print a connection URL (includes the secret key): the public
|
|
2493
|
+
URL when one is set, else the listener address. --save also
|
|
2494
|
+
writes it to this project's .rechrome/.env.local.
|
|
2495
|
+
\`rech profile [name] --print-uri\` is an alias.
|
|
2496
|
+
rech url ls List every listener × profile URL (keys hidden)
|
|
2497
|
+
rech connect <url> Check a shared URL answers, then save it for this project
|
|
2351
2498
|
rech <playwright-args...> Run Playwright CLI command (requires ${ENV_KEY})
|
|
2499
|
+
rech pw <playwright-args...> Forward verbatim to playwright-cli, even when a name clashes
|
|
2500
|
+
with rech's own (rech pw --version, rech pw status)
|
|
2501
|
+
rech --version rechrome's version
|
|
2352
2502
|
rech --isolate <args...> Run in a throwaway session (sugar for -s=<random>) so a
|
|
2353
2503
|
fragile single-shot flow (OAuth/login) never shares tabs
|
|
2354
2504
|
with the worktree's default session
|
|
@@ -2378,7 +2528,14 @@ export type RechHandlers = {
|
|
|
2378
2528
|
addListener(name: string, opts: { listen: string; profile: string[]; port?: number; prefix?: string }): Promise<void>;
|
|
2379
2529
|
removeListener(name: string): Promise<void>;
|
|
2380
2530
|
listProfiles(): Promise<void>;
|
|
2381
|
-
printProfileUri(selector?: string, listener?: string): Promise<void>;
|
|
2531
|
+
printProfileUri(selector?: string, listener?: string, opts?: { local?: boolean; save?: boolean }): Promise<void>;
|
|
2532
|
+
urlList(): Promise<void>;
|
|
2533
|
+
connect(url: string): Promise<void>;
|
|
2534
|
+
listenerPort(name?: string): Promise<void>;
|
|
2535
|
+
allowListener(name: string, profiles: string[]): Promise<void>;
|
|
2536
|
+
denyListener(name: string, profiles: string[]): Promise<void>;
|
|
2537
|
+
rotateKey(name: string): Promise<void>;
|
|
2538
|
+
setListener(name: string, opts: { publicUrl?: string; clearPublicUrl?: boolean }): Promise<void>;
|
|
2382
2539
|
setup(opts: SetupOptions): Promise<void>;
|
|
2383
2540
|
tray(action?: string): Promise<void>;
|
|
2384
2541
|
provisionProfile(name: string, opts: { headed: boolean; experimental: boolean }): Promise<void>;
|
|
@@ -2386,7 +2543,7 @@ export type RechHandlers = {
|
|
|
2386
2543
|
};
|
|
2387
2544
|
|
|
2388
2545
|
/** Commands rech handles itself; anything else is forwarded verbatim to playwright-cli. */
|
|
2389
|
-
export const RECH_COMMANDS = new Set(["serve", "status", "listener", "listeners", "profile", "profiles", "setup", "tray", "provision-profile", "uninstall"]);
|
|
2546
|
+
export const RECH_COMMANDS = new Set(["serve", "status", "listener", "listeners", "profile", "profiles", "url", "urls", "connect", "setup", "tray", "provision-profile", "uninstall"]);
|
|
2390
2547
|
|
|
2391
2548
|
const portOption = { type: "number", requiresArg: true, describe: "Listener port (1-65535)" } as const;
|
|
2392
2549
|
|
|
@@ -2407,13 +2564,40 @@ export function rechCli(argv: string[], handlers: RechHandlers) {
|
|
|
2407
2564
|
a => handlers.addListener(a.name, { listen: a.listen, profile: a.profile, port: a.port, prefix: a.prefix }))
|
|
2408
2565
|
.command("remove <name>", "Remove a listener", y => y.positional("name", { type: "string", demandOption: true }),
|
|
2409
2566
|
a => handlers.removeListener(a.name))
|
|
2567
|
+
.command("port [name]", "Print a listener's port (for a proxy command)", y => y.positional("name", { type: "string" }),
|
|
2568
|
+
a => handlers.listenerPort(a.name))
|
|
2569
|
+
.command("allow <name> <profiles..>", "Allow more profiles on a listener", y => y
|
|
2570
|
+
.positional("name", { type: "string", demandOption: true }).positional("profiles", { type: "string", array: true, demandOption: true }),
|
|
2571
|
+
a => handlers.allowListener(a.name, a.profiles))
|
|
2572
|
+
.command("deny <name> <profiles..>", "Remove profiles from a listener", y => y
|
|
2573
|
+
.positional("name", { type: "string", demandOption: true }).positional("profiles", { type: "string", array: true, demandOption: true }),
|
|
2574
|
+
a => handlers.denyListener(a.name, a.profiles))
|
|
2575
|
+
.command("rotate-key <name>", "Give a listener a new key (old URLs stop working)", y => y.positional("name", { type: "string", demandOption: true }),
|
|
2576
|
+
a => handlers.rotateKey(a.name))
|
|
2577
|
+
.command("set <name>", "Record where a reverse proxy exposes a listener", y => y
|
|
2578
|
+
.positional("name", { type: "string", demandOption: true })
|
|
2579
|
+
.option("public-url", { type: "string", requiresArg: true, describe: "e.g. https://host.example.js.net/rechrome/" })
|
|
2580
|
+
.option("clear-public-url", { type: "boolean", conflicts: "public-url" })
|
|
2581
|
+
.check(a => a.publicUrl !== undefined || a.clearPublicUrl ? true : "Pass --public-url <url> or --clear-public-url"),
|
|
2582
|
+
a => handlers.setListener(a.name, { publicUrl: a.publicUrl, clearPublicUrl: a.clearPublicUrl }))
|
|
2410
2583
|
.demandCommand(1).strict())
|
|
2584
|
+
.command(["url [profile]", "urls [profile]"], "Print a profile's connection URL (contains a secret key); `url ls` lists them", y => y
|
|
2585
|
+
.positional("profile", { type: "string", describe: "Profile (email, name or folder); ls/list lists all listeners' URLs" })
|
|
2586
|
+
.option("listener", { type: "string", requiresArg: true, describe: "Listener to build the URL for" })
|
|
2587
|
+
.option("local", { type: "boolean", describe: "Print the direct listener address even when a public URL is set" })
|
|
2588
|
+
.option("save", { type: "boolean", describe: "Also save it as RECHROME_URL in this project's .rechrome/.env.local" }),
|
|
2589
|
+
a => ["ls", "list"].includes(a.profile ?? "") && !a.listener && !a.save
|
|
2590
|
+
? handlers.urlList()
|
|
2591
|
+
: handlers.printProfileUri(a.profile, a.listener, { local: a.local, save: a.save }))
|
|
2592
|
+
.command("connect <url>", "Check a shared connection URL and save it for this project", y => y
|
|
2593
|
+
.positional("url", { type: "string", demandOption: true }),
|
|
2594
|
+
a => handlers.connect(a.url))
|
|
2411
2595
|
.command(["profile [name]", "profiles [name]"], "List profiles, or print a profile's connection URI", y => y
|
|
2412
2596
|
.positional("name", { type: "string", describe: "Profile (email, name or folder); ls/list lists all" })
|
|
2413
2597
|
.option("print-uri", { type: "boolean", describe: "Print the profile's connection URI (contains a secret key)" })
|
|
2414
2598
|
.option("listener", { type: "string", requiresArg: true, implies: "print-uri", describe: "Listener to build the URI for" }),
|
|
2415
2599
|
a => {
|
|
2416
|
-
if (a.printUri) return handlers.printProfileUri(a.name, a.listener);
|
|
2600
|
+
if (a.printUri) return handlers.printProfileUri(a.name, a.listener); // alias of `rech url`
|
|
2417
2601
|
if (a.name === undefined || ["ls", "list"].includes(a.name)) return handlers.listProfiles();
|
|
2418
2602
|
throw new Error("Usage: rech profile [ls|list] | rech profile [name] --print-uri. Create, rename, and delete are not implemented.");
|
|
2419
2603
|
})
|
|
@@ -2450,6 +2634,7 @@ if (import.meta.main) {
|
|
|
2450
2634
|
serve: async () => { const { serve } = await import("./serve.js"); serve(); }, // long-lived; watcher intentionally kept alive
|
|
2451
2635
|
status,
|
|
2452
2636
|
listListeners, addListener, removeListener, listProfiles, printProfileUri,
|
|
2637
|
+
urlList, connect, listenerPort, allowListener, denyListener, rotateKey, setListener,
|
|
2453
2638
|
setup: async (opts) => {
|
|
2454
2639
|
await setup(opts); // setup closes envWatcher itself before printing Done
|
|
2455
2640
|
// Auto-start the tray (best-effort, silent on headless / missing binary).
|
|
@@ -2478,6 +2663,9 @@ if (import.meta.main) {
|
|
|
2478
2663
|
} finally {
|
|
2479
2664
|
if (cmd !== "serve") envWatcher?.close();
|
|
2480
2665
|
}
|
|
2666
|
+
} else if (cmd === "--version") {
|
|
2667
|
+
console.log(rechromeVersion()); // playwright-cli's own: rech pw --version
|
|
2668
|
+
envWatcher?.close();
|
|
2481
2669
|
} else if (cmd === "help" || cmd === "--help" || cmd === "-h" || args.length === 0) {
|
|
2482
2670
|
printHelp();
|
|
2483
2671
|
envWatcher?.close();
|
|
@@ -2492,6 +2680,10 @@ if (import.meta.main) {
|
|
|
2492
2680
|
// the leading-flags-only rule that protects the forwarded CLI's own --profile).
|
|
2493
2681
|
let profileSelector: string | undefined;
|
|
2494
2682
|
let overrideEnv: Record<string, string> | undefined;
|
|
2683
|
+
// Everything after rech's `pw` (or `--`) goes to playwright-cli untouched; only the flags before it are rech's.
|
|
2684
|
+
const separator = rechSeparatorIndex(args);
|
|
2685
|
+
const forwarded = separator === -1 ? [] : args.slice(separator + 1);
|
|
2686
|
+
if (separator !== -1) args = args.slice(0, separator);
|
|
2495
2687
|
try {
|
|
2496
2688
|
const extracted = extractGlobalProfileArg(args);
|
|
2497
2689
|
profileSelector = extracted.selector;
|
|
@@ -2531,6 +2723,7 @@ if (import.meta.main) {
|
|
|
2531
2723
|
args.splice(isolateIdx, 1);
|
|
2532
2724
|
args.push(`-s=iso-${randomBytes(8).toString("hex")}`);
|
|
2533
2725
|
}
|
|
2726
|
+
args = [...forwarded, ...args];
|
|
2534
2727
|
await run(url, args, overrideEnv);
|
|
2535
2728
|
envWatcher?.close();
|
|
2536
2729
|
}
|