rechrome 1.20.0 → 1.22.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/package.json +1 -1
- package/rech.js +185 -61
- package/rech.ts +185 -61
- package/serve.js +231 -40
- package/serve.ts +231 -40
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -104,7 +104,7 @@ function openInDefaultApp(target: string): void {
|
|
|
104
104
|
const cmd = process.platform === "darwin" ? ["open", target]
|
|
105
105
|
: process.platform === "win32" ? ["cmd", "/c", "start", "", target]
|
|
106
106
|
: ["xdg-open", target];
|
|
107
|
-
try { Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" }); } catch {}
|
|
107
|
+
try { Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore", windowsHide: true }); } catch {}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Best-effort path to the Chrome executable for the current platform (used to open a
|
|
@@ -137,7 +137,7 @@ function openInChromeProfile(profileDir: string, target: string): boolean {
|
|
|
137
137
|
try {
|
|
138
138
|
Bun.spawn(
|
|
139
139
|
[chromeBin, `--profile-directory=${profileDir}`, target],
|
|
140
|
-
{ stdout: "ignore", stderr: "ignore", detached: true },
|
|
140
|
+
{ stdout: "ignore", stderr: "ignore", detached: true, windowsHide: true },
|
|
141
141
|
);
|
|
142
142
|
return true;
|
|
143
143
|
} catch {
|
|
@@ -194,48 +194,117 @@ export function authCheck(req: Request, key: string): Response | null {
|
|
|
194
194
|
return null;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
function realpathSafe(p: string): string {
|
|
198
|
+
try { return realpathSync(p); } catch { return p; }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function gitOutput(args: string[], cwd: string): Promise<string | null> {
|
|
199
202
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
// windowsHide: don't flash a console window on Windows (git.exe is a console app)
|
|
204
|
+
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
205
|
+
const out = (await new Response(proc.stdout).text()).trim();
|
|
206
|
+
await proc.exited;
|
|
207
|
+
return out || null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
207
212
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
213
|
+
// Normalize a git remote URL to "host/owner/repo" — no scheme, no .git, no credentials.
|
|
214
|
+
export function normalizeRemote(remoteUrl: string): string {
|
|
215
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
216
|
+
const httpsMatch = remoteUrl.match(/^https?:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/);
|
|
217
|
+
if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
218
|
+
if (httpsMatch) return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
219
|
+
return remoteUrl.replace(/^[^/]*:\/\//, "").replace(/^[^@]*@/, "").replace(/\.git$/, "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Derive the session bucket KEY (what the server hashes) and a human LABEL (logs / tab group)
|
|
223
|
+
// from already-gathered git facts. KEY and LABEL are deliberately decoupled: the key is keyed
|
|
224
|
+
// on the *worktree root path* for predictability (a human can tell which browser they drive),
|
|
225
|
+
// while the label renders a pretty <remote>#<basename>@<branch> for display only.
|
|
226
|
+
// - mode "worktree" (default): key = realpath(worktree root). Stable across `cd` within the
|
|
227
|
+
// project, distinct per worktree (no same-branch collisions), survives `git checkout`
|
|
228
|
+
// (no mutable-branch surprise), and has no branch so detached HEAD doesn't degrade it.
|
|
229
|
+
// - mode "branch": legacy opt-in — key = <remote>/tree/<branch> (the old behavior).
|
|
230
|
+
// - mode "cwd": key = realpath(cwd).
|
|
231
|
+
// Pass realpath'd `cwd` and `root`.
|
|
232
|
+
export function deriveIdentity(opts: {
|
|
233
|
+
mode: string;
|
|
234
|
+
cwd: string;
|
|
235
|
+
host: string;
|
|
236
|
+
root?: string | null; // worktree root (already realpath'd), null when not in git
|
|
237
|
+
remote?: string | null; // normalized host/owner/repo
|
|
238
|
+
branch?: string | null; // branch name, or short SHA when detached
|
|
239
|
+
}): { key: string; label: string } {
|
|
240
|
+
const { mode, cwd, host } = opts;
|
|
241
|
+
const root = opts.root || null;
|
|
242
|
+
const remote = opts.remote || null;
|
|
243
|
+
const branch = opts.branch || null;
|
|
244
|
+
|
|
245
|
+
let key: string;
|
|
246
|
+
if (mode === "branch") {
|
|
247
|
+
key = remote ? `https://${remote}${branch ? `/tree/${branch}` : ""}` : `${host}:${cwd}`;
|
|
248
|
+
} else if (mode === "cwd") {
|
|
249
|
+
key = `cwd:${cwd}`;
|
|
250
|
+
} else {
|
|
251
|
+
key = `worktree:${root || cwd}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let label: string;
|
|
255
|
+
if (root) {
|
|
256
|
+
label = `${remote ? `${remote}#` : ""}${basename(root)}${branch ? `@${branch}` : ""}`;
|
|
257
|
+
} else if (remote) {
|
|
258
|
+
label = `${remote}${branch ? `/tree/${branch}` : ""}`;
|
|
259
|
+
} else {
|
|
260
|
+
label = `${host}:${cwd}`;
|
|
261
|
+
}
|
|
262
|
+
return { key, label };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function getClientIdentity(): Promise<{ key: string; label: string; profile?: string }> {
|
|
266
|
+
const cwd = realpathSafe(process.cwd());
|
|
267
|
+
const mode = (process.env.RECH_IDENTITY || "worktree").toLowerCase();
|
|
268
|
+
let root: string | null = null;
|
|
269
|
+
let remote: string | null = null;
|
|
270
|
+
let branch: string | null = null;
|
|
271
|
+
|
|
272
|
+
const top = await gitOutput(["rev-parse", "--show-toplevel"], cwd);
|
|
273
|
+
if (top) {
|
|
274
|
+
// Roll a submodule cwd up to the outermost superproject working tree, so submodule work
|
|
275
|
+
// shares the parent worktree's browser session (monorepo-friendly). Bounded loop guards
|
|
276
|
+
// against pathological nesting.
|
|
277
|
+
let superCwd = top;
|
|
278
|
+
for (let i = 0; i < 16; i++) {
|
|
279
|
+
const sup = await gitOutput(["rev-parse", "--show-superproject-working-tree"], superCwd);
|
|
280
|
+
if (!sup) break;
|
|
281
|
+
superCwd = sup;
|
|
236
282
|
}
|
|
237
|
-
|
|
238
|
-
|
|
283
|
+
root = realpathSafe(superCwd);
|
|
284
|
+
branch = await gitOutput(["rev-parse", "--abbrev-ref", "HEAD"], root);
|
|
285
|
+
if (!branch || branch === "HEAD")
|
|
286
|
+
branch = await gitOutput(["rev-parse", "--short", "HEAD"], root); // detached HEAD
|
|
287
|
+
const remoteUrl = await gitOutput(["remote", "get-url", "origin"], root);
|
|
288
|
+
if (remoteUrl) remote = normalizeRemote(remoteUrl);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return deriveIdentity({ mode, cwd, host: hostname(), root, remote, branch });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Profile precedence: an explicit `?profile=` in RECHROME_URL is authoritative; the
|
|
295
|
+
// PLAYWRIGHT_MCP_PROFILE_DIRECTORY shell env is the fallback. When both are set and differ,
|
|
296
|
+
// warn once — a silent mismatch here is how an OAuth/login flow can target the WRONG account.
|
|
297
|
+
let _profileMismatchWarned = false;
|
|
298
|
+
function resolveEffectiveProfile(urlProfile?: string): string | undefined {
|
|
299
|
+
const envProfile = process.env.PLAYWRIGHT_MCP_PROFILE_DIRECTORY;
|
|
300
|
+
if (urlProfile && envProfile && urlProfile !== envProfile && !_profileMismatchWarned) {
|
|
301
|
+
_profileMismatchWarned = true;
|
|
302
|
+
console.error(
|
|
303
|
+
`[rech] warning: profile mismatch — RECHROME_URL profile="${urlProfile}" wins over ` +
|
|
304
|
+
`PLAYWRIGHT_MCP_PROFILE_DIRECTORY="${envProfile}". Unset the env var to silence this.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return urlProfile || envProfile;
|
|
239
308
|
}
|
|
240
309
|
|
|
241
310
|
async function getClientEnv(urlExtras?: { extensionId?: string; extensionToken?: string; profileDirectory?: string; userDataDir?: string; loadExtension?: string }): Promise<Record<string, string>> {
|
|
@@ -410,12 +479,18 @@ async function listProfiles(): Promise<void> {
|
|
|
410
479
|
}
|
|
411
480
|
}
|
|
412
481
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
// Header clarifies each column; email/nickname sit beside the profile dir so a bare
|
|
483
|
+
// "Profile N" is never shown alone. Only user_name (email) + name (nickname) are read
|
|
484
|
+
// from Local State — the gaia real name is deliberately never surfaced.
|
|
485
|
+
const rows = [
|
|
486
|
+
["PROFILE", "EMAIL", "NICKNAME", ""],
|
|
487
|
+
...Object.entries(cache).map(([dir, info]) => [
|
|
488
|
+
dir,
|
|
489
|
+
info.user_name || "",
|
|
490
|
+
info.name || "",
|
|
491
|
+
dir === currentDir ? "← current" : "",
|
|
492
|
+
]),
|
|
493
|
+
];
|
|
419
494
|
const widths = rows.reduce((w, r) => r.map((c, i) => Math.max(w[i] ?? 0, c.length)), [] as number[]);
|
|
420
495
|
for (const row of rows) {
|
|
421
496
|
console.log(row.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd());
|
|
@@ -426,11 +501,15 @@ async function callServe(
|
|
|
426
501
|
url: string,
|
|
427
502
|
args: string[],
|
|
428
503
|
overrideEnv?: Record<string, string>,
|
|
504
|
+
precomputedIdentity?: { key: string; label: string; profile?: string },
|
|
429
505
|
): Promise<{ status: number; stdout: string; stderr: string; files?: string[]; existingSession?: boolean }> {
|
|
430
506
|
const { key, host, port, protocol, extensionId, extensionToken, profileDirectory, userDataDir, loadExtension } = parseUrl(url);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
507
|
+
// Reuse the caller's identity when provided — computing it shells out to `git` several times,
|
|
508
|
+
// and run() has already done so for its log line. Recomputing here would double those git
|
|
509
|
+
// spawns (and, on Windows, the console-window flashes) on every `rech open`.
|
|
510
|
+
const identity = precomputedIdentity ?? await getClientIdentity();
|
|
511
|
+
const effectiveProfile = resolveEffectiveProfile(profileDirectory);
|
|
512
|
+
if (effectiveProfile) identity.profile = effectiveProfile;
|
|
434
513
|
const env = { ...(await getClientEnv({ extensionId, extensionToken, profileDirectory, userDataDir, loadExtension })), ...overrideEnv };
|
|
435
514
|
const res = await fetch(`${protocol}://${host}:${port}/run`, {
|
|
436
515
|
method: "POST",
|
|
@@ -468,16 +547,16 @@ async function callServe(
|
|
|
468
547
|
|
|
469
548
|
async function run(url: string, args: string[]) {
|
|
470
549
|
const { host, port, protocol, extensionId, extensionToken, profileDirectory, userDataDir, loadExtension } = parseUrl(url);
|
|
471
|
-
const effectiveProfile = profileDirectory
|
|
550
|
+
const effectiveProfile = resolveEffectiveProfile(profileDirectory);
|
|
472
551
|
const displayProfile = effectiveProfile ? await resolveProfileEmail(effectiveProfile) : undefined;
|
|
473
552
|
const identity = await getClientIdentity();
|
|
474
553
|
const profileSuffix = displayProfile ? ` profile:${displayProfile}` : "";
|
|
475
554
|
console.error(
|
|
476
|
-
`[rech] connecting to ${host}:${port} (identity: ${identity.
|
|
555
|
+
`[rech] connecting to ${host}:${port} (identity: ${identity.label}${profileSuffix})`,
|
|
477
556
|
);
|
|
478
557
|
|
|
479
558
|
const resolvedEnv = await getClientEnv({ extensionId, extensionToken, profileDirectory, userDataDir, loadExtension });
|
|
480
|
-
const { status, stdout, stderr, files, existingSession } = await callServe(url, args);
|
|
559
|
+
const { status, stdout, stderr, files, existingSession } = await callServe(url, args, undefined, identity);
|
|
481
560
|
|
|
482
561
|
const isOpenWithUrl = args[0] === "open" && args.length > 1;
|
|
483
562
|
if (existingSession && isOpenWithUrl) {
|
|
@@ -590,6 +669,7 @@ async function runPm(args: string[], env?: Record<string, string>): Promise<numb
|
|
|
590
669
|
const proc = Bun.spawn(["bunx", PM_BIN, ...args], {
|
|
591
670
|
stdout: "inherit",
|
|
592
671
|
stderr: "inherit",
|
|
672
|
+
windowsHide: true, // no console-window flash for the bunx/pm2 child on Windows
|
|
593
673
|
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
594
674
|
});
|
|
595
675
|
await proc.exited;
|
|
@@ -599,7 +679,7 @@ async function runPm(args: string[], env?: Record<string, string>): Promise<numb
|
|
|
599
679
|
// Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
|
|
600
680
|
// Both render the process name verbatim, so callers can substring-match it.
|
|
601
681
|
async function pmList(): Promise<string> {
|
|
602
|
-
const proc = Bun.spawn(["bunx", PM_BIN, IS_WINDOWS ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore" });
|
|
682
|
+
const proc = Bun.spawn(["bunx", PM_BIN, IS_WINDOWS ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
|
|
603
683
|
return await new Response(proc.stdout).text();
|
|
604
684
|
}
|
|
605
685
|
|
|
@@ -915,7 +995,7 @@ async function provisionExtensionToken(opts: {
|
|
|
915
995
|
if (!opts.headed) args.push("--headless=new");
|
|
916
996
|
if (process.platform === "linux") args.push("--no-sandbox");
|
|
917
997
|
args.push("about:blank");
|
|
918
|
-
const proc = Bun.spawn([chromeBin, ...args], { stdout: "ignore", stderr: "ignore" });
|
|
998
|
+
const proc = Bun.spawn([chromeBin, ...args], { stdout: "ignore", stderr: "ignore", windowsHide: true });
|
|
919
999
|
let cdp: CDPClient | null = null;
|
|
920
1000
|
try {
|
|
921
1001
|
// Chrome writes the chosen port to DevToolsActivePort once the debug server is up.
|
|
@@ -1130,13 +1210,34 @@ async function setup(opts: { profile?: string; token?: string } = {}): Promise<v
|
|
|
1130
1210
|
);
|
|
1131
1211
|
if (opts.profile !== undefined) {
|
|
1132
1212
|
const num = parseInt(opts.profile);
|
|
1133
|
-
if (!isNaN(num) && String(num) === opts.profile.trim())
|
|
1213
|
+
if (!isNaN(num) && String(num) === opts.profile.trim()) {
|
|
1214
|
+
// A bare integer is a 1-based MENU INDEX, NOT the Chrome directory literally named
|
|
1215
|
+
// "Profile <N>". The two collide in the user's head: `--profile 1` selects the first
|
|
1216
|
+
// *listed* profile (usually Default), not the dir "Profile 1". When such a dir exists
|
|
1217
|
+
// and differs from the indexed pick, warn so the mismatch is caught; echo the resolved
|
|
1218
|
+
// selection either way. Email is the unambiguous selector — steer toward it.
|
|
1219
|
+
const sel = available[num - 1] ?? null;
|
|
1220
|
+
const dirNamed = available.find(([dir]) => dir.toLowerCase() === `profile ${num}`);
|
|
1221
|
+
if (dirNamed && dirNamed[0] !== sel?.[0]) {
|
|
1222
|
+
const hint = dirNamed[1].user_name || `"Profile ${num}"`;
|
|
1223
|
+
console.error(
|
|
1224
|
+
` [warn] --profile ${num} = menu index ${num} → ` +
|
|
1225
|
+
`${sel ? `${sel[1].user_name || sel[0]} [${sel[0]}]` : "(out of range)"}, ` +
|
|
1226
|
+
`NOT the Chrome directory "Profile ${num}" (${dirNamed[1].user_name || dirNamed[0]}). ` +
|
|
1227
|
+
`For an unambiguous match use the email or exact directory name, e.g. --profile ${hint}.`,
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (sel) console.log(` selected: ${sel[1].user_name || "(no email)"} [${sel[0]}]`);
|
|
1231
|
+
return sel;
|
|
1232
|
+
}
|
|
1134
1233
|
const needle = opts.profile.toLowerCase();
|
|
1135
|
-
|
|
1234
|
+
const match = available.find(([dir, info]) =>
|
|
1136
1235
|
dir.toLowerCase() === needle
|
|
1137
1236
|
|| (info.name ?? "").toLowerCase() === needle
|
|
1138
1237
|
|| (info.user_name ?? "").toLowerCase().includes(needle)
|
|
1139
1238
|
) ?? null;
|
|
1239
|
+
if (match) console.log(` selected: ${match[1].user_name || "(no email)"} [${match[0]}]`);
|
|
1240
|
+
return match;
|
|
1140
1241
|
}
|
|
1141
1242
|
if (available.length === 1) {
|
|
1142
1243
|
console.log(` Only one profile available — selecting: ${available[0][1].user_name || available[0][0]}`);
|
|
@@ -1346,14 +1447,17 @@ async function status(): Promise<void> {
|
|
|
1346
1447
|
const ping = await fetch(`${protocol}://${host}:${port}/`, { signal: AbortSignal.timeout(2000) }).catch(() => null);
|
|
1347
1448
|
// Resolve the daemon's actual bind from its authenticated /ping (cross-platform; lsof is
|
|
1348
1449
|
// POSIX-only and absent on Windows). bind is "0.0.0.0" (all interfaces) or the loopback IP.
|
|
1349
|
-
const
|
|
1450
|
+
const pingBody = ping
|
|
1350
1451
|
? await fetch(`${protocol}://${host}:${port}/ping`, {
|
|
1351
1452
|
headers: { Authorization: `Bearer ${parsed.key}` },
|
|
1352
1453
|
signal: AbortSignal.timeout(2000),
|
|
1353
|
-
}).then(r => (r.ok ? r.json() : null)).
|
|
1354
|
-
:
|
|
1454
|
+
}).then(r => (r.ok ? r.json() : null)).catch(() => null) as { bind?: string; degraded?: boolean; consecutiveTimeouts?: number } | null
|
|
1455
|
+
: null;
|
|
1456
|
+
const bind = pingBody?.bind;
|
|
1355
1457
|
const listenAddr = bind ? `${bind}:${port}` : `${host}:${port}`;
|
|
1356
1458
|
console.log(`serve: ${ping ? `running ${protocol}://${listenAddr}` : "not running"}`);
|
|
1459
|
+
if (pingBody?.degraded)
|
|
1460
|
+
console.log(`relay: ⚠ degraded (${pingBody.consecutiveTimeouts} consecutive command timeouts) — if it persists, the daemon self-restarts; force it now with \`${PM_BIN} restart ${PM_PROCESS_NAME}\``);
|
|
1357
1461
|
const pmOut = await pmList();
|
|
1358
1462
|
const daemonRegistered = pmOut.includes(PM_PROCESS_NAME);
|
|
1359
1463
|
console.log(`daemon: ${daemonRegistered ? `${PM_BIN} (${PM_PROCESS_NAME})` : "not installed"}`);
|
|
@@ -1380,7 +1484,12 @@ function printHelp(): void {
|
|
|
1380
1484
|
Usage:
|
|
1381
1485
|
rech setup [--profile <num|email>] [--token <tok>]
|
|
1382
1486
|
First-time setup: daemon + Chrome extension + config
|
|
1383
|
-
--profile selects the Chrome profile non-interactively
|
|
1487
|
+
--profile selects the Chrome profile non-interactively.
|
|
1488
|
+
A bare number is the 1-based MENU INDEX (position in the
|
|
1489
|
+
listed order), NOT the Chrome directory "Profile N". For
|
|
1490
|
+
scripts prefer the email (e.g. --profile you@gmail.com) —
|
|
1491
|
+
it is the only unambiguous selector; an exact directory
|
|
1492
|
+
name ("Profile 1") also matches.
|
|
1384
1493
|
--token (or RECH_TOKEN) supplies the auth token for
|
|
1385
1494
|
non-TTY/agent runs, skipping the interactive paste
|
|
1386
1495
|
rech provision-profile <name> --experimental [--headed]
|
|
@@ -1396,10 +1505,16 @@ Usage:
|
|
|
1396
1505
|
rech serve Start the serve server manually (foreground)
|
|
1397
1506
|
rech profiles List Chrome profiles
|
|
1398
1507
|
rech <playwright-args...> Run Playwright CLI command (requires ${ENV_KEY})
|
|
1508
|
+
rech --isolate <args...> Run in a throwaway session (sugar for -s=<random>) so a
|
|
1509
|
+
fragile single-shot flow (OAuth/login) never shares tabs
|
|
1510
|
+
with the worktree's default session
|
|
1399
1511
|
|
|
1400
1512
|
Environment:
|
|
1401
1513
|
${ENV_KEY} Server URL set by \`rech setup\`
|
|
1402
1514
|
RECH_TOKEN Auth token for \`rech setup\` (same as --token)
|
|
1515
|
+
RECH_IDENTITY Session bucket mode: worktree (default) | branch | cwd. The session a
|
|
1516
|
+
client reuses is keyed on the worktree root path; \`branch\` restores the
|
|
1517
|
+
old <remote>/tree/<branch> keying, \`cwd\` keys on the exact directory
|
|
1403
1518
|
|
|
1404
1519
|
Examples:
|
|
1405
1520
|
rech setup
|
|
@@ -1468,6 +1583,15 @@ if (import.meta.main) {
|
|
|
1468
1583
|
printHelp();
|
|
1469
1584
|
process.exit(1);
|
|
1470
1585
|
}
|
|
1586
|
+
// --isolate: ephemeral session isolation, sugar for -s=iso-<random>. For fragile single-shot
|
|
1587
|
+
// flows (OAuth/login) that must not share tabs with the worktree's default session. The `iso-`
|
|
1588
|
+
// marker lets the daemon reap these throwaway sessions on an idle TTL (see serve.js), so an
|
|
1589
|
+
// OAuth drive can't leak an orphaned browser context.
|
|
1590
|
+
const isolateIdx = args.findIndex((a) => a === "--isolate" || a === "--isolated");
|
|
1591
|
+
if (isolateIdx !== -1) {
|
|
1592
|
+
args.splice(isolateIdx, 1);
|
|
1593
|
+
args.push(`-s=iso-${randomBytes(8).toString("hex")}`);
|
|
1594
|
+
}
|
|
1471
1595
|
await run(url, args);
|
|
1472
1596
|
envWatcher?.close();
|
|
1473
1597
|
}
|