rechrome 1.5.0 → 1.7.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/README.md +40 -20
- package/package.json +3 -3
- package/rech.js +608 -65
- package/rech.ts +608 -65
- package/serve.js +112 -13
- package/serve.ts +112 -13
package/serve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { file } from "bun";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
|
-
import { mkdirSync } from "fs";
|
|
2
|
+
import { createHash, X509Certificate } from "crypto";
|
|
3
|
+
import { mkdirSync, unlinkSync, accessSync, constants as fsConstants } from "fs";
|
|
4
4
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
5
5
|
import {
|
|
6
6
|
log,
|
|
@@ -11,12 +11,59 @@ import {
|
|
|
11
11
|
PASSTHROUGH_ENV_KEYS,
|
|
12
12
|
} from "./rech.js";
|
|
13
13
|
|
|
14
|
+
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
15
|
+
const CERT_RENEW_THRESHOLD_DAYS = 7;
|
|
16
|
+
|
|
17
|
+
async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
|
|
18
|
+
const certContent = await file(certPath).text().catch(() => null);
|
|
19
|
+
if (!certContent) return false;
|
|
20
|
+
try {
|
|
21
|
+
const cert = new X509Certificate(certContent);
|
|
22
|
+
const daysLeft = (new Date(cert.validTo).getTime() - Date.now()) / 86_400_000;
|
|
23
|
+
if (daysLeft > CERT_RENEW_THRESHOLD_DAYS) return false;
|
|
24
|
+
const domain = cert.subjectAltName?.match(/DNS:([^\s,]+)/)?.[1];
|
|
25
|
+
if (!domain) { log("TLS cert renewal: could not determine domain"); return false; }
|
|
26
|
+
log(`TLS cert expires in ${Math.floor(daysLeft)} days, renewing ${domain}...`);
|
|
27
|
+
const proc = Bun.spawn([TAILSCALE_BIN, "cert", "--cert-file", certPath, "--key-file", keyPath, domain], {
|
|
28
|
+
stdout: "pipe", stderr: "pipe",
|
|
29
|
+
});
|
|
30
|
+
const [status, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
31
|
+
if (status !== 0) { log(`TLS cert renewal failed: ${stderr.trim()}`); return false; }
|
|
32
|
+
log(`TLS cert renewed for ${domain}`);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
log(`TLS cert check error: ${e}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
15
41
|
const absBase = resolve(base) + "/";
|
|
16
42
|
const absCandidate = resolve(base, candidate);
|
|
17
43
|
return absCandidate.startsWith(absBase);
|
|
18
44
|
}
|
|
19
45
|
|
|
46
|
+
async function resolveProfileDirectory(nameOrEmail: string): Promise<string> {
|
|
47
|
+
if (/^(Default|Profile \d+)$/i.test(nameOrEmail)) return nameOrEmail;
|
|
48
|
+
const home = process.env.HOME || "~";
|
|
49
|
+
const candidates = [
|
|
50
|
+
join(home, "Library/Application Support/Google/Chrome/Local State"),
|
|
51
|
+
join(home, ".config/google-chrome/Local State"),
|
|
52
|
+
join(home, "AppData/Local/Google/Chrome/User Data/Local State"),
|
|
53
|
+
];
|
|
54
|
+
for (const statePath of candidates) {
|
|
55
|
+
const f = file(statePath);
|
|
56
|
+
if (!(await f.exists())) continue;
|
|
57
|
+
const data = JSON.parse(await f.text());
|
|
58
|
+
const cache: Record<string, any> = data?.profile?.info_cache ?? {};
|
|
59
|
+
for (const [dir, info] of Object.entries(cache)) {
|
|
60
|
+
if ([info.name, info.user_name, info.gaia_name].includes(nameOrEmail))
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return nameOrEmail;
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
export async function serve() {
|
|
21
68
|
const url = await getOrCreateUrl();
|
|
22
69
|
const { key, port } = parseUrl(url);
|
|
@@ -25,9 +72,26 @@ export async function serve() {
|
|
|
25
72
|
mkdirSync(workDir, { recursive: true });
|
|
26
73
|
|
|
27
74
|
const listenHost = process.env.RECH_HOST || "127.0.0.1";
|
|
75
|
+
const canRead = (p?: string) => { try { accessSync(p!, fsConstants.R_OK); return true; } catch { return false; } };
|
|
76
|
+
const certPath = canRead(process.env.RECH_TLS_CERT) ? process.env.RECH_TLS_CERT : undefined;
|
|
77
|
+
const keyPath = canRead(process.env.RECH_TLS_KEY) ? process.env.RECH_TLS_KEY : undefined;
|
|
78
|
+
if (certPath && keyPath) {
|
|
79
|
+
const renewed = await renewCertIfNeeded(certPath, keyPath);
|
|
80
|
+
if (renewed) { log("Restarting to load renewed TLS cert..."); process.exit(0); }
|
|
81
|
+
// Check daily; pm2 restarts cleanly after exit(0)
|
|
82
|
+
setInterval(async () => {
|
|
83
|
+
if (await renewCertIfNeeded(certPath, keyPath)) { log("Restarting to load renewed TLS cert..."); process.exit(0); }
|
|
84
|
+
}, 86_400_000);
|
|
85
|
+
}
|
|
86
|
+
const tls = certPath && keyPath ? { cert: Bun.file(certPath), key: Bun.file(keyPath) } : undefined;
|
|
28
87
|
const server = Bun.serve({
|
|
29
88
|
hostname: listenHost,
|
|
30
89
|
port,
|
|
90
|
+
tls,
|
|
91
|
+
error(err) {
|
|
92
|
+
log(`unhandled error: ${err.message}`);
|
|
93
|
+
return Response.json({ status: 1, stdout: "", stderr: err.message }, { status: 500 });
|
|
94
|
+
},
|
|
31
95
|
async fetch(req) {
|
|
32
96
|
const reqUrl = new URL(req.url);
|
|
33
97
|
|
|
@@ -55,22 +119,23 @@ export async function serve() {
|
|
|
55
119
|
if (Array.isArray(body)) {
|
|
56
120
|
args = body;
|
|
57
121
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
58
|
-
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0,
|
|
122
|
+
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0, 8);
|
|
59
123
|
clientName = clientAddr;
|
|
60
124
|
log(`session from client IP: ${clientAddr} -> ${sessionId}`);
|
|
61
125
|
} else {
|
|
62
126
|
args = body.args;
|
|
63
127
|
const id = body.identity as
|
|
64
|
-
| { gitUrl?: string; hostname?: string; cwd?: string }
|
|
128
|
+
| { gitUrl?: string; hostname?: string; cwd?: string; profile?: string }
|
|
65
129
|
| undefined;
|
|
66
|
-
const
|
|
130
|
+
const baseId = id?.gitUrl || (id?.hostname && id?.cwd ? `${id.hostname}:${id.cwd}` : null);
|
|
131
|
+
const raw = baseId && id?.profile ? `${baseId}@${id.profile}` : baseId;
|
|
67
132
|
if (raw) {
|
|
68
|
-
sessionId = createHash("sha256").update(raw).digest("hex").slice(0,
|
|
133
|
+
sessionId = createHash("sha256").update(raw).digest("hex").slice(0, 8);
|
|
69
134
|
clientName = raw;
|
|
70
135
|
log(`session from identity: ${raw} -> ${sessionId}`);
|
|
71
136
|
} else {
|
|
72
137
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
73
|
-
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0,
|
|
138
|
+
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0, 8);
|
|
74
139
|
clientName = clientAddr;
|
|
75
140
|
log(`session from client IP fallback: ${clientAddr} -> ${sessionId}`);
|
|
76
141
|
}
|
|
@@ -101,11 +166,15 @@ export async function serve() {
|
|
|
101
166
|
|
|
102
167
|
log(`run: rech ${filteredArgs.join(" ")} (session=${namespacedSession})`);
|
|
103
168
|
|
|
104
|
-
// For open commands,
|
|
169
|
+
// For open commands, default to about:blank to avoid leaving connect.html visible
|
|
105
170
|
const isOpenCmd = filteredArgs[0] === "open";
|
|
106
|
-
if (isOpenCmd)
|
|
171
|
+
if (isOpenCmd && filteredArgs.length === 1)
|
|
172
|
+
filteredArgs.push("about:blank");
|
|
173
|
+
|
|
174
|
+
// bare `rech open` with no URL: warn if session already has tabs
|
|
175
|
+
if (isOpenCmd && filteredArgs.length === 1) {
|
|
107
176
|
try {
|
|
108
|
-
const listProc = Bun.spawn([bin, ...binArgs, "tab-list",
|
|
177
|
+
const listProc = Bun.spawn([bin, ...binArgs, "tab-list", `-s=${namespacedSession}`], {
|
|
109
178
|
cwd: workDir,
|
|
110
179
|
stdin: "ignore",
|
|
111
180
|
stdout: "pipe",
|
|
@@ -138,6 +207,14 @@ export async function serve() {
|
|
|
138
207
|
}
|
|
139
208
|
Object.assign(passthroughEnv, clientEnv);
|
|
140
209
|
|
|
210
|
+
// Resolve profile name/email → directory name
|
|
211
|
+
if (passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
212
|
+
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
213
|
+
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
214
|
+
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
215
|
+
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
216
|
+
}
|
|
217
|
+
|
|
141
218
|
const childEnv: Record<string, string | undefined> = {
|
|
142
219
|
PATH: process.env.PATH,
|
|
143
220
|
HOME: process.env.HOME,
|
|
@@ -146,8 +223,30 @@ export async function serve() {
|
|
|
146
223
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
147
224
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
148
225
|
...passthroughEnv,
|
|
226
|
+
// Enable extension bridge when credentials are present
|
|
227
|
+
...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN
|
|
228
|
+
? { PLAYWRIGHT_MCP_EXTENSION: "1" }
|
|
229
|
+
: {}),
|
|
149
230
|
};
|
|
150
|
-
|
|
231
|
+
// For open commands: clean up stale sockets so a closed browser can be reopened
|
|
232
|
+
if (isOpenCmd) {
|
|
233
|
+
const tmpDir = (process.env.TMPDIR || "/tmp").replace(/\/$/, "");
|
|
234
|
+
const playwrightTmpDir = `${tmpDir}/playwright-cli`;
|
|
235
|
+
try {
|
|
236
|
+
const { readdirSync } = await import("fs");
|
|
237
|
+
for (const sub of readdirSync(playwrightTmpDir)) {
|
|
238
|
+
const subDir = `${playwrightTmpDir}/${sub}`;
|
|
239
|
+
for (const f of readdirSync(subDir)) {
|
|
240
|
+
if (f.startsWith(namespacedSession)) {
|
|
241
|
+
const sockPath = `${subDir}/${f}`;
|
|
242
|
+
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const proc = Bun.spawn([bin, ...binArgs, ...filteredArgs, `-s=${namespacedSession}`], {
|
|
151
250
|
cwd: workDir,
|
|
152
251
|
stdin: "ignore",
|
|
153
252
|
stdout: "pipe",
|
|
@@ -171,7 +270,7 @@ export async function serve() {
|
|
|
171
270
|
timeout.then(() => [1, "", ""] as [number, string, string]),
|
|
172
271
|
]).catch(
|
|
173
272
|
() => [1, "", `Command timed out after ${TIMEOUT / 1000}s\n`] as [number, string, string],
|
|
174
|
-
);
|
|
273
|
+
) as [number, string, string];
|
|
175
274
|
|
|
176
275
|
log(`exit: ${status}${stdout.trim() ? ` | ${stdout.trim().slice(0, 200)}` : ""}`);
|
|
177
276
|
|
|
@@ -209,6 +308,6 @@ export async function serve() {
|
|
|
209
308
|
},
|
|
210
309
|
});
|
|
211
310
|
|
|
212
|
-
log(`serving on http://${server.hostname}:${server.port}`);
|
|
311
|
+
log(`serving on ${tls ? "https" : "http"}://${server.hostname}:${server.port}`);
|
|
213
312
|
log(`Connection URL set (use .env.local to view)`);
|
|
214
313
|
}
|
package/serve.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { file } from "bun";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
|
-
import { mkdirSync } from "fs";
|
|
2
|
+
import { createHash, X509Certificate } from "crypto";
|
|
3
|
+
import { mkdirSync, unlinkSync, accessSync, constants as fsConstants } from "fs";
|
|
4
4
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
5
5
|
import {
|
|
6
6
|
log,
|
|
@@ -11,12 +11,59 @@ import {
|
|
|
11
11
|
PASSTHROUGH_ENV_KEYS,
|
|
12
12
|
} from "./rech.ts";
|
|
13
13
|
|
|
14
|
+
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
15
|
+
const CERT_RENEW_THRESHOLD_DAYS = 7;
|
|
16
|
+
|
|
17
|
+
async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
|
|
18
|
+
const certContent = await file(certPath).text().catch(() => null);
|
|
19
|
+
if (!certContent) return false;
|
|
20
|
+
try {
|
|
21
|
+
const cert = new X509Certificate(certContent);
|
|
22
|
+
const daysLeft = (new Date(cert.validTo).getTime() - Date.now()) / 86_400_000;
|
|
23
|
+
if (daysLeft > CERT_RENEW_THRESHOLD_DAYS) return false;
|
|
24
|
+
const domain = cert.subjectAltName?.match(/DNS:([^\s,]+)/)?.[1];
|
|
25
|
+
if (!domain) { log("TLS cert renewal: could not determine domain"); return false; }
|
|
26
|
+
log(`TLS cert expires in ${Math.floor(daysLeft)} days, renewing ${domain}...`);
|
|
27
|
+
const proc = Bun.spawn([TAILSCALE_BIN, "cert", "--cert-file", certPath, "--key-file", keyPath, domain], {
|
|
28
|
+
stdout: "pipe", stderr: "pipe",
|
|
29
|
+
});
|
|
30
|
+
const [status, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
31
|
+
if (status !== 0) { log(`TLS cert renewal failed: ${stderr.trim()}`); return false; }
|
|
32
|
+
log(`TLS cert renewed for ${domain}`);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
log(`TLS cert check error: ${e}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
15
41
|
const absBase = resolve(base) + "/";
|
|
16
42
|
const absCandidate = resolve(base, candidate);
|
|
17
43
|
return absCandidate.startsWith(absBase);
|
|
18
44
|
}
|
|
19
45
|
|
|
46
|
+
async function resolveProfileDirectory(nameOrEmail: string): Promise<string> {
|
|
47
|
+
if (/^(Default|Profile \d+)$/i.test(nameOrEmail)) return nameOrEmail;
|
|
48
|
+
const home = process.env.HOME || "~";
|
|
49
|
+
const candidates = [
|
|
50
|
+
join(home, "Library/Application Support/Google/Chrome/Local State"),
|
|
51
|
+
join(home, ".config/google-chrome/Local State"),
|
|
52
|
+
join(home, "AppData/Local/Google/Chrome/User Data/Local State"),
|
|
53
|
+
];
|
|
54
|
+
for (const statePath of candidates) {
|
|
55
|
+
const f = file(statePath);
|
|
56
|
+
if (!(await f.exists())) continue;
|
|
57
|
+
const data = JSON.parse(await f.text());
|
|
58
|
+
const cache: Record<string, any> = data?.profile?.info_cache ?? {};
|
|
59
|
+
for (const [dir, info] of Object.entries(cache)) {
|
|
60
|
+
if ([info.name, info.user_name, info.gaia_name].includes(nameOrEmail))
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return nameOrEmail;
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
export async function serve() {
|
|
21
68
|
const url = await getOrCreateUrl();
|
|
22
69
|
const { key, port } = parseUrl(url);
|
|
@@ -25,9 +72,26 @@ export async function serve() {
|
|
|
25
72
|
mkdirSync(workDir, { recursive: true });
|
|
26
73
|
|
|
27
74
|
const listenHost = process.env.RECH_HOST || "127.0.0.1";
|
|
75
|
+
const canRead = (p?: string) => { try { accessSync(p!, fsConstants.R_OK); return true; } catch { return false; } };
|
|
76
|
+
const certPath = canRead(process.env.RECH_TLS_CERT) ? process.env.RECH_TLS_CERT : undefined;
|
|
77
|
+
const keyPath = canRead(process.env.RECH_TLS_KEY) ? process.env.RECH_TLS_KEY : undefined;
|
|
78
|
+
if (certPath && keyPath) {
|
|
79
|
+
const renewed = await renewCertIfNeeded(certPath, keyPath);
|
|
80
|
+
if (renewed) { log("Restarting to load renewed TLS cert..."); process.exit(0); }
|
|
81
|
+
// Check daily; pm2 restarts cleanly after exit(0)
|
|
82
|
+
setInterval(async () => {
|
|
83
|
+
if (await renewCertIfNeeded(certPath, keyPath)) { log("Restarting to load renewed TLS cert..."); process.exit(0); }
|
|
84
|
+
}, 86_400_000);
|
|
85
|
+
}
|
|
86
|
+
const tls = certPath && keyPath ? { cert: Bun.file(certPath), key: Bun.file(keyPath) } : undefined;
|
|
28
87
|
const server = Bun.serve({
|
|
29
88
|
hostname: listenHost,
|
|
30
89
|
port,
|
|
90
|
+
tls,
|
|
91
|
+
error(err) {
|
|
92
|
+
log(`unhandled error: ${err.message}`);
|
|
93
|
+
return Response.json({ status: 1, stdout: "", stderr: err.message }, { status: 500 });
|
|
94
|
+
},
|
|
31
95
|
async fetch(req) {
|
|
32
96
|
const reqUrl = new URL(req.url);
|
|
33
97
|
|
|
@@ -55,22 +119,23 @@ export async function serve() {
|
|
|
55
119
|
if (Array.isArray(body)) {
|
|
56
120
|
args = body;
|
|
57
121
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
58
|
-
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0,
|
|
122
|
+
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0, 8);
|
|
59
123
|
clientName = clientAddr;
|
|
60
124
|
log(`session from client IP: ${clientAddr} -> ${sessionId}`);
|
|
61
125
|
} else {
|
|
62
126
|
args = body.args;
|
|
63
127
|
const id = body.identity as
|
|
64
|
-
| { gitUrl?: string; hostname?: string; cwd?: string }
|
|
128
|
+
| { gitUrl?: string; hostname?: string; cwd?: string; profile?: string }
|
|
65
129
|
| undefined;
|
|
66
|
-
const
|
|
130
|
+
const baseId = id?.gitUrl || (id?.hostname && id?.cwd ? `${id.hostname}:${id.cwd}` : null);
|
|
131
|
+
const raw = baseId && id?.profile ? `${baseId}@${id.profile}` : baseId;
|
|
67
132
|
if (raw) {
|
|
68
|
-
sessionId = createHash("sha256").update(raw).digest("hex").slice(0,
|
|
133
|
+
sessionId = createHash("sha256").update(raw).digest("hex").slice(0, 8);
|
|
69
134
|
clientName = raw;
|
|
70
135
|
log(`session from identity: ${raw} -> ${sessionId}`);
|
|
71
136
|
} else {
|
|
72
137
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
73
|
-
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0,
|
|
138
|
+
sessionId = createHash("sha256").update(clientAddr).digest("hex").slice(0, 8);
|
|
74
139
|
clientName = clientAddr;
|
|
75
140
|
log(`session from client IP fallback: ${clientAddr} -> ${sessionId}`);
|
|
76
141
|
}
|
|
@@ -101,11 +166,15 @@ export async function serve() {
|
|
|
101
166
|
|
|
102
167
|
log(`run: rech ${filteredArgs.join(" ")} (session=${namespacedSession})`);
|
|
103
168
|
|
|
104
|
-
// For open commands,
|
|
169
|
+
// For open commands, default to about:blank to avoid leaving connect.html visible
|
|
105
170
|
const isOpenCmd = filteredArgs[0] === "open";
|
|
106
|
-
if (isOpenCmd)
|
|
171
|
+
if (isOpenCmd && filteredArgs.length === 1)
|
|
172
|
+
filteredArgs.push("about:blank");
|
|
173
|
+
|
|
174
|
+
// bare `rech open` with no URL: warn if session already has tabs
|
|
175
|
+
if (isOpenCmd && filteredArgs.length === 1) {
|
|
107
176
|
try {
|
|
108
|
-
const listProc = Bun.spawn([bin, ...binArgs, "tab-list",
|
|
177
|
+
const listProc = Bun.spawn([bin, ...binArgs, "tab-list", `-s=${namespacedSession}`], {
|
|
109
178
|
cwd: workDir,
|
|
110
179
|
stdin: "ignore",
|
|
111
180
|
stdout: "pipe",
|
|
@@ -138,6 +207,14 @@ export async function serve() {
|
|
|
138
207
|
}
|
|
139
208
|
Object.assign(passthroughEnv, clientEnv);
|
|
140
209
|
|
|
210
|
+
// Resolve profile name/email → directory name
|
|
211
|
+
if (passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY) {
|
|
212
|
+
const resolved = await resolveProfileDirectory(passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY);
|
|
213
|
+
if (resolved !== passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY)
|
|
214
|
+
log(`profile resolved: "${passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY}" → "${resolved}"`);
|
|
215
|
+
passthroughEnv.PLAYWRIGHT_MCP_PROFILE_DIRECTORY = resolved;
|
|
216
|
+
}
|
|
217
|
+
|
|
141
218
|
const childEnv: Record<string, string | undefined> = {
|
|
142
219
|
PATH: process.env.PATH,
|
|
143
220
|
HOME: process.env.HOME,
|
|
@@ -146,8 +223,30 @@ export async function serve() {
|
|
|
146
223
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
147
224
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
148
225
|
...passthroughEnv,
|
|
226
|
+
// Enable extension bridge when credentials are present
|
|
227
|
+
...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN
|
|
228
|
+
? { PLAYWRIGHT_MCP_EXTENSION: "1" }
|
|
229
|
+
: {}),
|
|
149
230
|
};
|
|
150
|
-
|
|
231
|
+
// For open commands: clean up stale sockets so a closed browser can be reopened
|
|
232
|
+
if (isOpenCmd) {
|
|
233
|
+
const tmpDir = (process.env.TMPDIR || "/tmp").replace(/\/$/, "");
|
|
234
|
+
const playwrightTmpDir = `${tmpDir}/playwright-cli`;
|
|
235
|
+
try {
|
|
236
|
+
const { readdirSync } = await import("fs");
|
|
237
|
+
for (const sub of readdirSync(playwrightTmpDir)) {
|
|
238
|
+
const subDir = `${playwrightTmpDir}/${sub}`;
|
|
239
|
+
for (const f of readdirSync(subDir)) {
|
|
240
|
+
if (f.startsWith(namespacedSession)) {
|
|
241
|
+
const sockPath = `${subDir}/${f}`;
|
|
242
|
+
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const proc = Bun.spawn([bin, ...binArgs, ...filteredArgs, `-s=${namespacedSession}`], {
|
|
151
250
|
cwd: workDir,
|
|
152
251
|
stdin: "ignore",
|
|
153
252
|
stdout: "pipe",
|
|
@@ -171,7 +270,7 @@ export async function serve() {
|
|
|
171
270
|
timeout.then(() => [1, "", ""] as [number, string, string]),
|
|
172
271
|
]).catch(
|
|
173
272
|
() => [1, "", `Command timed out after ${TIMEOUT / 1000}s\n`] as [number, string, string],
|
|
174
|
-
);
|
|
273
|
+
) as [number, string, string];
|
|
175
274
|
|
|
176
275
|
log(`exit: ${status}${stdout.trim() ? ` | ${stdout.trim().slice(0, 200)}` : ""}`);
|
|
177
276
|
|
|
@@ -209,6 +308,6 @@ export async function serve() {
|
|
|
209
308
|
},
|
|
210
309
|
});
|
|
211
310
|
|
|
212
|
-
log(`serving on http://${server.hostname}:${server.port}`);
|
|
311
|
+
log(`serving on ${tls ? "https" : "http"}://${server.hostname}:${server.port}`);
|
|
213
312
|
log(`Connection URL set (use .env.local to view)`);
|
|
214
313
|
}
|