rechrome 1.31.3 → 1.31.4

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 CHANGED
@@ -74,6 +74,8 @@ between calls; another worktree gets its own. `-s=<name>` opens a named sub-sess
74
74
  `<project>` is the git worktree root (submodules count as their parent project), or the current
75
75
  directory outside git. rechrome reads `RECHROME_URL` from the nearest `.rechrome/.env.local` or
76
76
  `.env.local` walking up from the current directory; an explicit environment variable wins.
77
+ When editing a file by hand, quote the URL (`RECHROME_URL="…#key=…"`): unquoted, a `#` starts a
78
+ comment for some .env loaders. rech quotes it when it saves one.
77
79
 
78
80
  ### 4. Use it from another machine
79
81
 
@@ -106,7 +108,7 @@ rech open https://example.com
106
108
  `.rechrome/.env.local`. Remote listeners allow navigation, tabs, snapshots, screenshots and basic
107
109
  interactions, but not `eval`/`run-code` or filesystem commands (see [Remote access](#remote-access)).
108
110
 
109
- **Share several profiles in one link.** `rech share work@example.com taku2 symval-dev` puts
111
+ **Share several profiles in one link.** `rech share work@example.com personal team-dev` puts
110
112
  exactly those profiles on one listener (its own key; running it again reuses it). Add
111
113
  `--listener <name>` to use a listener that already has a proxy route. Its links then reach
112
114
  exactly this list, and `rech listener rotate-key <name>` issues a fresh key if old links shouldn't.
@@ -262,7 +264,7 @@ the last working configuration. Each listener has its own bearer key. Use `rech
262
264
  to see configured exposure, and `rech listener ls` to list bindings without credentials.
263
265
 
264
266
  ```bash
265
- rech listener add qa --listen tailscale --profile qamac-remote --port 13776
267
+ rech listener add qa --listen tailscale --profile qa-remote --port 13776
266
268
  rech listener remove qa
267
269
  ```
268
270
 
package/listeners.ts CHANGED
@@ -142,7 +142,7 @@ const profileIdentity = (e: RegisteredProfile) => `${e.userDataDir ?? ""}\0${e.p
142
142
 
143
143
  /**
144
144
  * One registry key per Chrome profile (a profile can be registered under several aliases,
145
- * e.g. "Profile 5" and "taku2"): prefer an email, else the alphabetically first key.
145
+ * e.g. "Profile 5" and "personal"): prefer an email, else the alphabetically first key.
146
146
  */
147
147
  export function canonicalProfileKeys(registry: Record<string, RegisteredProfile>): string[] {
148
148
  const byProfile = new Map<string, string>();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.31.3",
3
+ "version": "1.31.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rechrome.js CHANGED
@@ -88,7 +88,44 @@ const globalEnvFile = join(HOME || "~", ".env.local");
88
88
 
89
89
  // Capture inherited values once so explicit environment overrides survive reloads,
90
90
  // while values loaded from files can still change when those files are edited.
91
- const inheritedEnvKeys = new Set(Object.keys(process.env));
91
+ /**
92
+ * A KEY=value line for an env file. A value with "#" (every connection URL's #key=…) or
93
+ * whitespace is double-quoted: Bun's own .env loader treats an unquoted "#" as a comment and
94
+ * would drop the key. rech's loader strips the quotes again.
95
+ */
96
+ export function envAssignment(key: string, value: string): string {
97
+ return /[#\s]/.test(value) ? `${key}="${value}"` : `${key}=${value}`;
98
+ }
99
+
100
+ /** Parse KEY=value lines the way Bun's .env loader does (unquoted values end at "#"). */
101
+ function parseBunEnvFile(text: string): Record<string, string> {
102
+ const out: Record<string, string> = {};
103
+ for (const line of text.split("\n")) {
104
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/);
105
+ if (!m) continue;
106
+ const raw = m[2].trim();
107
+ const quoted = raw.match(/^(["'`])(.*)\1/);
108
+ out[m[1]] = quoted ? quoted[2] : raw.replace(/\s*#.*$/, "").trim();
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Keys Bun itself loaded from the current folder's .env files before rech started. They look
115
+ * like shell exports, but are file values: counting them as inherited would let a folder's
116
+ * .env.local beat its .rechrome/.env.local, and a keyless (cut at "#") URL beat a good one.
117
+ */
118
+ export function bunAutoloadedKeys(env: Record<string, string | undefined>, files: string[]): Set<string> {
119
+ const keys = new Set<string>();
120
+ for (const text of files)
121
+ for (const [key, value] of Object.entries(parseBunEnvFile(text))) if (env[key] === value) keys.add(key);
122
+ return keys;
123
+ }
124
+
125
+ const bunLoaded = bunAutoloadedKeys(process.env,
126
+ [".env", `.env.${process.env.NODE_ENV || "development"}`, ".env.local", `.env.${process.env.NODE_ENV || "development"}.local`]
127
+ .map(name => { try { return readFileSync(join(process.cwd(), name), "utf8"); } catch { return ""; } }));
128
+ const inheritedEnvKeys = new Set(Object.keys(process.env).filter(key => !bunLoaded.has(key)));
92
129
 
93
130
  // Walk CWD→root loading env files nearest-first; inherited environment wins over files.
94
131
  // At each level .rechrome/.env.local is checked before .env.local (rechrome-specific overrides general).
@@ -437,7 +474,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
437
474
  const key = randomBytes(12).toString("base64url"); // 16 chars
438
475
  const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
439
476
  if (persist) {
440
- const newLine = `${ENV_KEY}=${url}`;
477
+ const newLine = envAssignment(ENV_KEY, url);
441
478
  // Write to ~/.env.local so it's not shadowed by project .env.local
442
479
  const envRaw = await file(globalEnvFile).text().catch(() => "");
443
480
  const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
@@ -2048,7 +2085,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
2048
2085
  const envRaw = await file(globalEnvFile).text().catch(() => "");
2049
2086
  const lines = envRaw.trimEnd().split("\n");
2050
2087
  const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
2051
- await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))}`, ""].join("\n"));
2088
+ await Bun.write(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"));
2052
2089
 
2053
2090
  const home = HOME;
2054
2091
  const bunBin = Bun.which("bun") ?? process.execPath;
@@ -2434,14 +2471,15 @@ async function provisionProfile(name: string, opts: { headed?: boolean } = {}):
2434
2471
  rechUrl.searchParams.set("load_extension", dist);
2435
2472
 
2436
2473
 
2437
- const newLine = `RECHROME_URL=${registeredProfileUrl(rechUrl.toString())}`;
2474
+ const profileUrl = registeredProfileUrl(rechUrl.toString());
2475
+ const newLine = envAssignment(ENV_KEY, profileUrl);
2438
2476
 
2439
2477
  // [3/3] Register in the token registry so `rech status` lists it and the daemon can resolve it.
2440
2478
  await saveTokenEntry(name, { extensionId: EXTENSION_ID, token, profileDir: name, userDataDir, loadExtension: dist });
2441
2479
  console.log(`\n[3/3] Registered "${name}" in ${TOKENS_FILE}`);
2442
2480
 
2443
2481
  console.log(`\nDone! RECHROME_URL for "${name}":\n\n ${newLine}\n`);
2444
- console.log(`Use it per-call:\n ${newLine.replace("RECHROME_URL=", "RECHROME_URL='")}' rech open https://example.com\n`);
2482
+ console.log(`Use it per-call:\n ${ENV_KEY}='${profileUrl}' rech open https://example.com\n`);
2445
2483
  console.log(`Or save it to a project .env.local to make it the default.`);
2446
2484
  }
2447
2485
 
@@ -2598,7 +2636,7 @@ async function saveProjectUrl(url: string): Promise<string> {
2598
2636
  if (!existsSync(gitignore)) await Bun.write(gitignore, "*\n");
2599
2637
  const envPath = join(dataDir, ".env.local");
2600
2638
  const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
2601
- await writeFile(envPath, [...lines, `${ENV_KEY}=${url}`, ""].join("\n"), { mode: 0o600 });
2639
+ await writeFile(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), { mode: 0o600 });
2602
2640
  await chmod(envPath, 0o600);
2603
2641
  return envPath;
2604
2642
  }
@@ -2948,7 +2986,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
2948
2986
  console.log(` rech setup --profile ${JSON.stringify(profileEmail)} --listen local --prefix=rechrome`);
2949
2987
  console.log(` then expose it with tailscale serve (setup prints the command and the remote URL).`);
2950
2988
  }
2951
- const newLine = `RECHROME_URL=${registeredProfileUrl(rechUrl.toString())}`;
2989
+ const newLine = envAssignment(ENV_KEY, registeredProfileUrl(rechUrl.toString()));
2952
2990
 
2953
2991
  console.log("\n○ [5/5] Connection verified. Save connection configuration:");
2954
2992
  console.log(`\n${newLine}\n`);
package/rechrome.ts CHANGED
@@ -88,7 +88,44 @@ const globalEnvFile = join(HOME || "~", ".env.local");
88
88
 
89
89
  // Capture inherited values once so explicit environment overrides survive reloads,
90
90
  // while values loaded from files can still change when those files are edited.
91
- const inheritedEnvKeys = new Set(Object.keys(process.env));
91
+ /**
92
+ * A KEY=value line for an env file. A value with "#" (every connection URL's #key=…) or
93
+ * whitespace is double-quoted: Bun's own .env loader treats an unquoted "#" as a comment and
94
+ * would drop the key. rech's loader strips the quotes again.
95
+ */
96
+ export function envAssignment(key: string, value: string): string {
97
+ return /[#\s]/.test(value) ? `${key}="${value}"` : `${key}=${value}`;
98
+ }
99
+
100
+ /** Parse KEY=value lines the way Bun's .env loader does (unquoted values end at "#"). */
101
+ function parseBunEnvFile(text: string): Record<string, string> {
102
+ const out: Record<string, string> = {};
103
+ for (const line of text.split("\n")) {
104
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/);
105
+ if (!m) continue;
106
+ const raw = m[2].trim();
107
+ const quoted = raw.match(/^(["'`])(.*)\1/);
108
+ out[m[1]] = quoted ? quoted[2] : raw.replace(/\s*#.*$/, "").trim();
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Keys Bun itself loaded from the current folder's .env files before rech started. They look
115
+ * like shell exports, but are file values: counting them as inherited would let a folder's
116
+ * .env.local beat its .rechrome/.env.local, and a keyless (cut at "#") URL beat a good one.
117
+ */
118
+ export function bunAutoloadedKeys(env: Record<string, string | undefined>, files: string[]): Set<string> {
119
+ const keys = new Set<string>();
120
+ for (const text of files)
121
+ for (const [key, value] of Object.entries(parseBunEnvFile(text))) if (env[key] === value) keys.add(key);
122
+ return keys;
123
+ }
124
+
125
+ const bunLoaded = bunAutoloadedKeys(process.env,
126
+ [".env", `.env.${process.env.NODE_ENV || "development"}`, ".env.local", `.env.${process.env.NODE_ENV || "development"}.local`]
127
+ .map(name => { try { return readFileSync(join(process.cwd(), name), "utf8"); } catch { return ""; } }));
128
+ const inheritedEnvKeys = new Set(Object.keys(process.env).filter(key => !bunLoaded.has(key)));
92
129
 
93
130
  // Walk CWD→root loading env files nearest-first; inherited environment wins over files.
94
131
  // At each level .rechrome/.env.local is checked before .env.local (rechrome-specific overrides general).
@@ -437,7 +474,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
437
474
  const key = randomBytes(12).toString("base64url"); // 16 chars
438
475
  const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
439
476
  if (persist) {
440
- const newLine = `${ENV_KEY}=${url}`;
477
+ const newLine = envAssignment(ENV_KEY, url);
441
478
  // Write to ~/.env.local so it's not shadowed by project .env.local
442
479
  const envRaw = await file(globalEnvFile).text().catch(() => "");
443
480
  const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
@@ -2048,7 +2085,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
2048
2085
  const envRaw = await file(globalEnvFile).text().catch(() => "");
2049
2086
  const lines = envRaw.trimEnd().split("\n");
2050
2087
  const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
2051
- await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))}`, ""].join("\n"));
2088
+ await Bun.write(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"));
2052
2089
 
2053
2090
  const home = HOME;
2054
2091
  const bunBin = Bun.which("bun") ?? process.execPath;
@@ -2434,14 +2471,15 @@ async function provisionProfile(name: string, opts: { headed?: boolean } = {}):
2434
2471
  rechUrl.searchParams.set("load_extension", dist);
2435
2472
 
2436
2473
 
2437
- const newLine = `RECHROME_URL=${registeredProfileUrl(rechUrl.toString())}`;
2474
+ const profileUrl = registeredProfileUrl(rechUrl.toString());
2475
+ const newLine = envAssignment(ENV_KEY, profileUrl);
2438
2476
 
2439
2477
  // [3/3] Register in the token registry so `rech status` lists it and the daemon can resolve it.
2440
2478
  await saveTokenEntry(name, { extensionId: EXTENSION_ID, token, profileDir: name, userDataDir, loadExtension: dist });
2441
2479
  console.log(`\n[3/3] Registered "${name}" in ${TOKENS_FILE}`);
2442
2480
 
2443
2481
  console.log(`\nDone! RECHROME_URL for "${name}":\n\n ${newLine}\n`);
2444
- console.log(`Use it per-call:\n ${newLine.replace("RECHROME_URL=", "RECHROME_URL='")}' rech open https://example.com\n`);
2482
+ console.log(`Use it per-call:\n ${ENV_KEY}='${profileUrl}' rech open https://example.com\n`);
2445
2483
  console.log(`Or save it to a project .env.local to make it the default.`);
2446
2484
  }
2447
2485
 
@@ -2598,7 +2636,7 @@ async function saveProjectUrl(url: string): Promise<string> {
2598
2636
  if (!existsSync(gitignore)) await Bun.write(gitignore, "*\n");
2599
2637
  const envPath = join(dataDir, ".env.local");
2600
2638
  const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
2601
- await writeFile(envPath, [...lines, `${ENV_KEY}=${url}`, ""].join("\n"), { mode: 0o600 });
2639
+ await writeFile(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), { mode: 0o600 });
2602
2640
  await chmod(envPath, 0o600);
2603
2641
  return envPath;
2604
2642
  }
@@ -2948,7 +2986,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
2948
2986
  console.log(` rech setup --profile ${JSON.stringify(profileEmail)} --listen local --prefix=rechrome`);
2949
2987
  console.log(` then expose it with tailscale serve (setup prints the command and the remote URL).`);
2950
2988
  }
2951
- const newLine = `RECHROME_URL=${registeredProfileUrl(rechUrl.toString())}`;
2989
+ const newLine = envAssignment(ENV_KEY, registeredProfileUrl(rechUrl.toString()));
2952
2990
 
2953
2991
  console.log("\n○ [5/5] Connection verified. Save connection configuration:");
2954
2992
  console.log(`\n${newLine}\n`);
@@ -11,12 +11,12 @@ Use this workflow when the user requests rechrome setup for an existing Chrome p
11
11
 
12
12
  - Preserve the user's browser choice. Installed Google Chrome profiles belong to the user; do not rename, create, delete, or replace them as part of setup.
13
13
  - If the user wants an isolated Playwright browser, do not send them through real-Chrome extension installation. Playwright can control its own browser directly. Check the current CLI help for available managed-browser support: the legacy `provision-profile --experimental` flow still uses a bridge and may require the vendored relay patch described in the repo's `AGENTS.md`. Do not present a proposed direct-browser mode as implemented.
14
- - Run `rech profile ls` and resolve the exact email, display name, or folder. For no-email profiles, use the folder (for example `Profile 7`) to avoid ambiguity. A requested display name such as `taku3` is also accepted with `--profile`.
14
+ - Run `rech profile ls` and resolve the exact email, display name, or folder. For no-email profiles, use the folder (for example `Profile 7`) to avoid ambiguity. A requested display name such as `Work 2` is also accepted with `--profile`.
15
15
  - Use `rech` or `rechrome` if installed; from a checkout use `bun rechrome.ts`. The package is `rechrome`, not `rech`: use `bunx rechrome`, never `bunx rech`.
16
16
 
17
17
  ## Start and retain the setup process
18
18
 
19
- Run `rech setup --profile "<selector>"` in a retained terminal session, preferably with a TTY. A bare positional selector such as `rech setup taku3` is not supported by the current CLI.
19
+ Run `rech setup --profile "<selector>"` in a retained terminal session, preferably with a TTY. A bare positional selector such as `rech setup work-2` is not supported by the current CLI.
20
20
 
21
21
  - The listen choice applies to the selected profile. Local is the default and removes that profile from remote allowlists; choose LAN or Tailscale when requested. Other profiles keep their exposure. The menu shows Tailscale only when detected. Use `rech listener ls` and `rech profile ls` to review configured bindings. Listener changes reload in one daemon without restarting Chrome; first migration may restart only the daemon.
22
22
  - Keep the terminal session handle and poll it while working in Chrome. Non-TTY mode also works, but input is read until EOF before setup starts: provide all prompt answers and close stdin. EOF does not stop the guide server.