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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/rech.js +90 -0
  3. package/rech.ts +90 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
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");