rechrome 1.22.0 → 1.23.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 +82 -22
  3. package/rech.ts +82 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rech.js CHANGED
@@ -6,6 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync
6
6
  import { hostname, homedir } from "os";
7
7
  import { join, basename, dirname } from "path";
8
8
  import { spawn as cpSpawn } from "child_process";
9
+ import { pickDaemonManager, type DaemonManager } from "./daemon-manager.js";
9
10
 
10
11
  export const ENV_KEY = "RECHROME_URL";
11
12
  export const DEFAULT_PORT = 13775;
@@ -658,28 +659,80 @@ const PM_PROCESS_NAME = "rechrome";
658
659
  // Pre-rename names to evict on (re)install/uninstall so a single `rech setup`
659
660
  // migrates an existing checkout cleanly.
660
661
  const LEGACY_PROCESS_NAMES = ["rechrome-serve"];
661
- // oxmgr everywhere, but it's unstable on Windows — fall back to pm2 there.
662
662
  const IS_WINDOWS = process.platform === "win32";
663
- const PM_BIN = IS_WINDOWS ? "pm2" : "oxmgr";
664
663
 
665
- // Spawn the active process manager. `env` is merged over process.env for the
666
- // child: pm2 captures the CLI's environment for the managed process (it has no
667
- // per-var flag like oxmgr's --env), so install passes daemon env this way.
668
- async function runPm(args: string[], env?: Record<string, string>): Promise<number> {
669
- const proc = Bun.spawn(["bunx", PM_BIN, ...args], {
664
+ // Read the installed oxmgr's version (e.g. "0.4.0+winfix"), or null if oxmgr
665
+ // isn't on PATH / doesn't answer. Cached daemonManager() sits on the hot path
666
+ // of several subcommands (status, setup, install). Synchronous by design so the
667
+ // selection has no await threading through call sites.
668
+ let _oxmgrVersion: string | null | undefined;
669
+ function oxmgrVersion(bin: string): string | null {
670
+ if (_oxmgrVersion !== undefined) return _oxmgrVersion;
671
+ try {
672
+ const p = Bun.spawnSync([bin, "--version"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
673
+ const m = /(\d+\.\d+\.\d+[^\s]*)/.exec(p.stdout?.toString() ?? "");
674
+ _oxmgrVersion = m ? m[1]! : null;
675
+ } catch {
676
+ _oxmgrVersion = null;
677
+ }
678
+ return _oxmgrVersion;
679
+ }
680
+
681
+ // Resolve (and cache) the process manager that daemonizes `rech serve`. The
682
+ // selection policy — oxmgr default, pm2 fallback, winfix-guarded on Windows,
683
+ // RECH_DAEMON_MANAGER override — lives in ./daemon-manager.js (pure + tested);
684
+ // here we just supply the runtime inputs (what's on PATH, the oxmgr version).
685
+ let _daemonMgr: DaemonManager | undefined;
686
+ function daemonManager(): DaemonManager {
687
+ if (_daemonMgr) return _daemonMgr;
688
+ const oxmgrBin = Bun.which("oxmgr");
689
+ const pm2Bin = Bun.which("pm2");
690
+ _daemonMgr = pickDaemonManager({
691
+ oxmgrBin,
692
+ pm2Bin,
693
+ oxmgrVersion: oxmgrBin ? oxmgrVersion(oxmgrBin) : null,
694
+ isWindows: IS_WINDOWS,
695
+ override: process.env.RECH_DAEMON_MANAGER,
696
+ });
697
+ return _daemonMgr;
698
+ }
699
+
700
+ // Spawn the resolved process manager by its absolute path (Bun.which). `env` is
701
+ // merged over process.env for the child: pm2 captures the CLI's environment for
702
+ // the managed process (it has no per-var flag like oxmgr's --env), so install
703
+ // passes daemon env this way.
704
+ async function runPm(mgr: DaemonManager, args: string[], env?: Record<string, string>): Promise<number> {
705
+ const proc = Bun.spawn([mgr.bin, ...args], {
670
706
  stdout: "inherit",
671
707
  stderr: "inherit",
672
- windowsHide: true, // no console-window flash for the bunx/pm2 child on Windows
708
+ windowsHide: true, // no console-window flash for the manager child on Windows
673
709
  ...(env ? { env: { ...process.env, ...env } } : {}),
674
710
  });
675
711
  await proc.exited;
676
712
  return proc.exitCode ?? 1;
677
713
  }
678
714
 
715
+ // oxmgr boot/login autostart. `service install` wires the platform init
716
+ // integration (Windows Task Scheduler, macOS launchd, or a systemd --user unit)
717
+ // so the daemon — and the managed serve with it — returns at login/boot, the way
718
+ // the pm2 path relied on `pm2 resurrect`. Skipped when already installed:
719
+ // re-running `service install` re-bootstraps the oxmgr daemon, which restarts
720
+ // every managed process (including the live serve). Best-effort — a failure
721
+ // leaves serve crash-managed but not login-persistent.
722
+ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
723
+ let alreadyInstalled = false;
724
+ try {
725
+ alreadyInstalled =
726
+ Bun.spawnSync([mgr.bin, "service", "status"], { stdout: "ignore", stderr: "ignore", windowsHide: true }).exitCode === 0;
727
+ } catch { /* treat a probe failure as not-installed and attempt install */ }
728
+ if (alreadyInstalled) return;
729
+ await runPm(mgr, ["service", "install"]);
730
+ }
731
+
679
732
  // Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
680
733
  // Both render the process name verbatim, so callers can substring-match it.
681
- async function pmList(): Promise<string> {
682
- const proc = Bun.spawn(["bunx", PM_BIN, IS_WINDOWS ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
734
+ async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
735
+ const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
683
736
  return await new Response(proc.stdout).text();
684
737
  }
685
738
 
@@ -741,25 +794,27 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
741
794
  if (isReadable(process.env.RECH_TLS_CERT)) daemonEnv.RECH_TLS_CERT = process.env.RECH_TLS_CERT!;
742
795
  if (isReadable(process.env.RECH_TLS_KEY)) daemonEnv.RECH_TLS_KEY = process.env.RECH_TLS_KEY!;
743
796
 
797
+ const mgr = daemonManager();
798
+
744
799
  // Drop any prior registration (current + legacy names) before re-adding.
745
- for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(["delete", name]);
800
+ for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
746
801
 
747
802
  let startCode: number;
748
- if (IS_WINDOWS) {
803
+ if (mgr.id === "pm2") {
749
804
  // pm2 captures the CLI env (passed via runPm's env) for the managed process,
750
805
  // autorestarts by default, and runs the bun binary directly with
751
806
  // `--interpreter none` (so it isn't fed to node).
752
- startCode = await runPm([
807
+ startCode = await runPm(mgr, [
753
808
  "start", bunBin,
754
809
  "--name", PM_PROCESS_NAME,
755
810
  "--interpreter", "none",
756
811
  "--cwd", home,
757
812
  "--", rechScript, "serve",
758
813
  ], daemonEnv);
759
- await runPm(["save"]); // persist process list for `pm2 resurrect` on reboot
814
+ await runPm(mgr, ["save"]); // persist process list for `pm2 resurrect` on reboot
760
815
  } else {
761
816
  const envArgs = Object.entries(daemonEnv).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
762
- startCode = await runPm([
817
+ startCode = await runPm(mgr, [
763
818
  "start",
764
819
  "--name", PM_PROCESS_NAME,
765
820
  "--restart", "always",
@@ -767,18 +822,23 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
767
822
  ...envArgs,
768
823
  `${bunBin} ${rechScript} serve`,
769
824
  ]);
770
- await runPm(["service", "install"]);
825
+ // Boot/login persistence: on Windows the winfix oxmgr wires Task Scheduler,
826
+ // on POSIX a systemd --user unit / launchd agent — the equivalent of the pm2
827
+ // path's `pm2 resurrect` at login. Guarded so a re-install doesn't bounce the
828
+ // live daemon.
829
+ await oxmgrEnsureAutostart(mgr);
771
830
  }
772
831
  // Surface a failed start instead of reporting a daemon that was never registered.
773
832
  if (startCode !== 0)
774
- throw new Error(`${PM_BIN} failed to start "${PM_PROCESS_NAME}" (exit ${startCode}). Check that ${PM_BIN} is installed and on PATH.`);
833
+ throw new Error(`${mgr.id} failed to start "${PM_PROCESS_NAME}" (exit ${startCode}). Check that ${mgr.id} is installed and on PATH.`);
775
834
  }
776
835
 
777
836
  async function daemonUninstall(): Promise<void> {
778
- for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(["delete", name]);
779
- if (IS_WINDOWS) await runPm(["save"]);
780
- else await runPm(["service", "uninstall"]);
781
- console.log(`Removed ${PM_BIN} process: ${PM_PROCESS_NAME}`);
837
+ const mgr = daemonManager();
838
+ for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
839
+ if (mgr.id === "pm2") await runPm(mgr, ["save"]);
840
+ else await runPm(mgr, ["service", "uninstall"]);
841
+ console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
782
842
  }
783
843
 
784
844
  // ── Native tray (menu-bar / system-tray) icon ───────────────────────────────
@@ -1460,7 +1520,7 @@ async function status(): Promise<void> {
1460
1520
  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}\``);
1461
1521
  const pmOut = await pmList();
1462
1522
  const daemonRegistered = pmOut.includes(PM_PROCESS_NAME);
1463
- console.log(`daemon: ${daemonRegistered ? `${PM_BIN} (${PM_PROCESS_NAME})` : "not installed"}`);
1523
+ console.log(`daemon: ${daemonRegistered ? `${daemonManager().id} (${PM_PROCESS_NAME})` : "not installed"}`);
1464
1524
  const registry = await readTokenRegistry();
1465
1525
  const entries = Object.entries(registry);
1466
1526
  if (entries.length) {
package/rech.ts CHANGED
@@ -6,6 +6,7 @@ import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync
6
6
  import { hostname, homedir } from "os";
7
7
  import { join, basename, dirname } from "path";
8
8
  import { spawn as cpSpawn } from "child_process";
9
+ import { pickDaemonManager, type DaemonManager } from "./daemon-manager.ts";
9
10
 
10
11
  export const ENV_KEY = "RECHROME_URL";
11
12
  export const DEFAULT_PORT = 13775;
@@ -658,28 +659,80 @@ const PM_PROCESS_NAME = "rechrome";
658
659
  // Pre-rename names to evict on (re)install/uninstall so a single `rech setup`
659
660
  // migrates an existing checkout cleanly.
660
661
  const LEGACY_PROCESS_NAMES = ["rechrome-serve"];
661
- // oxmgr everywhere, but it's unstable on Windows — fall back to pm2 there.
662
662
  const IS_WINDOWS = process.platform === "win32";
663
- const PM_BIN = IS_WINDOWS ? "pm2" : "oxmgr";
664
663
 
665
- // Spawn the active process manager. `env` is merged over process.env for the
666
- // child: pm2 captures the CLI's environment for the managed process (it has no
667
- // per-var flag like oxmgr's --env), so install passes daemon env this way.
668
- async function runPm(args: string[], env?: Record<string, string>): Promise<number> {
669
- const proc = Bun.spawn(["bunx", PM_BIN, ...args], {
664
+ // Read the installed oxmgr's version (e.g. "0.4.0+winfix"), or null if oxmgr
665
+ // isn't on PATH / doesn't answer. Cached daemonManager() sits on the hot path
666
+ // of several subcommands (status, setup, install). Synchronous by design so the
667
+ // selection has no await threading through call sites.
668
+ let _oxmgrVersion: string | null | undefined;
669
+ function oxmgrVersion(bin: string): string | null {
670
+ if (_oxmgrVersion !== undefined) return _oxmgrVersion;
671
+ try {
672
+ const p = Bun.spawnSync([bin, "--version"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
673
+ const m = /(\d+\.\d+\.\d+[^\s]*)/.exec(p.stdout?.toString() ?? "");
674
+ _oxmgrVersion = m ? m[1]! : null;
675
+ } catch {
676
+ _oxmgrVersion = null;
677
+ }
678
+ return _oxmgrVersion;
679
+ }
680
+
681
+ // Resolve (and cache) the process manager that daemonizes `rech serve`. The
682
+ // selection policy — oxmgr default, pm2 fallback, winfix-guarded on Windows,
683
+ // RECH_DAEMON_MANAGER override — lives in ./daemon-manager.ts (pure + tested);
684
+ // here we just supply the runtime inputs (what's on PATH, the oxmgr version).
685
+ let _daemonMgr: DaemonManager | undefined;
686
+ function daemonManager(): DaemonManager {
687
+ if (_daemonMgr) return _daemonMgr;
688
+ const oxmgrBin = Bun.which("oxmgr");
689
+ const pm2Bin = Bun.which("pm2");
690
+ _daemonMgr = pickDaemonManager({
691
+ oxmgrBin,
692
+ pm2Bin,
693
+ oxmgrVersion: oxmgrBin ? oxmgrVersion(oxmgrBin) : null,
694
+ isWindows: IS_WINDOWS,
695
+ override: process.env.RECH_DAEMON_MANAGER,
696
+ });
697
+ return _daemonMgr;
698
+ }
699
+
700
+ // Spawn the resolved process manager by its absolute path (Bun.which). `env` is
701
+ // merged over process.env for the child: pm2 captures the CLI's environment for
702
+ // the managed process (it has no per-var flag like oxmgr's --env), so install
703
+ // passes daemon env this way.
704
+ async function runPm(mgr: DaemonManager, args: string[], env?: Record<string, string>): Promise<number> {
705
+ const proc = Bun.spawn([mgr.bin, ...args], {
670
706
  stdout: "inherit",
671
707
  stderr: "inherit",
672
- windowsHide: true, // no console-window flash for the bunx/pm2 child on Windows
708
+ windowsHide: true, // no console-window flash for the manager child on Windows
673
709
  ...(env ? { env: { ...process.env, ...env } } : {}),
674
710
  });
675
711
  await proc.exited;
676
712
  return proc.exitCode ?? 1;
677
713
  }
678
714
 
715
+ // oxmgr boot/login autostart. `service install` wires the platform init
716
+ // integration (Windows Task Scheduler, macOS launchd, or a systemd --user unit)
717
+ // so the daemon — and the managed serve with it — returns at login/boot, the way
718
+ // the pm2 path relied on `pm2 resurrect`. Skipped when already installed:
719
+ // re-running `service install` re-bootstraps the oxmgr daemon, which restarts
720
+ // every managed process (including the live serve). Best-effort — a failure
721
+ // leaves serve crash-managed but not login-persistent.
722
+ async function oxmgrEnsureAutostart(mgr: DaemonManager): Promise<void> {
723
+ let alreadyInstalled = false;
724
+ try {
725
+ alreadyInstalled =
726
+ Bun.spawnSync([mgr.bin, "service", "status"], { stdout: "ignore", stderr: "ignore", windowsHide: true }).exitCode === 0;
727
+ } catch { /* treat a probe failure as not-installed and attempt install */ }
728
+ if (alreadyInstalled) return;
729
+ await runPm(mgr, ["service", "install"]);
730
+ }
731
+
679
732
  // Capture the process-manager's process list as text (oxmgr `list` / pm2 `jlist`).
680
733
  // Both render the process name verbatim, so callers can substring-match it.
681
- async function pmList(): Promise<string> {
682
- const proc = Bun.spawn(["bunx", PM_BIN, IS_WINDOWS ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
734
+ async function pmList(mgr: DaemonManager = daemonManager()): Promise<string> {
735
+ const proc = Bun.spawn([mgr.bin, mgr.id === "pm2" ? "jlist" : "list"], { stdout: "pipe", stderr: "ignore", windowsHide: true });
683
736
  return await new Response(proc.stdout).text();
684
737
  }
685
738
 
@@ -741,25 +794,27 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
741
794
  if (isReadable(process.env.RECH_TLS_CERT)) daemonEnv.RECH_TLS_CERT = process.env.RECH_TLS_CERT!;
742
795
  if (isReadable(process.env.RECH_TLS_KEY)) daemonEnv.RECH_TLS_KEY = process.env.RECH_TLS_KEY!;
743
796
 
797
+ const mgr = daemonManager();
798
+
744
799
  // Drop any prior registration (current + legacy names) before re-adding.
745
- for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(["delete", name]);
800
+ for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
746
801
 
747
802
  let startCode: number;
748
- if (IS_WINDOWS) {
803
+ if (mgr.id === "pm2") {
749
804
  // pm2 captures the CLI env (passed via runPm's env) for the managed process,
750
805
  // autorestarts by default, and runs the bun binary directly with
751
806
  // `--interpreter none` (so it isn't fed to node).
752
- startCode = await runPm([
807
+ startCode = await runPm(mgr, [
753
808
  "start", bunBin,
754
809
  "--name", PM_PROCESS_NAME,
755
810
  "--interpreter", "none",
756
811
  "--cwd", home,
757
812
  "--", rechScript, "serve",
758
813
  ], daemonEnv);
759
- await runPm(["save"]); // persist process list for `pm2 resurrect` on reboot
814
+ await runPm(mgr, ["save"]); // persist process list for `pm2 resurrect` on reboot
760
815
  } else {
761
816
  const envArgs = Object.entries(daemonEnv).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
762
- startCode = await runPm([
817
+ startCode = await runPm(mgr, [
763
818
  "start",
764
819
  "--name", PM_PROCESS_NAME,
765
820
  "--restart", "always",
@@ -767,18 +822,23 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
767
822
  ...envArgs,
768
823
  `${bunBin} ${rechScript} serve`,
769
824
  ]);
770
- await runPm(["service", "install"]);
825
+ // Boot/login persistence: on Windows the winfix oxmgr wires Task Scheduler,
826
+ // on POSIX a systemd --user unit / launchd agent — the equivalent of the pm2
827
+ // path's `pm2 resurrect` at login. Guarded so a re-install doesn't bounce the
828
+ // live daemon.
829
+ await oxmgrEnsureAutostart(mgr);
771
830
  }
772
831
  // Surface a failed start instead of reporting a daemon that was never registered.
773
832
  if (startCode !== 0)
774
- throw new Error(`${PM_BIN} failed to start "${PM_PROCESS_NAME}" (exit ${startCode}). Check that ${PM_BIN} is installed and on PATH.`);
833
+ throw new Error(`${mgr.id} failed to start "${PM_PROCESS_NAME}" (exit ${startCode}). Check that ${mgr.id} is installed and on PATH.`);
775
834
  }
776
835
 
777
836
  async function daemonUninstall(): Promise<void> {
778
- for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(["delete", name]);
779
- if (IS_WINDOWS) await runPm(["save"]);
780
- else await runPm(["service", "uninstall"]);
781
- console.log(`Removed ${PM_BIN} process: ${PM_PROCESS_NAME}`);
837
+ const mgr = daemonManager();
838
+ for (const name of [PM_PROCESS_NAME, ...LEGACY_PROCESS_NAMES]) await runPm(mgr, ["delete", name]);
839
+ if (mgr.id === "pm2") await runPm(mgr, ["save"]);
840
+ else await runPm(mgr, ["service", "uninstall"]);
841
+ console.log(`Removed ${mgr.id} process: ${PM_PROCESS_NAME}`);
782
842
  }
783
843
 
784
844
  // ── Native tray (menu-bar / system-tray) icon ───────────────────────────────
@@ -1460,7 +1520,7 @@ async function status(): Promise<void> {
1460
1520
  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}\``);
1461
1521
  const pmOut = await pmList();
1462
1522
  const daemonRegistered = pmOut.includes(PM_PROCESS_NAME);
1463
- console.log(`daemon: ${daemonRegistered ? `${PM_BIN} (${PM_PROCESS_NAME})` : "not installed"}`);
1523
+ console.log(`daemon: ${daemonRegistered ? `${daemonManager().id} (${PM_PROCESS_NAME})` : "not installed"}`);
1464
1524
  const registry = await readTokenRegistry();
1465
1525
  const entries = Object.entries(registry);
1466
1526
  if (entries.length) {