botsync 0.2.0 → 0.2.1

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.
@@ -40,8 +40,10 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.stop = stop;
42
42
  const syncthing_js_1 = require("../syncthing.js");
43
+ const heartbeat_js_1 = require("../heartbeat.js");
43
44
  const ui = __importStar(require("../ui.js"));
44
45
  async function stop() {
46
+ (0, heartbeat_js_1.stopHeartbeat)();
45
47
  const killed = (0, syncthing_js_1.stopDaemon)();
46
48
  if (killed) {
47
49
  ui.stopped();
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * heartbeat-daemon.ts — Standalone background heartbeat process.
4
+ *
5
+ * Spawned as a detached child by init/join. Sends heartbeats every 60s
6
+ * until the Syncthing daemon dies (checked via API ping), then exits.
7
+ *
8
+ * Usage: node heartbeat-daemon.js
9
+ * (Not meant to be run directly — spawned by init/join)
10
+ */
11
+ export {};
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * heartbeat-daemon.ts — Standalone background heartbeat process.
5
+ *
6
+ * Spawned as a detached child by init/join. Sends heartbeats every 60s
7
+ * until the Syncthing daemon dies (checked via API ping), then exits.
8
+ *
9
+ * Usage: node heartbeat-daemon.js
10
+ * (Not meant to be run directly — spawned by init/join)
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const os_1 = require("os");
14
+ const config_js_1 = require("./config.js");
15
+ const fs_1 = require("fs");
16
+ const path_1 = require("path");
17
+ const RELAY_URL = "https://relay.botsync.io";
18
+ const HEARTBEAT_INTERVAL_MS = 60_000;
19
+ const HEALTH_CHECK_INTERVAL_MS = 120_000; // Check if Syncthing is alive every 2 min
20
+ const PID_FILE = (0, path_1.join)(config_js_1.BOTSYNC_DIR, "heartbeat.pid");
21
+ function getVersion() {
22
+ try {
23
+ return require("../package.json").version || "0.0.0";
24
+ }
25
+ catch {
26
+ return "0.0.0";
27
+ }
28
+ }
29
+ async function sendHeartbeat() {
30
+ const config = (0, config_js_1.readConfig)();
31
+ const networkId = (0, config_js_1.readNetworkId)();
32
+ if (!config?.deviceId || !networkId)
33
+ return false;
34
+ try {
35
+ const res = await fetch(`${RELAY_URL}/network/${encodeURIComponent(networkId)}/heartbeat`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({
39
+ deviceId: config.deviceId,
40
+ name: (0, os_1.hostname)(),
41
+ os: process.platform,
42
+ version: getVersion(),
43
+ }),
44
+ signal: AbortSignal.timeout(5000),
45
+ });
46
+ return res.ok;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function isSyncthingAlive() {
53
+ const config = (0, config_js_1.readConfig)();
54
+ if (!config)
55
+ return false;
56
+ try {
57
+ const res = await fetch(`http://127.0.0.1:${config.apiPort}/rest/system/ping`, {
58
+ headers: { "X-API-Key": config.apiKey },
59
+ signal: AbortSignal.timeout(3000),
60
+ });
61
+ return res.ok;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ async function main() {
68
+ // Write our PID so stop can kill us
69
+ (0, fs_1.writeFileSync)(PID_FILE, String(process.pid));
70
+ // Cleanup PID file on exit
71
+ const cleanup = () => {
72
+ try {
73
+ (0, fs_1.unlinkSync)(PID_FILE);
74
+ }
75
+ catch { }
76
+ };
77
+ process.on("exit", cleanup);
78
+ process.on("SIGTERM", () => {
79
+ cleanup();
80
+ process.exit(0);
81
+ });
82
+ process.on("SIGINT", () => {
83
+ cleanup();
84
+ process.exit(0);
85
+ });
86
+ // Initial heartbeat
87
+ await sendHeartbeat();
88
+ // Heartbeat loop
89
+ setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
90
+ // Health check loop — exit if Syncthing is gone
91
+ setInterval(async () => {
92
+ const alive = await isSyncthingAlive();
93
+ if (!alive) {
94
+ cleanup();
95
+ process.exit(0);
96
+ }
97
+ }, HEALTH_CHECK_INTERVAL_MS);
98
+ }
99
+ main().catch(() => process.exit(1));
@@ -1,14 +1,13 @@
1
1
  /**
2
- * heartbeat.ts — Periodic heartbeat to the botsync relay.
2
+ * heartbeat.ts — Spawn/stop the background heartbeat daemon.
3
3
  *
4
- * Sends device info every 60s so the dashboard at botsync.io/dashboard
5
- * can show connected devices. The relay stores each heartbeat in KV
6
- * with a 5-minute TTL — if we stop heartbeating, we disappear.
7
- *
8
- * The heartbeat runs in the background and never throws — a failed
9
- * heartbeat just means we're invisible on the dashboard for a cycle.
4
+ * Instead of running in-process (which dies when init/join exits),
5
+ * we spawn heartbeat-daemon.js as a detached child process that
6
+ * outlives the parent CLI command.
10
7
  */
11
- /** Start the heartbeat loop. Sends one immediately, then every 60s. */
8
+ /** Spawn the heartbeat daemon as a detached background process. */
12
9
  export declare function startHeartbeat(): void;
13
- /** Stop the heartbeat loop. */
10
+ /** Stop the heartbeat daemon if running. */
14
11
  export declare function stopHeartbeat(): void;
12
+ /** Check if the heartbeat daemon is still alive. */
13
+ export declare function isHeartbeatRunning(): boolean;
package/dist/heartbeat.js CHANGED
@@ -1,73 +1,66 @@
1
1
  "use strict";
2
2
  /**
3
- * heartbeat.ts — Periodic heartbeat to the botsync relay.
3
+ * heartbeat.ts — Spawn/stop the background heartbeat daemon.
4
4
  *
5
- * Sends device info every 60s so the dashboard at botsync.io/dashboard
6
- * can show connected devices. The relay stores each heartbeat in KV
7
- * with a 5-minute TTL — if we stop heartbeating, we disappear.
8
- *
9
- * The heartbeat runs in the background and never throws — a failed
10
- * heartbeat just means we're invisible on the dashboard for a cycle.
5
+ * Instead of running in-process (which dies when init/join exits),
6
+ * we spawn heartbeat-daemon.js as a detached child process that
7
+ * outlives the parent CLI command.
11
8
  */
12
9
  Object.defineProperty(exports, "__esModule", { value: true });
13
10
  exports.startHeartbeat = startHeartbeat;
14
11
  exports.stopHeartbeat = stopHeartbeat;
15
- const os_1 = require("os");
12
+ exports.isHeartbeatRunning = isHeartbeatRunning;
13
+ const child_process_1 = require("child_process");
14
+ const fs_1 = require("fs");
15
+ const path_1 = require("path");
16
16
  const config_js_1 = require("./config.js");
17
- const RELAY_URL = "https://relay.botsync.io";
18
- const HEARTBEAT_INTERVAL_MS = 60_000;
19
- let heartbeatTimer = null;
20
- /** Send a single heartbeat to the relay. */
21
- async function sendHeartbeat() {
22
- const config = (0, config_js_1.readConfig)();
23
- const networkId = (0, config_js_1.readNetworkId)();
24
- if (!config?.deviceId || !networkId)
17
+ const HEARTBEAT_PID_FILE = (0, path_1.join)(config_js_1.BOTSYNC_DIR, "heartbeat.pid");
18
+ /** Spawn the heartbeat daemon as a detached background process. */
19
+ function startHeartbeat() {
20
+ // Don't double-spawn
21
+ if (isHeartbeatRunning())
25
22
  return;
26
- try {
27
- await fetch(`${RELAY_URL}/network/${encodeURIComponent(networkId)}/heartbeat`, {
28
- method: "POST",
29
- headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify({
31
- deviceId: config.deviceId,
32
- name: (0, os_1.hostname)(),
33
- os: process.platform,
34
- version: getVersion(),
35
- }),
36
- signal: AbortSignal.timeout(5000),
37
- });
38
- }
39
- catch {
40
- // Silent fail — dashboard visibility is best-effort
41
- }
23
+ const daemonScript = (0, path_1.join)((0, path_1.dirname)(__filename), "heartbeat-daemon.js");
24
+ const child = (0, child_process_1.spawn)(process.execPath, [daemonScript], {
25
+ detached: true,
26
+ stdio: "ignore",
27
+ });
28
+ child.unref();
42
29
  }
43
- /** Start the heartbeat loop. Sends one immediately, then every 60s. */
44
- function startHeartbeat() {
45
- if (heartbeatTimer)
46
- return; // Already running
47
- // Fire immediately so the device shows up right away
48
- sendHeartbeat();
49
- heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
50
- // Don't let the heartbeat timer keep the process alive if
51
- // everything else is done (shouldn't happen since Syncthing
52
- // daemon is the main keep-alive, but just in case)
53
- if (heartbeatTimer.unref) {
54
- heartbeatTimer.unref();
30
+ /** Stop the heartbeat daemon if running. */
31
+ function stopHeartbeat() {
32
+ const pid = getHeartbeatPid();
33
+ if (pid) {
34
+ try {
35
+ process.kill(pid, "SIGTERM");
36
+ }
37
+ catch {
38
+ // Already dead
39
+ }
55
40
  }
56
41
  }
57
- /** Stop the heartbeat loop. */
58
- function stopHeartbeat() {
59
- if (heartbeatTimer) {
60
- clearInterval(heartbeatTimer);
61
- heartbeatTimer = null;
42
+ /** Check if the heartbeat daemon is still alive. */
43
+ function isHeartbeatRunning() {
44
+ const pid = getHeartbeatPid();
45
+ if (!pid)
46
+ return false;
47
+ try {
48
+ process.kill(pid, 0); // Signal 0 = just check if alive
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
62
53
  }
63
54
  }
64
- /** Read package version, fallback to 0.0.0 */
65
- function getVersion() {
55
+ function getHeartbeatPid() {
66
56
  try {
67
- const pkg = require("../package.json");
68
- return pkg.version || "0.0.0";
57
+ if (!(0, fs_1.existsSync)(HEARTBEAT_PID_FILE))
58
+ return null;
59
+ const raw = (0, fs_1.readFileSync)(HEARTBEAT_PID_FILE, "utf-8").trim();
60
+ const pid = parseInt(raw, 10);
61
+ return isNaN(pid) ? null : pid;
69
62
  }
70
63
  catch {
71
- return "0.0.0";
64
+ return null;
72
65
  }
73
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botsync",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "P2P file sync for AI agents. Syncthing under the hood. Two commands.",
5
5
  "bin": {
6
6
  "botsync": "dist/cli.js"