rechrome 1.37.2 → 1.38.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 +35 -0
- package/fill-secret.js +405 -0
- package/fill-secret.ts +399 -0
- package/listeners.js +2 -2
- package/listeners.ts +2 -2
- package/package.json +4 -2
- package/rechrome.js +154 -67
- package/rechrome.ts +154 -67
- package/serve.js +102 -10
- package/serve.ts +102 -10
package/rechrome.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readListeners, writeListeners, listenerAddress, isLoopback, normalizePr
|
|
|
5
5
|
import { file } from "bun";
|
|
6
6
|
import yargs from "yargs";
|
|
7
7
|
import { readExtensionTokenFromProfile } from "./extension-token.js";
|
|
8
|
+
import { FILL_SECRET_USAGE, SECRET_MASK, parseFillSecretArgs, fillSecretWireArgs, readSecretSource, totpCode, totpWaitMs } from "./fill-secret.js";
|
|
8
9
|
import { createHash, randomBytes } from "crypto";
|
|
9
10
|
import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, renameSync, rmdirSync, constants as fsConstants } from "fs";
|
|
10
11
|
import { hostname, homedir, networkInterfaces } from "os";
|
|
@@ -1869,6 +1870,7 @@ async function callServe(
|
|
|
1869
1870
|
precomputedIdentity?: { key: string; label: string; profile?: string },
|
|
1870
1871
|
throwOnFailure = false,
|
|
1871
1872
|
upload?: UploadPayload,
|
|
1873
|
+
secret?: string,
|
|
1872
1874
|
): Promise<{ status: number; stdout: string; stderr: string; files?: string[]; existingSession?: boolean }> {
|
|
1873
1875
|
const { key, host, port, protocol, extensionId, extensionToken, profileDirectory, userDataDir, loadExtension } = parseUrl(url);
|
|
1874
1876
|
// Reuse the caller's identity when provided — computing it shells out to `git` several times,
|
|
@@ -1887,7 +1889,8 @@ async function callServe(
|
|
|
1887
1889
|
method: "POST",
|
|
1888
1890
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
1889
1891
|
// dataDir is a client-local path; the daemon (possibly remote) only needs the session identity.
|
|
1890
|
-
|
|
1892
|
+
// `secret` (fill-secret only) rides in its own field so it is never part of the logged args.
|
|
1893
|
+
body: JSON.stringify({ args, identity: { key: identity.key, label: identity.label, profile: identity.profile }, env, ...(upload ? { upload } : {}), ...(secret ? { secret } : {}) }),
|
|
1891
1894
|
signal: AbortSignal.timeout(70_000),
|
|
1892
1895
|
}).catch(async (e) => {
|
|
1893
1896
|
if (throwOnFailure) throw new Error("Cannot reach the rechrome daemon. Check that it is running.");
|
|
@@ -2099,12 +2102,26 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
2099
2102
|
|
|
2100
2103
|
const resolvedEnv = await getClientEnv({ extensionId, extensionToken, profileDirectory: effectiveProfile, userDataDir, loadExtension });
|
|
2101
2104
|
const effectiveEnv = { ...resolvedEnv, ...overrideEnv };
|
|
2105
|
+
let secret: string | undefined;
|
|
2102
2106
|
let upload: UploadPayload | undefined;
|
|
2103
|
-
|
|
2107
|
+
const cmdIdx = args.findIndex(a => !a.startsWith("-s="));
|
|
2108
|
+
if (args[cmdIdx] === "fill-secret") {
|
|
2109
|
+
if (args.some(a => a === "--help" || a === "-h")) { console.log(FILL_SECRET_USAGE); process.exit(0); }
|
|
2110
|
+
try {
|
|
2111
|
+
({ args, secret } = await prepareFillSecret(args));
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
console.error(`[rech] ${err instanceof Error ? err.message : String(err)}\n${FILL_SECRET_USAGE}`);
|
|
2114
|
+
process.exit(2);
|
|
2115
|
+
}
|
|
2116
|
+
} else if (args.find(a => !/^-s=[a-zA-Z0-9_-]{1,64}$/.test(a)) === "upload") {
|
|
2104
2117
|
if (process.stdin.isTTY) throw new Error("upload reads stdin; redirect a PNG/SVG file with < icon.png");
|
|
2105
2118
|
({ args, upload } = await prepareStdinUpload(args, process.stdin));
|
|
2106
2119
|
}
|
|
2107
|
-
const
|
|
2120
|
+
const served = await callServe(url, args, overrideEnv, identity, false, upload, secret);
|
|
2121
|
+
// The daemon already masks the value; this is a second line of defence for an older daemon.
|
|
2122
|
+
const scrub = (s: string) => secret && s ? s.replaceAll(secret, SECRET_MASK) : s;
|
|
2123
|
+
const { files, existingSession } = served;
|
|
2124
|
+
const status = served.status, stdout = scrub(served.stdout), stderr = scrub(served.stderr);
|
|
2108
2125
|
if (!opts.bridgeRetried && isBridgeTimeout(args, { status, stderr })) {
|
|
2109
2126
|
// Close the failed session so the retry gets a fresh cliDaemon and relay. callServe re-reads
|
|
2110
2127
|
// the extension's live token from the profile, so a re-minted token heals here too.
|
|
@@ -2171,6 +2188,22 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
2171
2188
|
process.exit(status);
|
|
2172
2189
|
}
|
|
2173
2190
|
|
|
2191
|
+
/** Swap the client-side fill-secret args for the wire form and read the value (or TOTP code). */
|
|
2192
|
+
async function prepareFillSecret(args: string[]): Promise<{ args: string[]; secret: string }> {
|
|
2193
|
+
const sessionArgs = args.filter(a => a.startsWith("-s="));
|
|
2194
|
+
const req = parseFillSecretArgs(args.filter(a => !a.startsWith("-s=")).slice(1));
|
|
2195
|
+
const raw = await readSecretSource(req.source);
|
|
2196
|
+
let secret = raw;
|
|
2197
|
+
if (req.totp) {
|
|
2198
|
+
const wait = totpWaitMs();
|
|
2199
|
+
if (wait) { console.error(`[rech] TOTP code expires in under 5s; waiting ${Math.ceil(wait / 1000)}s for the next one`); await Bun.sleep(wait); }
|
|
2200
|
+
secret = totpCode(raw); // only the code leaves this machine, never the seed
|
|
2201
|
+
}
|
|
2202
|
+
if (!req.allowDomains.length) console.error("[rech] fill-secret: no --allow-domain given; the value will be typed into whatever page is active");
|
|
2203
|
+
const wire = fillSecretWireArgs(req);
|
|
2204
|
+
return { args: [...wire, ...sessionArgs], secret };
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2174
2207
|
/** The daemon's Playwright CLI predates `open --wait` (rechrome <= 1.35.1 bundled one). */
|
|
2175
2208
|
export function cliRejectsWaitOption(output: string): boolean {
|
|
2176
2209
|
return /Unknown options?:?[^\n]*?['"`]?--wait\b/i.test(output);
|
|
@@ -2883,45 +2916,90 @@ function findChromiumForTesting(): string | null {
|
|
|
2883
2916
|
// Minimal Chrome DevTools Protocol client over a WebSocket — just enough to create a
|
|
2884
2917
|
// target, attach to it, and evaluate JS. Used to seed the auth token into a managed
|
|
2885
2918
|
// profile's extension localStorage without pulling in the full Playwright dependency.
|
|
2886
|
-
class CDPClient {
|
|
2919
|
+
export class CDPClient {
|
|
2887
2920
|
private ws: WebSocket;
|
|
2888
2921
|
private nextId = 0;
|
|
2889
|
-
private pending = new Map<number, { resolve: (v: any) => void; reject: (e:
|
|
2922
|
+
private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
|
2890
2923
|
private opened: Promise<void>;
|
|
2891
|
-
constructor(url: string) {
|
|
2924
|
+
constructor(url: string, private timeoutMs = 30_000) {
|
|
2892
2925
|
this.ws = new WebSocket(url);
|
|
2893
2926
|
this.opened = new Promise<void>((resolve, reject) => {
|
|
2894
|
-
|
|
2895
|
-
this.ws.addEventListener("
|
|
2927
|
+
const timer = setTimeout(() => { reject(new Error("CDP WebSocket connection timed out")); this.close(); }, timeoutMs);
|
|
2928
|
+
this.ws.addEventListener("open", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
2929
|
+
const fail = () => {
|
|
2930
|
+
clearTimeout(timer);
|
|
2931
|
+
const error = new Error("CDP WebSocket closed or failed");
|
|
2932
|
+
reject(error);
|
|
2933
|
+
for (const p of this.pending.values()) p.reject(error);
|
|
2934
|
+
this.pending.clear();
|
|
2935
|
+
};
|
|
2936
|
+
this.ws.addEventListener("error", fail);
|
|
2937
|
+
this.ws.addEventListener("close", fail);
|
|
2896
2938
|
});
|
|
2897
2939
|
this.ws.addEventListener("message", (ev: MessageEvent) => {
|
|
2898
2940
|
let msg: any;
|
|
2899
2941
|
try { msg = JSON.parse(typeof ev.data === "string" ? ev.data : ""); } catch { return; }
|
|
2900
2942
|
const p = msg.id != null ? this.pending.get(msg.id) : undefined;
|
|
2901
2943
|
if (!p) return;
|
|
2902
|
-
this.pending.delete(msg.id);
|
|
2903
2944
|
if (msg.error) p.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
2904
2945
|
else p.resolve(msg.result);
|
|
2905
2946
|
});
|
|
2906
2947
|
}
|
|
2907
2948
|
async open(): Promise<void> { await this.opened; }
|
|
2908
2949
|
send(method: string, params: Record<string, any> = {}, sessionId?: string): Promise<any> {
|
|
2950
|
+
if (this.ws.readyState !== WebSocket.OPEN) return Promise.reject(new Error("CDP WebSocket is not open"));
|
|
2909
2951
|
const id = ++this.nextId;
|
|
2910
|
-
const payload: any = { id, method, params };
|
|
2911
|
-
if (sessionId) payload.sessionId = sessionId;
|
|
2912
2952
|
return new Promise((resolve, reject) => {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2953
|
+
const finish = (callback: (value: any) => void, value: any) => {
|
|
2954
|
+
clearTimeout(timer);
|
|
2955
|
+
this.pending.delete(id);
|
|
2956
|
+
callback(value);
|
|
2957
|
+
};
|
|
2958
|
+
const timer = setTimeout(() => finish(reject, new Error(`CDP ${method} timed out`)), this.timeoutMs);
|
|
2959
|
+
this.pending.set(id, { resolve: v => finish(resolve, v), reject: e => finish(reject, e) });
|
|
2960
|
+
try { this.ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })); }
|
|
2961
|
+
catch (error) { finish(reject, error); }
|
|
2916
2962
|
});
|
|
2917
2963
|
}
|
|
2918
|
-
close(): void {
|
|
2964
|
+
close(): void {
|
|
2965
|
+
for (const p of this.pending.values()) p.reject(new Error("CDP client closed"));
|
|
2966
|
+
this.pending.clear();
|
|
2967
|
+
try { this.ws.close(); } catch {}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
async function waitForExit(exited: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
|
2972
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
2973
|
+
try {
|
|
2974
|
+
return await Promise.race([exited.then(() => true), new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), timeoutMs); })]);
|
|
2975
|
+
} finally { clearTimeout(timer!); }
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
export async function seedExtensionToken(cdp: Pick<CDPClient, "send">, token: string): Promise<void> {
|
|
2979
|
+
const { targetId } = await cdp.send("Target.createTarget", { url: `chrome-extension://${EXTENSION_ID}/status.html` });
|
|
2980
|
+
const { sessionId } = await cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
|
2981
|
+
// The extension page may still be loading; retry the write until localStorage reflects it.
|
|
2982
|
+
let ok = false;
|
|
2983
|
+
const expr = `(()=>{try{if(location.origin!==${JSON.stringify(`chrome-extension://${EXTENSION_ID}`)})return 'ERR:Extension page not loaded';localStorage.setItem('auth-token',${JSON.stringify(token)});return localStorage.getItem('auth-token');}catch(e){return 'ERR:'+e.message}})()`;
|
|
2984
|
+
let lastError = "Extension page is not ready";
|
|
2985
|
+
const deadline = Date.now() + 30_000;
|
|
2986
|
+
while (Date.now() < deadline) {
|
|
2987
|
+
const r = await cdp.send("Runtime.evaluate", { expression: expr, returnByValue: true }, sessionId);
|
|
2988
|
+
lastError = r?.exceptionDetails?.text || String(r?.result?.value ?? "No evaluation result");
|
|
2989
|
+
if (r?.result?.value === token) { ok = true; break; }
|
|
2990
|
+
// A target opened before the extension finishes loading can remain on Chrome's
|
|
2991
|
+
// error document forever. Evaluating it again cannot finish the navigation.
|
|
2992
|
+
const navigation = await cdp.send("Page.navigate", { url: `chrome-extension://${EXTENSION_ID}/status.html` }, sessionId);
|
|
2993
|
+
if (navigation.errorText) lastError = navigation.errorText;
|
|
2994
|
+
await Bun.sleep(1_000);
|
|
2995
|
+
}
|
|
2996
|
+
if (!ok) throw new Error(`Could not seed auth token into chrome-extension://${EXTENSION_ID}/: ${lastError}`);
|
|
2919
2997
|
}
|
|
2920
2998
|
|
|
2921
2999
|
// Launch a throwaway Chrome against a dedicated user-data-dir with the unpacked extension
|
|
2922
3000
|
// loaded, then seed `token` into the extension's localStorage (the value `connect.html` checks
|
|
2923
3001
|
// for token-bypass). Headless by default; never touches the user's real Chrome/profiles.
|
|
2924
|
-
async function provisionExtensionToken(opts: {
|
|
3002
|
+
export async function provisionExtensionToken(opts: {
|
|
2925
3003
|
userDataDir: string; profileDir: string; dist: string; token: string; headed?: boolean;
|
|
2926
3004
|
}): Promise<void> {
|
|
2927
3005
|
// Branded Google Chrome 149+ rejects --load-extension ("not allowed in Google Chrome"), so a
|
|
@@ -2944,73 +3022,80 @@ async function provisionExtensionToken(opts: {
|
|
|
2944
3022
|
"--no-first-run",
|
|
2945
3023
|
"--no-default-browser-check",
|
|
2946
3024
|
"--disable-background-timer-throttling",
|
|
3025
|
+
"--disable-background-networking",
|
|
3026
|
+
"--disable-component-update",
|
|
3027
|
+
"--disable-search-engine-choice-screen",
|
|
3028
|
+
// Match Playwright's unattended macOS launch: no interactive keychain prompt.
|
|
3029
|
+
"--password-store=basic",
|
|
3030
|
+
"--use-mock-keychain",
|
|
2947
3031
|
];
|
|
2948
3032
|
if (!opts.headed) args.push("--headless=new");
|
|
2949
3033
|
if (process.platform === "linux") args.push("--no-sandbox");
|
|
2950
|
-
// Headless can't answer a Keychain prompt, so first launch could stall on it (as Playwright does).
|
|
2951
|
-
if (process.platform === "darwin") args.push("--use-mock-keychain");
|
|
2952
3034
|
args.push("about:blank");
|
|
2953
|
-
|
|
3035
|
+
// Own a separate process group so escalation cannot touch the user's Chrome.
|
|
3036
|
+
const proc = cpSpawn(chromeBin, args, { detached: process.platform !== "win32", stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
3037
|
+
let stderr = "";
|
|
3038
|
+
proc.stderr?.on("data", data => { stderr = (stderr + data.toString()).slice(-8_192); });
|
|
3039
|
+
let launchError: Error | undefined;
|
|
3040
|
+
const exited = new Promise<void>(resolve => {
|
|
3041
|
+
proc.once("exit", () => resolve());
|
|
3042
|
+
proc.once("error", error => { launchError = error; resolve(); });
|
|
3043
|
+
});
|
|
3044
|
+
const signal = (value: NodeJS.Signals) => {
|
|
3045
|
+
if (!proc.pid) return;
|
|
3046
|
+
try {
|
|
3047
|
+
if (process.platform === "win32") proc.kill(value);
|
|
3048
|
+
else process.kill(-proc.pid, value);
|
|
3049
|
+
} catch {}
|
|
3050
|
+
};
|
|
3051
|
+
const interrupted = (value: NodeJS.Signals) => {
|
|
3052
|
+
signal("SIGKILL");
|
|
3053
|
+
console.error(`Profile provisioning interrupted (${value})`);
|
|
3054
|
+
process.exit(value === "SIGINT" ? 130 : 143);
|
|
3055
|
+
};
|
|
3056
|
+
const onInt = () => interrupted("SIGINT");
|
|
3057
|
+
const onTerm = () => interrupted("SIGTERM");
|
|
3058
|
+
process.once("SIGINT", onInt);
|
|
3059
|
+
process.once("SIGTERM", onTerm);
|
|
2954
3060
|
let cdp: CDPClient | null = null;
|
|
2955
|
-
|
|
2956
|
-
const within = <T>(ms: number, what: string, work: Promise<T>) => Promise.race([work,
|
|
2957
|
-
Bun.sleep(ms).then(() => { throw new Error(`${what} took over ${ms / 1000}s`); })]);
|
|
3061
|
+
let phase = "Chrome startup";
|
|
2958
3062
|
try {
|
|
2959
3063
|
// Chrome writes the chosen port to DevToolsActivePort once the debug server is up.
|
|
2960
3064
|
let port: number | null = null;
|
|
2961
|
-
for (let i = 0; i <
|
|
3065
|
+
for (let i = 0; i < 600; i++) {
|
|
2962
3066
|
await Bun.sleep(100);
|
|
2963
3067
|
const line = (await file(portFile).text().catch(() => "")).split("\n")[0]?.trim();
|
|
2964
3068
|
if (line && /^\d+$/.test(line)) { port = parseInt(line); break; }
|
|
2965
|
-
if (
|
|
3069
|
+
if (launchError) throw launchError;
|
|
3070
|
+
if (proc.exitCode !== null || proc.signalCode !== null) throw new Error(`Chrome exited before opening the DevTools port (${proc.exitCode ?? proc.signalCode})`);
|
|
2966
3071
|
}
|
|
2967
|
-
if (!port) throw new Error("Chrome
|
|
2968
|
-
|
|
3072
|
+
if (!port) throw new Error("Chrome DevTools port not found (extension may have failed to load)");
|
|
3073
|
+
phase = "Chrome DevTools discovery";
|
|
3074
|
+
const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(30_000) })).json();
|
|
3075
|
+
phase = "Chrome DevTools connection";
|
|
2969
3076
|
cdp = new CDPClient(ver.webSocketDebuggerUrl as string);
|
|
2970
|
-
await
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
let loaded = false;
|
|
2974
|
-
for (let i = 0; i < 50 && !loaded; i++) {
|
|
2975
|
-
const { targetInfos } = await cdp.send("Target.getTargets");
|
|
2976
|
-
loaded = (targetInfos as { url: string }[]).some(t => t.url.startsWith(extensionUrl));
|
|
2977
|
-
if (!loaded) await Bun.sleep(200);
|
|
2978
|
-
}
|
|
2979
|
-
if (!loaded) throw new Error(`The extension at ${dist} didn't load in Chrome for Testing (no ${extensionUrl} target within 10s)`);
|
|
2980
|
-
const { targetId } = await cdp.send("Target.createTarget", { url: `${extensionUrl}status.html` });
|
|
2981
|
-
const { sessionId } = await cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
|
2982
|
-
// The extension page may still be loading; retry the write until localStorage reflects it.
|
|
2983
|
-
let ok = false, last = "";
|
|
2984
|
-
const expr = `(()=>{try{localStorage.setItem('auth-token',${JSON.stringify(token)});return localStorage.getItem('auth-token');}catch(e){return 'ERR:'+e.message}})()`;
|
|
2985
|
-
const deadline = Date.now() + 10_000;
|
|
2986
|
-
while (Date.now() < deadline) {
|
|
2987
|
-
const r = await within(3000, "Runtime.evaluate", cdp.send("Runtime.evaluate", { expression: expr, returnByValue: true }, sessionId)).catch(e => ({ error: String(e) }));
|
|
2988
|
-
const value = (r as any)?.result?.value;
|
|
2989
|
-
if (value === token) { ok = true; break; }
|
|
2990
|
-
last = typeof value === "string" ? value : (r as any)?.error ?? (r as any)?.result?.description ?? "";
|
|
2991
|
-
await Bun.sleep(200);
|
|
2992
|
-
}
|
|
2993
|
-
if (!ok) throw new Error(`Could not seed auth token into ${extensionUrl}${last ? ` (${last})` : ""}`);
|
|
3077
|
+
await cdp.open();
|
|
3078
|
+
phase = "Extension token seeding";
|
|
3079
|
+
await seedExtensionToken(cdp, token);
|
|
2994
3080
|
// Graceful close flushes localStorage to the profile's leveldb before we kill Chrome.
|
|
2995
|
-
|
|
2996
|
-
await
|
|
3081
|
+
phase = "Chrome shutdown";
|
|
3082
|
+
await cdp.send("Browser.close").catch(() => {});
|
|
3083
|
+
if (!await waitForExit(exited, 30_000)) throw new Error("Chrome did not exit after flushing the auth token");
|
|
3084
|
+
if (readExtensionTokenFromProfile(userDataDir, profileDir, EXTENSION_ID) !== token)
|
|
3085
|
+
throw new Error("Chrome exited without persisting the extension auth token");
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3088
|
+
throw new Error(`${phase}: ${message}${stderr.trim() ? `\nChrome stderr:\n${stderr.trim()}` : ""}`.replaceAll(token, "[redacted]"));
|
|
2997
3089
|
} finally {
|
|
2998
3090
|
cdp?.close();
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
*/
|
|
3007
|
-
async function stopThrowawayChrome(proc: ReturnType<typeof Bun.spawn>, userDataDir: string): Promise<void> {
|
|
3008
|
-
if (proc.exitCode === null) {
|
|
3009
|
-
try { proc.kill(); } catch {}
|
|
3010
|
-
const exited = await Promise.race([proc.exited.then(() => true, () => true), Bun.sleep(3000).then(() => false)]);
|
|
3011
|
-
if (!exited) { try { proc.kill("SIGKILL"); } catch {} await Promise.race([proc.exited.catch(() => {}), Bun.sleep(2000)]); }
|
|
3091
|
+
signal("SIGTERM");
|
|
3092
|
+
if (!await waitForExit(exited, 2_000)) signal("SIGKILL");
|
|
3093
|
+
// Also reap helpers that survived the browser parent exiting.
|
|
3094
|
+
signal("SIGKILL");
|
|
3095
|
+
await waitForExit(exited, 2_000);
|
|
3096
|
+
process.removeListener("SIGINT", onInt);
|
|
3097
|
+
process.removeListener("SIGTERM", onTerm);
|
|
3012
3098
|
}
|
|
3013
|
-
if (process.platform !== "win32") Bun.spawnSync(["pkill", "-9", "-f", `--user-data-dir=${userDataDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}( |$)`], { stdout: "ignore", stderr: "ignore" });
|
|
3014
3099
|
}
|
|
3015
3100
|
|
|
3016
3101
|
async function provisionProfile(name: string, opts: { headed?: boolean } = {}): Promise<void> {
|
|
@@ -3029,7 +3114,7 @@ async function provisionProfile(name: string, opts: { headed?: boolean } = {}):
|
|
|
3029
3114
|
console.log(` extension: ${dist}`);
|
|
3030
3115
|
console.log(` Launching ${opts.headed ? "headed" : "headless"} Chrome to seed the auth token...`);
|
|
3031
3116
|
await provisionExtensionToken({ userDataDir, profileDir: name, dist, token, headed: opts.headed });
|
|
3032
|
-
console.log(
|
|
3117
|
+
console.log(" Token seeded");
|
|
3033
3118
|
|
|
3034
3119
|
// [2/3] Daemon URL — reuse the running daemon's key; warn (don't fail) if it isn't up yet.
|
|
3035
3120
|
console.log(`\n[2/3] Building RECHROME_URL`);
|
|
@@ -4140,6 +4225,8 @@ const HELP_EPILOGUE = `Browser commands (sent to this project's Chrome session):
|
|
|
4140
4225
|
open, goto, click, fill, screenshot, eval, tab-list… (\`rech pw --help\` lists all)
|
|
4141
4226
|
--profile <p> as another registered profile (email, name or folder); put it first
|
|
4142
4227
|
--isolate in a throwaway session, e.g. for a login flow
|
|
4228
|
+
rech fill-secret <ref> --from-env VAR | --totp-from-env VAR [--env-file f] [--allow-domain '*.example.com']
|
|
4229
|
+
type a password / TOTP code without it reaching argv, logs or output
|
|
4143
4230
|
rech pw <args> forward verbatim to playwright-cli, e.g. \`rech pw --version\`
|
|
4144
4231
|
rech --version rechrome's version
|
|
4145
4232
|
|