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.
Files changed (4) hide show
  1. package/README.md +5 -0
  2. package/package.json +1 -1
  3. package/rech.js +52 -14
  4. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.24.0",
3
+ "version": "1.24.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
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
- const newLine = `${ENV_KEY}=${url}`;
183
- // Write to ~/.env.local so it's not shadowed by project .env.local
184
- const envRaw = await file(globalEnvFile).text().catch(() => "");
185
- const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
186
- const content = [...lines, newLine, ""].join("\n");
187
- Bun.write(globalEnvFile, content);
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
- const url = await getOrCreateUrl();
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
- const newLine = `${ENV_KEY}=${url}`;
183
- // Write to ~/.env.local so it's not shadowed by project .env.local
184
- const envRaw = await file(globalEnvFile).text().catch(() => "");
185
- const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
186
- const content = [...lines, newLine, ""].join("\n");
187
- Bun.write(globalEnvFile, content);
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
- const url = await getOrCreateUrl();
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") {