rechrome 1.31.0 → 1.31.2
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/package.json +1 -1
- package/rechrome.js +23 -3
- package/rechrome.ts +23 -3
- package/serve.js +33 -15
- package/serve.ts +33 -15
package/package.json
CHANGED
package/rechrome.js
CHANGED
|
@@ -1922,14 +1922,31 @@ async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<str
|
|
|
1922
1922
|
daemonManager();
|
|
1923
1923
|
}
|
|
1924
1924
|
|
|
1925
|
+
/**
|
|
1926
|
+
* Rewriting RECHROME_URL for a new daemon key must not drop the saved `?profile=`: without it
|
|
1927
|
+
* every client loses its Chrome profile (and a setup aborted before its final save step used
|
|
1928
|
+
* to leave it that way).
|
|
1929
|
+
*/
|
|
1930
|
+
export function keepProfileParam(next: string, previous?: string): string {
|
|
1931
|
+
try {
|
|
1932
|
+
const url = new URL(next);
|
|
1933
|
+
// A .env.local value may be quoted: RECHROME_URL="http://…/?profile=work".
|
|
1934
|
+
const raw = previous?.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
1935
|
+
const profile = raw ? new URL(raw).searchParams.get("profile") : null;
|
|
1936
|
+
if (profile && !url.searchParams.has("profile")) url.searchParams.set("profile", profile);
|
|
1937
|
+
return url.toString();
|
|
1938
|
+
} catch { return next; }
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1925
1941
|
export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
1926
1942
|
// Resolve the manager first so a missing dependency fails before config is mutated.
|
|
1927
1943
|
const mgr = daemonManager();
|
|
1928
1944
|
// Persist the URL for future clients without an explicit environment override.
|
|
1929
1945
|
// The daemon's explicit environment takes precedence over this saved default.
|
|
1930
1946
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1947
|
+
const lines = envRaw.trimEnd().split("\n");
|
|
1948
|
+
const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
1949
|
+
await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))}`, ""].join("\n"));
|
|
1933
1950
|
|
|
1934
1951
|
const home = HOME;
|
|
1935
1952
|
const bunBin = Bun.which("bun") ?? process.execPath;
|
|
@@ -1990,13 +2007,16 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1990
2007
|
await runPm(mgr, ["save"]); // persist process list for `pm2 resurrect` on reboot
|
|
1991
2008
|
} else {
|
|
1992
2009
|
const envArgs = Object.entries(daemonEnv).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
2010
|
+
// oxmgr shell-splits the command string (backslashes are escapes, so `C:\Users\…` became
|
|
2011
|
+
// `C:Users…`); pass quoted forward-slash paths — Windows accepts `/`, quotes keep spaces.
|
|
2012
|
+
const q = (path: string) => `"${path.replaceAll("\\", "/")}"`;
|
|
1993
2013
|
startCode = await runPm(mgr, [
|
|
1994
2014
|
"start",
|
|
1995
2015
|
"--name", PM_PROCESS_NAME,
|
|
1996
2016
|
"--restart", "always",
|
|
1997
2017
|
"--cwd", home,
|
|
1998
2018
|
...envArgs,
|
|
1999
|
-
`${bunBin} ${rechScript} serve`,
|
|
2019
|
+
`${q(bunBin)} ${q(rechScript)} serve`,
|
|
2000
2020
|
]);
|
|
2001
2021
|
// Boot/login persistence: on Windows the winfix oxmgr wires Task Scheduler,
|
|
2002
2022
|
// on POSIX a systemd --user unit / launchd agent — the equivalent of the pm2
|
package/rechrome.ts
CHANGED
|
@@ -1922,14 +1922,31 @@ async function ensureDaemonManager(ask: (q: string, def?: string) => Promise<str
|
|
|
1922
1922
|
daemonManager();
|
|
1923
1923
|
}
|
|
1924
1924
|
|
|
1925
|
+
/**
|
|
1926
|
+
* Rewriting RECHROME_URL for a new daemon key must not drop the saved `?profile=`: without it
|
|
1927
|
+
* every client loses its Chrome profile (and a setup aborted before its final save step used
|
|
1928
|
+
* to leave it that way).
|
|
1929
|
+
*/
|
|
1930
|
+
export function keepProfileParam(next: string, previous?: string): string {
|
|
1931
|
+
try {
|
|
1932
|
+
const url = new URL(next);
|
|
1933
|
+
// A .env.local value may be quoted: RECHROME_URL="http://…/?profile=work".
|
|
1934
|
+
const raw = previous?.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
1935
|
+
const profile = raw ? new URL(raw).searchParams.get("profile") : null;
|
|
1936
|
+
if (profile && !url.searchParams.has("profile")) url.searchParams.set("profile", profile);
|
|
1937
|
+
return url.toString();
|
|
1938
|
+
} catch { return next; }
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1925
1941
|
export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
1926
1942
|
// Resolve the manager first so a missing dependency fails before config is mutated.
|
|
1927
1943
|
const mgr = daemonManager();
|
|
1928
1944
|
// Persist the URL for future clients without an explicit environment override.
|
|
1929
1945
|
// The daemon's explicit environment takes precedence over this saved default.
|
|
1930
1946
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1947
|
+
const lines = envRaw.trimEnd().split("\n");
|
|
1948
|
+
const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
1949
|
+
await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))}`, ""].join("\n"));
|
|
1933
1950
|
|
|
1934
1951
|
const home = HOME;
|
|
1935
1952
|
const bunBin = Bun.which("bun") ?? process.execPath;
|
|
@@ -1990,13 +2007,16 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
1990
2007
|
await runPm(mgr, ["save"]); // persist process list for `pm2 resurrect` on reboot
|
|
1991
2008
|
} else {
|
|
1992
2009
|
const envArgs = Object.entries(daemonEnv).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
2010
|
+
// oxmgr shell-splits the command string (backslashes are escapes, so `C:\Users\…` became
|
|
2011
|
+
// `C:Users…`); pass quoted forward-slash paths — Windows accepts `/`, quotes keep spaces.
|
|
2012
|
+
const q = (path: string) => `"${path.replaceAll("\\", "/")}"`;
|
|
1993
2013
|
startCode = await runPm(mgr, [
|
|
1994
2014
|
"start",
|
|
1995
2015
|
"--name", PM_PROCESS_NAME,
|
|
1996
2016
|
"--restart", "always",
|
|
1997
2017
|
"--cwd", home,
|
|
1998
2018
|
...envArgs,
|
|
1999
|
-
`${bunBin} ${rechScript} serve`,
|
|
2019
|
+
`${q(bunBin)} ${q(rechScript)} serve`,
|
|
2000
2020
|
]);
|
|
2001
2021
|
// Boot/login persistence: on Windows the winfix oxmgr wires Task Scheduler,
|
|
2002
2022
|
// on POSIX a systemd --user unit / launchd agent — the equivalent of the pm2
|
package/serve.js
CHANGED
|
@@ -691,6 +691,39 @@ export async function serve() {
|
|
|
691
691
|
const isOpenNoUrl = isOpenCmd && filteredArgs.length === 1;
|
|
692
692
|
if (isOpenNoUrl) filteredArgs.push("about:blank");
|
|
693
693
|
|
|
694
|
+
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
695
|
+
const passthroughEnv: Record<string, string | undefined> = {};
|
|
696
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
697
|
+
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
698
|
+
}
|
|
699
|
+
Object.assign(passthroughEnv, clientEnv, profileEnv);
|
|
700
|
+
|
|
701
|
+
// Resolve profile name/email → directory name
|
|
702
|
+
if (!scoped && passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
703
|
+
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
704
|
+
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
705
|
+
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
706
|
+
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Never fall back silently: without extension credentials (a registered profile) or a managed
|
|
710
|
+
// user-data dir, `open` would launch a separate headless browser with a throwaway profile —
|
|
711
|
+
// the command "succeeds" and nothing shows up in the user's Chrome.
|
|
712
|
+
const extensionMode = !!(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN);
|
|
713
|
+
// Checked before the existing-session shortcuts below: reusing a leftover headless session
|
|
714
|
+
// (bare `open` answered with its tab list, or `open <url>` rewritten to goto) is the same
|
|
715
|
+
// silent fallback.
|
|
716
|
+
if (isOpenCmd && !extensionMode && !passthroughEnv.PLAYWRIGHT_MCP_USER_DATA_DIR) {
|
|
717
|
+
const requested = passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY;
|
|
718
|
+
const registered = Object.keys(await readTokenRegistry().catch(() => ({})));
|
|
719
|
+
const known = registered.length ? `Registered here: ${registered.join(", ")}` : "No profiles are registered here yet: rech setup";
|
|
720
|
+
const stderr = requested
|
|
721
|
+
? `[rech] profile "${requested}" has no extension token on this machine, so rech would open a separate headless browser instead of your Chrome. Refusing.\n Register it: rech setup --profile ${JSON.stringify(requested)}\n ${known}\n`
|
|
722
|
+
: `[rech] no Chrome profile selected, so rech would open a separate headless browser instead of your Chrome. Refusing.\n Pick one: rech --profile <email> open <url>, or add ?profile=<email> to RECHROME_URL\n ${known}\n`;
|
|
723
|
+
log(`refused open without a profile (session=${namespacedSession}, requested=${requested ?? "-"})`);
|
|
724
|
+
return Response.json({ status: 1, stdout: "", stderr, files: [] });
|
|
725
|
+
}
|
|
726
|
+
|
|
694
727
|
// open against an existing session: bare `open` returns a tab-list hint; `open <url>`
|
|
695
728
|
// converts to `goto` to reuse the live browser. (Guarding on filteredArgs.length===1
|
|
696
729
|
// was dead — about:blank/<url> is already appended above, so length is always >=2.)
|
|
@@ -730,21 +763,6 @@ export async function serve() {
|
|
|
730
763
|
}
|
|
731
764
|
}
|
|
732
765
|
|
|
733
|
-
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
734
|
-
const passthroughEnv: Record<string, string | undefined> = {};
|
|
735
|
-
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
736
|
-
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
737
|
-
}
|
|
738
|
-
Object.assign(passthroughEnv, clientEnv, profileEnv);
|
|
739
|
-
|
|
740
|
-
// Resolve profile name/email → directory name
|
|
741
|
-
if (!scoped && passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
742
|
-
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
743
|
-
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
744
|
-
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
745
|
-
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
766
|
const childEnv: Record<string, string | undefined> = {
|
|
749
767
|
PATH: process.env.PATH,
|
|
750
768
|
HOME: HOME,
|
package/serve.ts
CHANGED
|
@@ -691,6 +691,39 @@ export async function serve() {
|
|
|
691
691
|
const isOpenNoUrl = isOpenCmd && filteredArgs.length === 1;
|
|
692
692
|
if (isOpenNoUrl) filteredArgs.push("about:blank");
|
|
693
693
|
|
|
694
|
+
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
695
|
+
const passthroughEnv: Record<string, string | undefined> = {};
|
|
696
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
697
|
+
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
698
|
+
}
|
|
699
|
+
Object.assign(passthroughEnv, clientEnv, profileEnv);
|
|
700
|
+
|
|
701
|
+
// Resolve profile name/email → directory name
|
|
702
|
+
if (!scoped && passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
703
|
+
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
704
|
+
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
705
|
+
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
706
|
+
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Never fall back silently: without extension credentials (a registered profile) or a managed
|
|
710
|
+
// user-data dir, `open` would launch a separate headless browser with a throwaway profile —
|
|
711
|
+
// the command "succeeds" and nothing shows up in the user's Chrome.
|
|
712
|
+
const extensionMode = !!(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN);
|
|
713
|
+
// Checked before the existing-session shortcuts below: reusing a leftover headless session
|
|
714
|
+
// (bare `open` answered with its tab list, or `open <url>` rewritten to goto) is the same
|
|
715
|
+
// silent fallback.
|
|
716
|
+
if (isOpenCmd && !extensionMode && !passthroughEnv.PLAYWRIGHT_MCP_USER_DATA_DIR) {
|
|
717
|
+
const requested = passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY;
|
|
718
|
+
const registered = Object.keys(await readTokenRegistry().catch(() => ({})));
|
|
719
|
+
const known = registered.length ? `Registered here: ${registered.join(", ")}` : "No profiles are registered here yet: rech setup";
|
|
720
|
+
const stderr = requested
|
|
721
|
+
? `[rech] profile "${requested}" has no extension token on this machine, so rech would open a separate headless browser instead of your Chrome. Refusing.\n Register it: rech setup --profile ${JSON.stringify(requested)}\n ${known}\n`
|
|
722
|
+
: `[rech] no Chrome profile selected, so rech would open a separate headless browser instead of your Chrome. Refusing.\n Pick one: rech --profile <email> open <url>, or add ?profile=<email> to RECHROME_URL\n ${known}\n`;
|
|
723
|
+
log(`refused open without a profile (session=${namespacedSession}, requested=${requested ?? "-"})`);
|
|
724
|
+
return Response.json({ status: 1, stdout: "", stderr, files: [] });
|
|
725
|
+
}
|
|
726
|
+
|
|
694
727
|
// open against an existing session: bare `open` returns a tab-list hint; `open <url>`
|
|
695
728
|
// converts to `goto` to reuse the live browser. (Guarding on filteredArgs.length===1
|
|
696
729
|
// was dead — about:blank/<url> is already appended above, so length is always >=2.)
|
|
@@ -730,21 +763,6 @@ export async function serve() {
|
|
|
730
763
|
}
|
|
731
764
|
}
|
|
732
765
|
|
|
733
|
-
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
734
|
-
const passthroughEnv: Record<string, string | undefined> = {};
|
|
735
|
-
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
736
|
-
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
737
|
-
}
|
|
738
|
-
Object.assign(passthroughEnv, clientEnv, profileEnv);
|
|
739
|
-
|
|
740
|
-
// Resolve profile name/email → directory name
|
|
741
|
-
if (!scoped && passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
742
|
-
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
743
|
-
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
744
|
-
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
745
|
-
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
766
|
const childEnv: Record<string, string | undefined> = {
|
|
749
767
|
PATH: process.env.PATH,
|
|
750
768
|
HOME: HOME,
|