rechrome 1.12.2 → 1.13.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/extension/connect.html +1 -1
- package/extension/lib/background.mjs +518 -262
- package/extension/lib/ui/authToken.css +34 -6
- package/extension/lib/ui/authToken.js +11638 -6158
- package/extension/lib/ui/connect.js +52 -60
- package/extension/lib/ui/status.js +31 -50
- package/extension/manifest.json +8 -7
- package/extension/status.html +1 -1
- package/package.json +1 -1
- package/rech.js +63 -22
- package/rech.ts +63 -22
package/rech.ts
CHANGED
|
@@ -254,7 +254,7 @@ export const EXTENSION_DIST_DIR = join(process.env.HOME!, ".rechrome", "extensio
|
|
|
254
254
|
|
|
255
255
|
// With the manifest `key` field set, Chrome derives this ID deterministically from the key (not the path),
|
|
256
256
|
// so we can locate the extension by ID even when the on-disk path differs from what Chrome stored.
|
|
257
|
-
export const EXTENSION_ID = "
|
|
257
|
+
export const EXTENSION_ID = "mmlmfjhmonkocbjadbfplnigmagldckm";
|
|
258
258
|
|
|
259
259
|
async function ensureExtensionDistInstalled(): Promise<string> {
|
|
260
260
|
const source = existsSync(BUNDLED_EXTENSION_DIST_DIR)
|
|
@@ -279,28 +279,41 @@ async function findInstalledExtension(
|
|
|
279
279
|
const cache = await readChromeProfileCache();
|
|
280
280
|
const profiles = profileDir ? [profileDir] : (cache ? Object.keys(cache) : []);
|
|
281
281
|
// Resolve our known-good install paths up front for path-based fallback matching.
|
|
282
|
+
// LEGACY_EXTENSION_DIST_DIR is intentionally excluded: it points at the pre-V2 multi-tab
|
|
283
|
+
// bridge, which is incompatible with the current cdpRelayV2 relay — matching it would hand
|
|
284
|
+
// setup a stale, broken extension.
|
|
282
285
|
const knownPaths = new Set<string>();
|
|
283
|
-
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR
|
|
286
|
+
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR]) {
|
|
284
287
|
try { knownPaths.add(realpathSync(p)); } catch {}
|
|
285
288
|
}
|
|
289
|
+
// Read each profile's settings once so we can prioritize stable-ID matches over path fallbacks.
|
|
290
|
+
const perProfile: Array<{ prof: string; settings: Record<string, any> }> = [];
|
|
286
291
|
for (const prof of profiles) {
|
|
287
292
|
const prefsPath = join(userDataDir, prof, "Secure Preferences");
|
|
288
293
|
const f = file(prefsPath);
|
|
289
294
|
if (!(await f.exists())) continue;
|
|
290
295
|
try {
|
|
291
296
|
const data = JSON.parse(await f.text());
|
|
292
|
-
|
|
293
|
-
for (const [extId, info] of Object.entries(settings as Record<string, any>)) {
|
|
294
|
-
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
295
|
-
// Primary: stable ID match (works when manifest `key` is set, regardless of path).
|
|
296
|
-
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
297
|
-
// Fallback: path equality for legacy installs without a stable key.
|
|
298
|
-
let storedPath = info.path as string;
|
|
299
|
-
try { storedPath = realpathSync(storedPath); } catch {}
|
|
300
|
-
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
301
|
-
}
|
|
297
|
+
perProfile.push({ prof, settings: (data?.extensions?.settings ?? {}) as Record<string, any> });
|
|
302
298
|
} catch {}
|
|
303
299
|
}
|
|
300
|
+
// Pass 1: stable ID match (manifest `key` set, path-independent). This must win over any path
|
|
301
|
+
// fallback so a stale legacy install sitting on a known path can't shadow the current extension.
|
|
302
|
+
for (const { prof, settings } of perProfile) {
|
|
303
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
304
|
+
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
305
|
+
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Pass 2: path equality fallback for legacy keyless installs without a stable ID.
|
|
309
|
+
for (const { prof, settings } of perProfile) {
|
|
310
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
311
|
+
if (!info?.path || info.state === 0) continue;
|
|
312
|
+
let storedPath = info.path as string;
|
|
313
|
+
try { storedPath = realpathSync(storedPath); } catch {}
|
|
314
|
+
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
304
317
|
return null;
|
|
305
318
|
}
|
|
306
319
|
|
|
@@ -584,7 +597,7 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
584
597
|
};
|
|
585
598
|
|
|
586
599
|
// [1/4] Daemon
|
|
587
|
-
console.log("\n[1/4]
|
|
600
|
+
console.log("\n[1/4] Checking serve daemon...");
|
|
588
601
|
|
|
589
602
|
// Bind address (persists to ~/.env.local as RECH_HOST).
|
|
590
603
|
// Read the persisted value from ~/.env.local directly — process.env may be shadowed by nearer .env files.
|
|
@@ -617,15 +630,24 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
617
630
|
const liveBindUnknown = !!authPing?.ok && !liveBind;
|
|
618
631
|
const currentBind = liveBind || persistedBind;
|
|
619
632
|
|
|
633
|
+
// A healthy daemon already answering on our key needs no reinstall — don't re-prompt for it.
|
|
634
|
+
const daemonHealthy = !!(anonPing && authPing?.ok && !liveBindUnknown);
|
|
635
|
+
// An explicit RECH_HOST override that differs from the live bind is a deliberate rebind request.
|
|
636
|
+
const explicitRebind = !!process.env.RECH_HOST && process.env.RECH_HOST !== currentBind;
|
|
637
|
+
|
|
620
638
|
// Non-TTY honors explicit process.env.RECH_HOST (shell or merged env stack) — matches the documented `RECH_HOST=0.0.0.0 rech setup` flow.
|
|
621
639
|
let desiredBind = process.env.RECH_HOST || currentBind;
|
|
622
|
-
|
|
640
|
+
// Only prompt to (re)configure the bind when we actually need to set up the daemon. A running
|
|
641
|
+
// daemon is left alone unless the user explicitly asks for a different bind via RECH_HOST.
|
|
642
|
+
if (isTTY && (!daemonHealthy || explicitRebind)) {
|
|
623
643
|
console.log(`\n Bind address (current: ${currentBind}):`);
|
|
624
644
|
console.log(` 1. 127.0.0.1 (localhost only)`);
|
|
625
645
|
console.log(` 2. 0.0.0.0 (all interfaces — HTTP plaintext, trust your network)`);
|
|
626
646
|
const defaultBindChoice = currentBind === "0.0.0.0" ? "2" : "1";
|
|
627
647
|
const bindAns = (await ask(` Choice [${defaultBindChoice}]: `, defaultBindChoice)).trim();
|
|
628
648
|
desiredBind = bindAns === "2" || bindAns === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1";
|
|
649
|
+
} else if (daemonHealthy) {
|
|
650
|
+
console.log(` Daemon already running at ${protocol}://${host}:${port} (bind: ${currentBind}) — skipping daemon setup`);
|
|
629
651
|
}
|
|
630
652
|
const bindChanged = desiredBind !== currentBind;
|
|
631
653
|
const persistedChanged = desiredBind !== persistedBind;
|
|
@@ -761,11 +783,26 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
761
783
|
console.log(` [agent] Find PLAYWRIGHT_MCP_EXTENSION_TOKEN=... on that page`);
|
|
762
784
|
console.log(` [agent] Provide the token value on next stdin line:\n`);
|
|
763
785
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
786
|
+
// Retry on empty/too-short paste — a truncated copy or a stale token shouldn't
|
|
787
|
+
// abort the whole setup. Bounded so a non-TTY agent with exhausted stdin can't spin.
|
|
788
|
+
const maxTries = isTTY ? 5 : 3;
|
|
789
|
+
for (let attempt = 1; attempt <= maxTries; attempt++) {
|
|
790
|
+
const tokenInput = (await ask(" Paste token: ")).trim();
|
|
791
|
+
const token = tokenInput.replace(/^.*?=/, "").trim();
|
|
792
|
+
const retriesLeft = maxTries - attempt;
|
|
793
|
+
if (!token) {
|
|
794
|
+
console.error(` No token entered.${retriesLeft ? " Copy the full PLAYWRIGHT_MCP_EXTENSION_TOKEN value and try again." : ""}`);
|
|
795
|
+
} else if (token.length < 20) {
|
|
796
|
+
console.error(` Token too short (${token.length} chars) — likely truncated when copying.${retriesLeft ? " Re-copy the full value and try again." : ""}`);
|
|
797
|
+
} else {
|
|
798
|
+
console.log(" Token accepted");
|
|
799
|
+
return { extId, token };
|
|
800
|
+
}
|
|
801
|
+
// Non-TTY with no input left: ask() won't block, so stop instead of burning retries on empty reads.
|
|
802
|
+
if (!isTTY && !tokenInput) break;
|
|
803
|
+
}
|
|
804
|
+
console.error(" No valid token provided — aborting");
|
|
805
|
+
return null;
|
|
769
806
|
}
|
|
770
807
|
|
|
771
808
|
// [2/4] Primary profile
|
|
@@ -796,12 +833,16 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
796
833
|
const pwdEnvPath = join(process.cwd(), ".env.local");
|
|
797
834
|
const pwdRechPath = join(process.cwd(), ".rechrome", ".env.local");
|
|
798
835
|
const homeEnvPath = join(process.env.HOME!, ".env.local");
|
|
836
|
+
// Show whether each target already exists so it's clear we'll update (merge) vs create.
|
|
837
|
+
const tag = async (p: string) => (await file(p).exists()) ? "exists → will update" : "new file";
|
|
838
|
+
const [pwdTag, pwdRechTag, homeTag] = await Promise.all([tag(pwdEnvPath), tag(pwdRechPath), tag(homeEnvPath)]);
|
|
799
839
|
const saveChoice = (await ask(
|
|
800
|
-
`Save to:\n 1. ${pwdEnvPath} (current dir) [default]\n 2. ${pwdRechPath} (current dir, rechrome-only)\n 3. ${homeEnvPath} (user home)\n 4. Skip (already copied)\n\n Choice [1]: `
|
|
840
|
+
`Save to:\n 1. ${pwdEnvPath} (current dir) [${pwdTag}] [default]\n 2. ${pwdRechPath} (current dir, rechrome-only) [${pwdRechTag}]\n 3. ${homeEnvPath} (user home) [${homeTag}]\n 4. Skip (already copied)\n\n Choice [1]: `
|
|
801
841
|
)).trim();
|
|
802
842
|
if (saveChoice !== "4") {
|
|
803
843
|
const globalEnvPath = saveChoice === "3" ? homeEnvPath : saveChoice === "2" ? pwdRechPath : pwdEnvPath;
|
|
804
844
|
if (saveChoice === "2") mkdirSync(join(process.cwd(), ".rechrome"), { recursive: true });
|
|
845
|
+
const existedBefore = await file(globalEnvPath).exists();
|
|
805
846
|
const existing = await file(globalEnvPath).text().catch(() => "");
|
|
806
847
|
const keysToRemove = ["PLAYWRIGHT_MCP_USER_DATA_DIR", "PLAYWRIGHT_MCP_EXTENSION_ID", "PLAYWRIGHT_MCP_EXTENSION_TOKEN", "PLAYWRIGHT_MCP_PROFILE_DIRECTORY"];
|
|
807
848
|
let lines = existing.trimEnd().split("\n").filter(l => !keysToRemove.some(k => l.startsWith(`${k}=`)));
|
|
@@ -809,7 +850,7 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
809
850
|
if (rechIdx >= 0) lines[rechIdx] = newLine;
|
|
810
851
|
else lines.push(newLine);
|
|
811
852
|
await Bun.write(globalEnvPath, lines.join("\n").trim() + "\n");
|
|
812
|
-
console.log(`\
|
|
853
|
+
console.log(`\n${existedBefore ? "Updated" : "Created"} ${globalEnvPath}`);
|
|
813
854
|
}
|
|
814
855
|
|
|
815
856
|
// Save primary to token registry
|
|
@@ -837,7 +878,7 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
|
|
|
837
878
|
}
|
|
838
879
|
rl?.close();
|
|
839
880
|
envWatcher?.close();
|
|
840
|
-
console.log(`\nDone! Test with:\n rech
|
|
881
|
+
console.log(`\nDone! Test with:\n rech open github.com/snomiao`);
|
|
841
882
|
}
|
|
842
883
|
|
|
843
884
|
async function status(): Promise<void> {
|