rechrome 1.18.1 → 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/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");
|