rechrome 1.24.0 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/package.json +1 -1
- package/rech.js +52 -14
- package/rech.ts +52 -14
package/README.md
CHANGED
|
@@ -46,6 +46,11 @@ Now `rechrome` (or `rech`) is available globally.
|
|
|
46
46
|
|
|
47
47
|
`rech setup` configures the daemon, Chrome extension, and connection URL in one pass:
|
|
48
48
|
|
|
49
|
+
If no supported daemon manager is available, setup asks before installing `oxmgr` globally
|
|
50
|
+
(default: No). It uses `bun i -g oxmgr` when launched with bunx and `npm i -g oxmgr`
|
|
51
|
+
when launched with npx. Pass `--yes` to approve this installation without prompting,
|
|
52
|
+
for example `bunx rechrome setup --profile Default --yes`.
|
|
53
|
+
|
|
49
54
|
```bash
|
|
50
55
|
rech setup # interactive: pick a profile, follow the prompts
|
|
51
56
|
rech setup --profile you@email.com # non-interactive profile selection
|
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync
|
|
|
6
6
|
import { hostname, homedir } from "os";
|
|
7
7
|
import { join, basename, dirname } from "path";
|
|
8
8
|
import { spawn as cpSpawn } from "child_process";
|
|
9
|
-
import { pickDaemonManager, type DaemonManager } from "./daemon-manager.js";
|
|
9
|
+
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.js";
|
|
10
10
|
|
|
11
11
|
export const ENV_KEY = "RECHROME_URL";
|
|
12
12
|
export const DEFAULT_PORT = 13775;
|
|
@@ -174,17 +174,19 @@ export function parseUrl(raw: string) {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export async function getOrCreateUrl(): Promise<string> {
|
|
177
|
+
export async function getOrCreateUrl(persist = true): Promise<string> {
|
|
178
178
|
// Treat a URL without a bearer key as missing — it cannot authenticate
|
|
179
179
|
try { if (process.env[ENV_KEY] && new URL(process.env[ENV_KEY]!).username) return process.env[ENV_KEY]!; } catch {}
|
|
180
180
|
const key = randomBytes(12).toString("base64url"); // 16 chars
|
|
181
181
|
const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
if (persist) {
|
|
183
|
+
const newLine = `${ENV_KEY}=${url}`;
|
|
184
|
+
// Write to ~/.env.local so it's not shadowed by project .env.local
|
|
185
|
+
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
186
|
+
const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
187
|
+
const content = [...lines, newLine, ""].join("\n");
|
|
188
|
+
await Bun.write(globalEnvFile, content);
|
|
189
|
+
}
|
|
188
190
|
process.env[ENV_KEY] = url;
|
|
189
191
|
return url;
|
|
190
192
|
}
|
|
@@ -898,6 +900,7 @@ export function resolvePlaywrightCli(): string {
|
|
|
898
900
|
}
|
|
899
901
|
|
|
900
902
|
export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
903
|
+
const mgr = daemonManager();
|
|
901
904
|
// Persist the URL to ~/.env.local before starting the daemon. The daemon's
|
|
902
905
|
// loadEnv() walks CWD→root reading .env.local files and unconditionally
|
|
903
906
|
// overwrites process.env.RECHROME_URL from whichever file it finds first.
|
|
@@ -934,8 +937,6 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
934
937
|
if (isReadable(process.env.RECH_TLS_CERT)) daemonEnv.RECH_TLS_CERT = process.env.RECH_TLS_CERT!;
|
|
935
938
|
if (isReadable(process.env.RECH_TLS_KEY)) daemonEnv.RECH_TLS_KEY = process.env.RECH_TLS_KEY!;
|
|
936
939
|
|
|
937
|
-
const mgr = daemonManager();
|
|
938
|
-
|
|
939
940
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
940
941
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
941
942
|
|
|
@@ -1319,7 +1320,7 @@ async function provisionProfile(name: string, opts: { headed?: boolean } = {}):
|
|
|
1319
1320
|
console.log(`Or save it to a project .env.local to make it the default.`);
|
|
1320
1321
|
}
|
|
1321
1322
|
|
|
1322
|
-
async function setup(opts: { profile?: string; token?: string } = {}): Promise<void> {
|
|
1323
|
+
async function setup(opts: { profile?: string; token?: string; yes?: boolean } = {}): Promise<void> {
|
|
1323
1324
|
if (opts.profile !== undefined) {
|
|
1324
1325
|
try {
|
|
1325
1326
|
validateChromeProfileSelector(opts.profile);
|
|
@@ -1366,7 +1367,8 @@ async function setup(opts: { profile?: string; token?: string } = {}): Promise<v
|
|
|
1366
1367
|
if (!["127.0.0.1", "localhost"].includes(u.hostname)) delete process.env[ENV_KEY];
|
|
1367
1368
|
} catch {}
|
|
1368
1369
|
}
|
|
1369
|
-
|
|
1370
|
+
// Defer persistence until daemon prerequisites have passed.
|
|
1371
|
+
const url = await getOrCreateUrl(false);
|
|
1370
1372
|
const { host, port, protocol } = parseUrl(url);
|
|
1371
1373
|
|
|
1372
1374
|
const { key: serveKey } = parseUrl(url);
|
|
@@ -1404,6 +1406,40 @@ async function setup(opts: { profile?: string; token?: string } = {}): Promise<v
|
|
|
1404
1406
|
console.log(` Daemon already running at ${protocol}://${host}:${port} (bind: ${currentBind}) — skipping daemon setup`);
|
|
1405
1407
|
}
|
|
1406
1408
|
const bindChanged = desiredBind !== currentBind;
|
|
1409
|
+
if (!daemonHealthy || bindChanged) {
|
|
1410
|
+
try {
|
|
1411
|
+
try {
|
|
1412
|
+
daemonManager();
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
// Respect an explicit pm2 choice; installing oxmgr would not satisfy it.
|
|
1415
|
+
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1416
|
+
const command = oxmgrInstallCommand(process.env);
|
|
1417
|
+
const answer = opts.yes ? "yes" : (await ask(` oxmgr is missing. Install globally with \`${command.join(" ")}\`? [y/N]: `)).trim();
|
|
1418
|
+
if (!/^(y|yes)$/i.test(answer)) {
|
|
1419
|
+
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1420
|
+
}
|
|
1421
|
+
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
1422
|
+
const installer = Bun.which(command[0]) ?? (command[0] === "bun" ? process.execPath : null);
|
|
1423
|
+
if (!installer) throw new Error(`${command[0]} is not on PATH. Install it or run \`${command.join(" ")}\` in your terminal, then rerun setup.`);
|
|
1424
|
+
const proc = Bun.spawn([installer, ...command.slice(1)], {
|
|
1425
|
+
stdin: "inherit", stdout: "inherit", stderr: "inherit", windowsHide: true,
|
|
1426
|
+
});
|
|
1427
|
+
const code = await proc.exited;
|
|
1428
|
+
if (code !== 0) throw new Error(`\`${command.join(" ")}\` failed (exit code ${code}). Resolve the installation error, then rerun setup.`);
|
|
1429
|
+
if (!Bun.which("oxmgr")) {
|
|
1430
|
+
throw new Error("oxmgr was installed but is not on PATH. Add the package manager's global bin directory to PATH, then rerun setup.");
|
|
1431
|
+
}
|
|
1432
|
+
_daemonMgr = undefined;
|
|
1433
|
+
_oxmgrVersion = undefined;
|
|
1434
|
+
daemonManager();
|
|
1435
|
+
}
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1438
|
+
rl?.close();
|
|
1439
|
+
envWatcher?.close();
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1407
1443
|
const persistedChanged = desiredBind !== persistedBind;
|
|
1408
1444
|
if (persistedChanged) {
|
|
1409
1445
|
const lines = globalEnvRaw.trimEnd().split("\n").filter(l => !/^\s*RECH_HOST\s*=/.test(l));
|
|
@@ -1763,8 +1799,10 @@ Usage:
|
|
|
1763
1799
|
must already be registered (see \`rech setup\`). Place
|
|
1764
1800
|
--profile before the playwright subcommand. Requires
|
|
1765
1801
|
${ENV_KEY}.
|
|
1766
|
-
rech setup [--profile <email|name|folder>] [--token <tok>]
|
|
1802
|
+
rech setup [--profile <email|name|folder>] [--token <tok>] [--yes]
|
|
1767
1803
|
First-time setup: daemon + Chrome extension + config
|
|
1804
|
+
Offers to install missing oxmgr globally (y/N).
|
|
1805
|
+
--yes approves installation without prompting.
|
|
1768
1806
|
--profile selects the Chrome profile non-interactively.
|
|
1769
1807
|
Menu numbers are not accepted. Resolution order is exact
|
|
1770
1808
|
email (e.g. you@gmail.com), exact Chrome profile name,
|
|
@@ -1828,7 +1866,7 @@ if (import.meta.main) {
|
|
|
1828
1866
|
? args[tokenIdx + 1]
|
|
1829
1867
|
: args.find(a => a.startsWith("--token="))?.slice("--token=".length))
|
|
1830
1868
|
?? process.env.RECH_TOKEN;
|
|
1831
|
-
await setup({ profile, token }); // setup closes envWatcher itself before printing Done
|
|
1869
|
+
await setup({ profile, token, yes: args.includes("--yes") }); // setup closes envWatcher itself before printing Done
|
|
1832
1870
|
// Auto-start the tray (best-effort, silent on headless / missing binary).
|
|
1833
1871
|
await startTray({ quiet: true }).catch(() => {});
|
|
1834
1872
|
} else if (cmd === "tray") {
|
package/rech.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync
|
|
|
6
6
|
import { hostname, homedir } from "os";
|
|
7
7
|
import { join, basename, dirname } from "path";
|
|
8
8
|
import { spawn as cpSpawn } from "child_process";
|
|
9
|
-
import { pickDaemonManager, type DaemonManager } from "./daemon-manager.ts";
|
|
9
|
+
import { oxmgrInstallCommand, pickDaemonManager, type DaemonManager } from "./daemon-manager.ts";
|
|
10
10
|
|
|
11
11
|
export const ENV_KEY = "RECHROME_URL";
|
|
12
12
|
export const DEFAULT_PORT = 13775;
|
|
@@ -174,17 +174,19 @@ export function parseUrl(raw: string) {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export async function getOrCreateUrl(): Promise<string> {
|
|
177
|
+
export async function getOrCreateUrl(persist = true): Promise<string> {
|
|
178
178
|
// Treat a URL without a bearer key as missing — it cannot authenticate
|
|
179
179
|
try { if (process.env[ENV_KEY] && new URL(process.env[ENV_KEY]!).username) return process.env[ENV_KEY]!; } catch {}
|
|
180
180
|
const key = randomBytes(12).toString("base64url"); // 16 chars
|
|
181
181
|
const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
if (persist) {
|
|
183
|
+
const newLine = `${ENV_KEY}=${url}`;
|
|
184
|
+
// Write to ~/.env.local so it's not shadowed by project .env.local
|
|
185
|
+
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
186
|
+
const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
187
|
+
const content = [...lines, newLine, ""].join("\n");
|
|
188
|
+
await Bun.write(globalEnvFile, content);
|
|
189
|
+
}
|
|
188
190
|
process.env[ENV_KEY] = url;
|
|
189
191
|
return url;
|
|
190
192
|
}
|
|
@@ -898,6 +900,7 @@ export function resolvePlaywrightCli(): string {
|
|
|
898
900
|
}
|
|
899
901
|
|
|
900
902
|
export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
903
|
+
const mgr = daemonManager();
|
|
901
904
|
// Persist the URL to ~/.env.local before starting the daemon. The daemon's
|
|
902
905
|
// loadEnv() walks CWD→root reading .env.local files and unconditionally
|
|
903
906
|
// overwrites process.env.RECHROME_URL from whichever file it finds first.
|
|
@@ -934,8 +937,6 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
934
937
|
if (isReadable(process.env.RECH_TLS_CERT)) daemonEnv.RECH_TLS_CERT = process.env.RECH_TLS_CERT!;
|
|
935
938
|
if (isReadable(process.env.RECH_TLS_KEY)) daemonEnv.RECH_TLS_KEY = process.env.RECH_TLS_KEY!;
|
|
936
939
|
|
|
937
|
-
const mgr = daemonManager();
|
|
938
|
-
|
|
939
940
|
// Drop any prior registration (current + legacy names) before re-adding.
|
|
940
941
|
for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
|
|
941
942
|
|
|
@@ -1319,7 +1320,7 @@ async function provisionProfile(name: string, opts: { headed?: boolean } = {}):
|
|
|
1319
1320
|
console.log(`Or save it to a project .env.local to make it the default.`);
|
|
1320
1321
|
}
|
|
1321
1322
|
|
|
1322
|
-
async function setup(opts: { profile?: string; token?: string } = {}): Promise<void> {
|
|
1323
|
+
async function setup(opts: { profile?: string; token?: string; yes?: boolean } = {}): Promise<void> {
|
|
1323
1324
|
if (opts.profile !== undefined) {
|
|
1324
1325
|
try {
|
|
1325
1326
|
validateChromeProfileSelector(opts.profile);
|
|
@@ -1366,7 +1367,8 @@ async function setup(opts: { profile?: string; token?: string } = {}): Promise<v
|
|
|
1366
1367
|
if (!["127.0.0.1", "localhost"].includes(u.hostname)) delete process.env[ENV_KEY];
|
|
1367
1368
|
} catch {}
|
|
1368
1369
|
}
|
|
1369
|
-
|
|
1370
|
+
// Defer persistence until daemon prerequisites have passed.
|
|
1371
|
+
const url = await getOrCreateUrl(false);
|
|
1370
1372
|
const { host, port, protocol } = parseUrl(url);
|
|
1371
1373
|
|
|
1372
1374
|
const { key: serveKey } = parseUrl(url);
|
|
@@ -1404,6 +1406,40 @@ async function setup(opts: { profile?: string; token?: string } = {}): Promise<v
|
|
|
1404
1406
|
console.log(` Daemon already running at ${protocol}://${host}:${port} (bind: ${currentBind}) — skipping daemon setup`);
|
|
1405
1407
|
}
|
|
1406
1408
|
const bindChanged = desiredBind !== currentBind;
|
|
1409
|
+
if (!daemonHealthy || bindChanged) {
|
|
1410
|
+
try {
|
|
1411
|
+
try {
|
|
1412
|
+
daemonManager();
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
// Respect an explicit pm2 choice; installing oxmgr would not satisfy it.
|
|
1415
|
+
if (process.env.RECH_DAEMON_MANAGER?.toLowerCase() === "pm2") throw error;
|
|
1416
|
+
const command = oxmgrInstallCommand(process.env);
|
|
1417
|
+
const answer = opts.yes ? "yes" : (await ask(` oxmgr is missing. Install globally with \`${command.join(" ")}\`? [y/N]: `)).trim();
|
|
1418
|
+
if (!/^(y|yes)$/i.test(answer)) {
|
|
1419
|
+
throw new Error(`Setup cancelled. To install oxmgr, run \`${command.join(" ")}\`, then rerun setup.`);
|
|
1420
|
+
}
|
|
1421
|
+
console.log(` Installing oxmgr: ${command.join(" ")}`);
|
|
1422
|
+
const installer = Bun.which(command[0]) ?? (command[0] === "bun" ? process.execPath : null);
|
|
1423
|
+
if (!installer) throw new Error(`${command[0]} is not on PATH. Install it or run \`${command.join(" ")}\` in your terminal, then rerun setup.`);
|
|
1424
|
+
const proc = Bun.spawn([installer, ...command.slice(1)], {
|
|
1425
|
+
stdin: "inherit", stdout: "inherit", stderr: "inherit", windowsHide: true,
|
|
1426
|
+
});
|
|
1427
|
+
const code = await proc.exited;
|
|
1428
|
+
if (code !== 0) throw new Error(`\`${command.join(" ")}\` failed (exit code ${code}). Resolve the installation error, then rerun setup.`);
|
|
1429
|
+
if (!Bun.which("oxmgr")) {
|
|
1430
|
+
throw new Error("oxmgr was installed but is not on PATH. Add the package manager's global bin directory to PATH, then rerun setup.");
|
|
1431
|
+
}
|
|
1432
|
+
_daemonMgr = undefined;
|
|
1433
|
+
_oxmgrVersion = undefined;
|
|
1434
|
+
daemonManager();
|
|
1435
|
+
}
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1438
|
+
rl?.close();
|
|
1439
|
+
envWatcher?.close();
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1407
1443
|
const persistedChanged = desiredBind !== persistedBind;
|
|
1408
1444
|
if (persistedChanged) {
|
|
1409
1445
|
const lines = globalEnvRaw.trimEnd().split("\n").filter(l => !/^\s*RECH_HOST\s*=/.test(l));
|
|
@@ -1763,8 +1799,10 @@ Usage:
|
|
|
1763
1799
|
must already be registered (see \`rech setup\`). Place
|
|
1764
1800
|
--profile before the playwright subcommand. Requires
|
|
1765
1801
|
${ENV_KEY}.
|
|
1766
|
-
rech setup [--profile <email|name|folder>] [--token <tok>]
|
|
1802
|
+
rech setup [--profile <email|name|folder>] [--token <tok>] [--yes]
|
|
1767
1803
|
First-time setup: daemon + Chrome extension + config
|
|
1804
|
+
Offers to install missing oxmgr globally (y/N).
|
|
1805
|
+
--yes approves installation without prompting.
|
|
1768
1806
|
--profile selects the Chrome profile non-interactively.
|
|
1769
1807
|
Menu numbers are not accepted. Resolution order is exact
|
|
1770
1808
|
email (e.g. you@gmail.com), exact Chrome profile name,
|
|
@@ -1828,7 +1866,7 @@ if (import.meta.main) {
|
|
|
1828
1866
|
? args[tokenIdx + 1]
|
|
1829
1867
|
: args.find(a => a.startsWith("--token="))?.slice("--token=".length))
|
|
1830
1868
|
?? process.env.RECH_TOKEN;
|
|
1831
|
-
await setup({ profile, token }); // setup closes envWatcher itself before printing Done
|
|
1869
|
+
await setup({ profile, token, yes: args.includes("--yes") }); // setup closes envWatcher itself before printing Done
|
|
1832
1870
|
// Auto-start the tray (best-effort, silent on headless / missing binary).
|
|
1833
1871
|
await startTray({ quiet: true }).catch(() => {});
|
|
1834
1872
|
} else if (cmd === "tray") {
|