botsync 0.1.1 → 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.
- package/dist/commands/init.js +9 -0
- package/dist/commands/join.js +13 -1
- package/dist/commands/stop.js +2 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +21 -1
- package/dist/heartbeat-daemon.d.ts +11 -0
- package/dist/heartbeat-daemon.js +99 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +66 -0
- package/dist/passphrase.d.ts +1 -0
- package/dist/passphrase.js +3 -2
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -51,6 +51,7 @@ const crypto_1 = require("crypto");
|
|
|
51
51
|
const config_js_1 = require("../config.js");
|
|
52
52
|
const syncthing_js_1 = require("../syncthing.js");
|
|
53
53
|
const passphrase_js_1 = require("../passphrase.js");
|
|
54
|
+
const heartbeat_js_1 = require("../heartbeat.js");
|
|
54
55
|
const ui = __importStar(require("../ui.js"));
|
|
55
56
|
/**
|
|
56
57
|
* Pick a random port in the ephemeral range for the Syncthing REST API.
|
|
@@ -87,11 +88,17 @@ async function init() {
|
|
|
87
88
|
spin.succeed();
|
|
88
89
|
const deviceId = await (0, syncthing_js_1.getDeviceId)();
|
|
89
90
|
(0, config_js_1.writeConfig)({ apiKey, apiPort, deviceId });
|
|
91
|
+
// Step 5b: Generate network ID and start heartbeat
|
|
92
|
+
const networkId = (0, crypto_1.randomUUID)();
|
|
93
|
+
(0, config_js_1.writeNetworkId)(networkId);
|
|
94
|
+
(0, heartbeat_js_1.startHeartbeat)();
|
|
95
|
+
ui.stepDone("Network registered");
|
|
90
96
|
// Step 6: Register with relay and display the code
|
|
91
97
|
ui.gap();
|
|
92
98
|
const { code, isRelay } = await (0, passphrase_js_1.createCode)({
|
|
93
99
|
deviceId,
|
|
94
100
|
folders: config_js_1.FOLDERS.map((f) => f.id),
|
|
101
|
+
networkId,
|
|
95
102
|
});
|
|
96
103
|
if (isRelay) {
|
|
97
104
|
ui.passphraseBox(code, `npx botsync join ${code}`);
|
|
@@ -103,6 +110,8 @@ async function init() {
|
|
|
103
110
|
ui.passphraseBox(code, `npx botsync join ${code.substring(0, 20)}...`);
|
|
104
111
|
}
|
|
105
112
|
ui.gap();
|
|
113
|
+
ui.info(`Dashboard: https://botsync.io/dashboard#${networkId}`);
|
|
114
|
+
ui.gap();
|
|
106
115
|
// Step 7: Wait for the joining device to connect and auto-accept it
|
|
107
116
|
const peerSpin = ui.spinner("Waiting for peer...");
|
|
108
117
|
const accepted = await waitForPeer(apiKey, apiPort, peerSpin);
|
package/dist/commands/join.js
CHANGED
|
@@ -51,6 +51,7 @@ const crypto_1 = require("crypto");
|
|
|
51
51
|
const config_js_1 = require("../config.js");
|
|
52
52
|
const syncthing_js_1 = require("../syncthing.js");
|
|
53
53
|
const passphrase_js_1 = require("../passphrase.js");
|
|
54
|
+
const heartbeat_js_1 = require("../heartbeat.js");
|
|
54
55
|
const ui = __importStar(require("../ui.js"));
|
|
55
56
|
async function join(passphrase) {
|
|
56
57
|
ui.header();
|
|
@@ -58,10 +59,12 @@ async function join(passphrase) {
|
|
|
58
59
|
const spin0 = ui.spinner("Resolving pairing code...");
|
|
59
60
|
let remoteId;
|
|
60
61
|
let folders;
|
|
62
|
+
let networkId;
|
|
61
63
|
try {
|
|
62
64
|
const data = await (0, passphrase_js_1.resolveCode)(passphrase);
|
|
63
65
|
remoteId = data.deviceId;
|
|
64
66
|
folders = data.folders;
|
|
67
|
+
networkId = data.networkId;
|
|
65
68
|
spin0.succeed();
|
|
66
69
|
}
|
|
67
70
|
catch (err) {
|
|
@@ -102,5 +105,14 @@ async function join(passphrase) {
|
|
|
102
105
|
await (0, syncthing_js_1.addDeviceToFolder)(folderId, remoteId);
|
|
103
106
|
}
|
|
104
107
|
spin2.stop();
|
|
105
|
-
|
|
108
|
+
// Save network ID (inherited from init side) and start heartbeat
|
|
109
|
+
if (networkId) {
|
|
110
|
+
(0, config_js_1.writeNetworkId)(networkId);
|
|
111
|
+
(0, heartbeat_js_1.startHeartbeat)();
|
|
112
|
+
ui.connected(remoteId);
|
|
113
|
+
ui.info(`Dashboard: https://botsync.io/dashboard#${networkId}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
ui.connected(remoteId);
|
|
117
|
+
}
|
|
106
118
|
}
|
package/dist/commands/stop.js
CHANGED
|
@@ -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();
|
package/dist/config.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const FOLDERS: {
|
|
|
19
19
|
id: string;
|
|
20
20
|
path: string;
|
|
21
21
|
}[];
|
|
22
|
+
export declare const NETWORK_FILE: string;
|
|
22
23
|
/** Runtime config shape — everything we need to talk to Syncthing */
|
|
23
24
|
export interface BotsyncConfig {
|
|
24
25
|
apiKey: string;
|
|
@@ -29,3 +30,7 @@ export interface BotsyncConfig {
|
|
|
29
30
|
export declare function readConfig(): BotsyncConfig | null;
|
|
30
31
|
/** Write config to disk. Creates parent dirs if needed. */
|
|
31
32
|
export declare function writeConfig(config: BotsyncConfig): void;
|
|
33
|
+
/** Read the network ID, or return null if not yet assigned */
|
|
34
|
+
export declare function readNetworkId(): string | null;
|
|
35
|
+
/** Write the network ID to disk. */
|
|
36
|
+
export declare function writeNetworkId(networkId: string): void;
|
package/dist/config.js
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* and PID so that all commands can talk to the running Syncthing instance.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.FOLDERS = exports.PID_FILE = exports.CONFIG_FILE = exports.SYNCTHING_BIN = exports.SYNCTHING_BIN_DIR = exports.SYNCTHING_CONFIG_DIR = exports.BOTSYNC_DIR = exports.SYNC_DIR = void 0;
|
|
13
|
+
exports.NETWORK_FILE = exports.FOLDERS = exports.PID_FILE = exports.CONFIG_FILE = exports.SYNCTHING_BIN = exports.SYNCTHING_BIN_DIR = exports.SYNCTHING_CONFIG_DIR = exports.BOTSYNC_DIR = exports.SYNC_DIR = void 0;
|
|
14
14
|
exports.readConfig = readConfig;
|
|
15
15
|
exports.writeConfig = writeConfig;
|
|
16
|
+
exports.readNetworkId = readNetworkId;
|
|
17
|
+
exports.writeNetworkId = writeNetworkId;
|
|
16
18
|
const fs_1 = require("fs");
|
|
17
19
|
const path_1 = require("path");
|
|
18
20
|
const os_1 = require("os");
|
|
@@ -38,6 +40,8 @@ exports.FOLDERS = [
|
|
|
38
40
|
{ id: "botsync-deliverables", path: (0, path_1.join)(exports.SYNC_DIR, "deliverables") },
|
|
39
41
|
{ id: "botsync-inbox", path: (0, path_1.join)(exports.SYNC_DIR, "inbox") },
|
|
40
42
|
];
|
|
43
|
+
// Network identity file — stores the network ID for dashboard visibility
|
|
44
|
+
exports.NETWORK_FILE = (0, path_1.join)(exports.BOTSYNC_DIR, "network.json");
|
|
41
45
|
/** Read the config file, or return null if it doesn't exist */
|
|
42
46
|
function readConfig() {
|
|
43
47
|
try {
|
|
@@ -53,3 +57,19 @@ function writeConfig(config) {
|
|
|
53
57
|
(0, fs_1.mkdirSync)(exports.BOTSYNC_DIR, { recursive: true });
|
|
54
58
|
(0, fs_1.writeFileSync)(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
55
59
|
}
|
|
60
|
+
/** Read the network ID, or return null if not yet assigned */
|
|
61
|
+
function readNetworkId() {
|
|
62
|
+
try {
|
|
63
|
+
const raw = (0, fs_1.readFileSync)(exports.NETWORK_FILE, "utf-8");
|
|
64
|
+
const data = JSON.parse(raw);
|
|
65
|
+
return data.networkId || null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Write the network ID to disk. */
|
|
72
|
+
function writeNetworkId(networkId) {
|
|
73
|
+
(0, fs_1.mkdirSync)(exports.BOTSYNC_DIR, { recursive: true });
|
|
74
|
+
(0, fs_1.writeFileSync)(exports.NETWORK_FILE, JSON.stringify({ networkId }, null, 2));
|
|
75
|
+
}
|
|
@@ -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));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heartbeat.ts — Spawn/stop the background heartbeat daemon.
|
|
3
|
+
*
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
/** Spawn the heartbeat daemon as a detached background process. */
|
|
9
|
+
export declare function startHeartbeat(): void;
|
|
10
|
+
/** Stop the heartbeat daemon if running. */
|
|
11
|
+
export declare function stopHeartbeat(): void;
|
|
12
|
+
/** Check if the heartbeat daemon is still alive. */
|
|
13
|
+
export declare function isHeartbeatRunning(): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* heartbeat.ts — Spawn/stop the background heartbeat daemon.
|
|
4
|
+
*
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.startHeartbeat = startHeartbeat;
|
|
11
|
+
exports.stopHeartbeat = stopHeartbeat;
|
|
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
|
+
const config_js_1 = require("./config.js");
|
|
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())
|
|
22
|
+
return;
|
|
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();
|
|
29
|
+
}
|
|
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
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
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;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getHeartbeatPid() {
|
|
56
|
+
try {
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/passphrase.d.ts
CHANGED
package/dist/passphrase.js
CHANGED
|
@@ -32,7 +32,7 @@ async function createCode(data) {
|
|
|
32
32
|
const res = await fetch(`${RELAY_URL}/pair`, {
|
|
33
33
|
method: "POST",
|
|
34
34
|
headers: { "Content-Type": "application/json" },
|
|
35
|
-
body: JSON.stringify({ deviceId: data.deviceId }),
|
|
35
|
+
body: JSON.stringify({ deviceId: data.deviceId, networkId: data.networkId }),
|
|
36
36
|
signal: AbortSignal.timeout(5000),
|
|
37
37
|
});
|
|
38
38
|
if (res.ok) {
|
|
@@ -64,11 +64,12 @@ async function resolveCode(code) {
|
|
|
64
64
|
const body = (await res.json());
|
|
65
65
|
throw new Error(body.error || `Relay returned ${res.status}`);
|
|
66
66
|
}
|
|
67
|
-
const { deviceId } = (await res.json());
|
|
67
|
+
const { deviceId, networkId } = (await res.json());
|
|
68
68
|
// Folders are always the standard set — no need to encode them
|
|
69
69
|
return {
|
|
70
70
|
deviceId,
|
|
71
71
|
folders: ["botsync-shared", "botsync-deliverables", "botsync-inbox"],
|
|
72
|
+
networkId: networkId || undefined,
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
// Legacy base58 fallback
|