rechrome 1.18.0 → 1.19.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 +90 -0
- package/rech.ts +90 -0
- package/serve.js +6 -2
- package/serve.ts +6 -2
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -5,6 +5,7 @@ import { randomBytes } from "crypto";
|
|
|
5
5
|
import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, constants as fsConstants } from "fs";
|
|
6
6
|
import { hostname, homedir } from "os";
|
|
7
7
|
import { join, basename, dirname } from "path";
|
|
8
|
+
import { spawn as cpSpawn } from "child_process";
|
|
8
9
|
|
|
9
10
|
export const ENV_KEY = "RECHROME_URL";
|
|
10
11
|
export const DEFAULT_PORT = 13775;
|
|
@@ -685,6 +686,87 @@ async function daemonUninstall(): Promise<void> {
|
|
|
685
686
|
console.log(`Removed ${PM_BIN} process: ${PM_PROCESS_NAME}`);
|
|
686
687
|
}
|
|
687
688
|
|
|
689
|
+
// ── Native tray (menu-bar / system-tray) icon ───────────────────────────────
|
|
690
|
+
// The tray is a small native binary (tray/, Rust). `rech` just supervises it:
|
|
691
|
+
// locate the binary and launch it detached (singleton via a pidfile).
|
|
692
|
+
// `rech tray hide` / the menu "Hide" item both kill the process;
|
|
693
|
+
// `rech tray show` starts a fresh one.
|
|
694
|
+
const TRAY_PID_FILE = join(RECH_HOME_DIR, "tray.pid");
|
|
695
|
+
|
|
696
|
+
// A desktop GUI must be present. Linux needs an X11/Wayland display; a headless
|
|
697
|
+
// box (SSH, CI, container) has neither, so the tray is skipped. macOS/Windows
|
|
698
|
+
// desktop sessions effectively always have one (the binary bypasses if not).
|
|
699
|
+
function trayGuiAvailable(): boolean {
|
|
700
|
+
if (process.platform === "linux")
|
|
701
|
+
return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Resolve the tray binary: explicit override, then the copy shipped beside
|
|
706
|
+
// `rech` (packaged installs), then the dev cargo build, then PATH.
|
|
707
|
+
function findTrayBinary(): string | undefined {
|
|
708
|
+
const ext = IS_WINDOWS ? ".exe" : "";
|
|
709
|
+
const candidates = [
|
|
710
|
+
process.env.RECH_TRAY_BIN,
|
|
711
|
+
join(import.meta.dir, "tray", `rechrome-tray${ext}`),
|
|
712
|
+
join(import.meta.dir, "tray", "target", "release", `rechrome-tray${ext}`),
|
|
713
|
+
join(import.meta.dir, "tray", "target", "debug", `rechrome-tray${ext}`),
|
|
714
|
+
].filter(Boolean) as string[];
|
|
715
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
716
|
+
return Bun.which(`rechrome-tray${ext}`) ?? undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isTrayRunning(): boolean {
|
|
720
|
+
try {
|
|
721
|
+
const pid = parseInt(readFileSync(TRAY_PID_FILE, "utf8"), 10);
|
|
722
|
+
if (!Number.isFinite(pid)) return false;
|
|
723
|
+
process.kill(pid, 0); // signal 0 = liveness probe, doesn't actually signal
|
|
724
|
+
return true;
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Start (and "show") the tray. quiet=true is used by `rech setup` auto-start so
|
|
731
|
+
// a missing binary / headless box stays silent rather than noisy.
|
|
732
|
+
async function startTray({ quiet = false }: { quiet?: boolean } = {}): Promise<void> {
|
|
733
|
+
if (!trayGuiAvailable()) {
|
|
734
|
+
if (!quiet) console.log("tray: no desktop GUI session detected — skipped.");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (isTrayRunning()) {
|
|
738
|
+
if (!quiet) console.log("tray: already running.");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const bin = findTrayBinary();
|
|
742
|
+
if (!bin) {
|
|
743
|
+
if (!quiet)
|
|
744
|
+
console.error("tray: binary not found. Build it with: (cd tray && cargo build --release)");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const child = cpSpawn(bin, [], { detached: true, stdio: "ignore" });
|
|
748
|
+
child.unref(); // outlive this CLI invocation
|
|
749
|
+
if (child.pid) await Bun.write(TRAY_PID_FILE, String(child.pid));
|
|
750
|
+
if (!quiet) console.log(`tray: started (pid ${child.pid}).`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function stopTray(): void {
|
|
754
|
+
if (!isTrayRunning()) { console.log("tray: not running."); return; }
|
|
755
|
+
try { process.kill(parseInt(readFileSync(TRAY_PID_FILE, "utf8"), 10)); } catch {}
|
|
756
|
+
try { unlinkSync(TRAY_PID_FILE); } catch {}
|
|
757
|
+
console.log("tray: stopped. Run `rech tray show` to restore.");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function trayCommand(sub?: string): Promise<void> {
|
|
761
|
+
switch (sub) {
|
|
762
|
+
case "hide": case "stop": case "quit": stopTray(); break;
|
|
763
|
+
case undefined: case "": case "show": case "start": await startTray(); break;
|
|
764
|
+
default:
|
|
765
|
+
console.error(`Unknown tray command: "${sub}". Usage: rech tray [show|hide|stop]`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
688
770
|
// Read the extension's auth token straight from a profile's localStorage LevelDB. Read-only
|
|
689
771
|
// (we never take LevelDB's lock), so it's safe while the user's Chrome is running. The token is
|
|
690
772
|
// the value of the `auth-token` key under the extension origin, stored as a 0x01 (Latin-1)
|
|
@@ -1292,6 +1374,9 @@ Usage:
|
|
|
1292
1374
|
--load-extension, so this is a clean browser, not your
|
|
1293
1375
|
real Chrome. For your real Chrome, use \`rech setup\`
|
|
1294
1376
|
rech status Show current configuration and serve health
|
|
1377
|
+
rech tray [show|hide|stop] Native menu-bar/tray icon for the serve daemon
|
|
1378
|
+
(show=start, hide/show toggle, stop=quit). Auto-
|
|
1379
|
+
starts after \`rech setup\`; skipped with no GUI
|
|
1295
1380
|
rech uninstall Remove the serve daemon and clear config
|
|
1296
1381
|
rech serve Start the serve server manually (foreground)
|
|
1297
1382
|
rech profiles List Chrome profiles
|
|
@@ -1333,6 +1418,11 @@ if (import.meta.main) {
|
|
|
1333
1418
|
: args.find(a => a.startsWith("--token="))?.slice("--token=".length))
|
|
1334
1419
|
?? process.env.RECH_TOKEN;
|
|
1335
1420
|
await setup({ profile, token }); // setup closes envWatcher itself before printing Done
|
|
1421
|
+
// Auto-start the tray (best-effort, silent on headless / missing binary).
|
|
1422
|
+
await startTray({ quiet: true }).catch(() => {});
|
|
1423
|
+
} else if (cmd === "tray") {
|
|
1424
|
+
await trayCommand(args[1]?.toLowerCase());
|
|
1425
|
+
envWatcher?.close();
|
|
1336
1426
|
} else if (cmd === "provision-profile") {
|
|
1337
1427
|
const name = args.find((a, i) => i > 0 && !a.startsWith("-"));
|
|
1338
1428
|
const headed = args.includes("--headed");
|
package/rech.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { randomBytes } from "crypto";
|
|
|
5
5
|
import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, constants as fsConstants } from "fs";
|
|
6
6
|
import { hostname, homedir } from "os";
|
|
7
7
|
import { join, basename, dirname } from "path";
|
|
8
|
+
import { spawn as cpSpawn } from "child_process";
|
|
8
9
|
|
|
9
10
|
export const ENV_KEY = "RECHROME_URL";
|
|
10
11
|
export const DEFAULT_PORT = 13775;
|
|
@@ -685,6 +686,87 @@ async function daemonUninstall(): Promise<void> {
|
|
|
685
686
|
console.log(`Removed ${PM_BIN} process: ${PM_PROCESS_NAME}`);
|
|
686
687
|
}
|
|
687
688
|
|
|
689
|
+
// ── Native tray (menu-bar / system-tray) icon ───────────────────────────────
|
|
690
|
+
// The tray is a small native binary (tray/, Rust). `rech` just supervises it:
|
|
691
|
+
// locate the binary and launch it detached (singleton via a pidfile).
|
|
692
|
+
// `rech tray hide` / the menu "Hide" item both kill the process;
|
|
693
|
+
// `rech tray show` starts a fresh one.
|
|
694
|
+
const TRAY_PID_FILE = join(RECH_HOME_DIR, "tray.pid");
|
|
695
|
+
|
|
696
|
+
// A desktop GUI must be present. Linux needs an X11/Wayland display; a headless
|
|
697
|
+
// box (SSH, CI, container) has neither, so the tray is skipped. macOS/Windows
|
|
698
|
+
// desktop sessions effectively always have one (the binary bypasses if not).
|
|
699
|
+
function trayGuiAvailable(): boolean {
|
|
700
|
+
if (process.platform === "linux")
|
|
701
|
+
return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Resolve the tray binary: explicit override, then the copy shipped beside
|
|
706
|
+
// `rech` (packaged installs), then the dev cargo build, then PATH.
|
|
707
|
+
function findTrayBinary(): string | undefined {
|
|
708
|
+
const ext = IS_WINDOWS ? ".exe" : "";
|
|
709
|
+
const candidates = [
|
|
710
|
+
process.env.RECH_TRAY_BIN,
|
|
711
|
+
join(import.meta.dir, "tray", `rechrome-tray${ext}`),
|
|
712
|
+
join(import.meta.dir, "tray", "target", "release", `rechrome-tray${ext}`),
|
|
713
|
+
join(import.meta.dir, "tray", "target", "debug", `rechrome-tray${ext}`),
|
|
714
|
+
].filter(Boolean) as string[];
|
|
715
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
716
|
+
return Bun.which(`rechrome-tray${ext}`) ?? undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isTrayRunning(): boolean {
|
|
720
|
+
try {
|
|
721
|
+
const pid = parseInt(readFileSync(TRAY_PID_FILE, "utf8"), 10);
|
|
722
|
+
if (!Number.isFinite(pid)) return false;
|
|
723
|
+
process.kill(pid, 0); // signal 0 = liveness probe, doesn't actually signal
|
|
724
|
+
return true;
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Start (and "show") the tray. quiet=true is used by `rech setup` auto-start so
|
|
731
|
+
// a missing binary / headless box stays silent rather than noisy.
|
|
732
|
+
async function startTray({ quiet = false }: { quiet?: boolean } = {}): Promise<void> {
|
|
733
|
+
if (!trayGuiAvailable()) {
|
|
734
|
+
if (!quiet) console.log("tray: no desktop GUI session detected — skipped.");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (isTrayRunning()) {
|
|
738
|
+
if (!quiet) console.log("tray: already running.");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const bin = findTrayBinary();
|
|
742
|
+
if (!bin) {
|
|
743
|
+
if (!quiet)
|
|
744
|
+
console.error("tray: binary not found. Build it with: (cd tray && cargo build --release)");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const child = cpSpawn(bin, [], { detached: true, stdio: "ignore" });
|
|
748
|
+
child.unref(); // outlive this CLI invocation
|
|
749
|
+
if (child.pid) await Bun.write(TRAY_PID_FILE, String(child.pid));
|
|
750
|
+
if (!quiet) console.log(`tray: started (pid ${child.pid}).`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function stopTray(): void {
|
|
754
|
+
if (!isTrayRunning()) { console.log("tray: not running."); return; }
|
|
755
|
+
try { process.kill(parseInt(readFileSync(TRAY_PID_FILE, "utf8"), 10)); } catch {}
|
|
756
|
+
try { unlinkSync(TRAY_PID_FILE); } catch {}
|
|
757
|
+
console.log("tray: stopped. Run `rech tray show` to restore.");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function trayCommand(sub?: string): Promise<void> {
|
|
761
|
+
switch (sub) {
|
|
762
|
+
case "hide": case "stop": case "quit": stopTray(); break;
|
|
763
|
+
case undefined: case "": case "show": case "start": await startTray(); break;
|
|
764
|
+
default:
|
|
765
|
+
console.error(`Unknown tray command: "${sub}". Usage: rech tray [show|hide|stop]`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
688
770
|
// Read the extension's auth token straight from a profile's localStorage LevelDB. Read-only
|
|
689
771
|
// (we never take LevelDB's lock), so it's safe while the user's Chrome is running. The token is
|
|
690
772
|
// the value of the `auth-token` key under the extension origin, stored as a 0x01 (Latin-1)
|
|
@@ -1292,6 +1374,9 @@ Usage:
|
|
|
1292
1374
|
--load-extension, so this is a clean browser, not your
|
|
1293
1375
|
real Chrome. For your real Chrome, use \`rech setup\`
|
|
1294
1376
|
rech status Show current configuration and serve health
|
|
1377
|
+
rech tray [show|hide|stop] Native menu-bar/tray icon for the serve daemon
|
|
1378
|
+
(show=start, hide/show toggle, stop=quit). Auto-
|
|
1379
|
+
starts after \`rech setup\`; skipped with no GUI
|
|
1295
1380
|
rech uninstall Remove the serve daemon and clear config
|
|
1296
1381
|
rech serve Start the serve server manually (foreground)
|
|
1297
1382
|
rech profiles List Chrome profiles
|
|
@@ -1333,6 +1418,11 @@ if (import.meta.main) {
|
|
|
1333
1418
|
: args.find(a => a.startsWith("--token="))?.slice("--token=".length))
|
|
1334
1419
|
?? process.env.RECH_TOKEN;
|
|
1335
1420
|
await setup({ profile, token }); // setup closes envWatcher itself before printing Done
|
|
1421
|
+
// Auto-start the tray (best-effort, silent on headless / missing binary).
|
|
1422
|
+
await startTray({ quiet: true }).catch(() => {});
|
|
1423
|
+
} else if (cmd === "tray") {
|
|
1424
|
+
await trayCommand(args[1]?.toLowerCase());
|
|
1425
|
+
envWatcher?.close();
|
|
1336
1426
|
} else if (cmd === "provision-profile") {
|
|
1337
1427
|
const name = args.find((a, i) => i > 0 && !a.startsWith("-"));
|
|
1338
1428
|
const headed = args.includes("--headed");
|
package/serve.js
CHANGED
|
@@ -376,7 +376,9 @@ export async function serve() {
|
|
|
376
376
|
} else {
|
|
377
377
|
const basename = f.split("/").pop()!;
|
|
378
378
|
for (const subdir of [".playwright-cli", ".rech-multi-tab"]) {
|
|
379
|
-
|
|
379
|
+
// Forward-slash for the wire: join() would use "\" on the Windows daemon, which
|
|
380
|
+
// a POSIX client can't treat as a separator (it builds a literal-backslash path).
|
|
381
|
+
const subpath = `${subdir}/${basename}`;
|
|
380
382
|
if (await file(join(workDir, subpath)).exists()) {
|
|
381
383
|
outputFiles.push(subpath);
|
|
382
384
|
break;
|
|
@@ -390,7 +392,9 @@ export async function serve() {
|
|
|
390
392
|
status,
|
|
391
393
|
stdout: rebrand(stdout),
|
|
392
394
|
stderr: rebrand(stderr),
|
|
393
|
-
|
|
395
|
+
// Normalize any platform separators to "/" so relative paths are portable across
|
|
396
|
+
// a cross-OS daemon↔client (e.g. Windows daemon serving a Linux container client).
|
|
397
|
+
files: outputFiles.map((p) => p.replaceAll("\\", "/")),
|
|
394
398
|
});
|
|
395
399
|
},
|
|
396
400
|
});
|
package/serve.ts
CHANGED
|
@@ -376,7 +376,9 @@ export async function serve() {
|
|
|
376
376
|
} else {
|
|
377
377
|
const basename = f.split("/").pop()!;
|
|
378
378
|
for (const subdir of [".playwright-cli", ".rech-multi-tab"]) {
|
|
379
|
-
|
|
379
|
+
// Forward-slash for the wire: join() would use "\" on the Windows daemon, which
|
|
380
|
+
// a POSIX client can't treat as a separator (it builds a literal-backslash path).
|
|
381
|
+
const subpath = `${subdir}/${basename}`;
|
|
380
382
|
if (await file(join(workDir, subpath)).exists()) {
|
|
381
383
|
outputFiles.push(subpath);
|
|
382
384
|
break;
|
|
@@ -390,7 +392,9 @@ export async function serve() {
|
|
|
390
392
|
status,
|
|
391
393
|
stdout: rebrand(stdout),
|
|
392
394
|
stderr: rebrand(stderr),
|
|
393
|
-
|
|
395
|
+
// Normalize any platform separators to "/" so relative paths are portable across
|
|
396
|
+
// a cross-OS daemon↔client (e.g. Windows daemon serving a Linux container client).
|
|
397
|
+
files: outputFiles.map((p) => p.replaceAll("\\", "/")),
|
|
394
398
|
});
|
|
395
399
|
},
|
|
396
400
|
});
|