botsync 0.1.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/README.md +37 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +102 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/join.d.ts +13 -0
- package/dist/commands/join.js +106 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/stop.d.ts +6 -0
- package/dist/commands/stop.js +52 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +55 -0
- package/dist/passphrase.d.ts +31 -0
- package/dist/passphrase.js +89 -0
- package/dist/syncthing.d.ts +80 -0
- package/dist/syncthing.js +312 -0
- package/dist/ui.d.ts +47 -0
- package/dist/ui.js +148 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# botsync
|
|
2
|
+
|
|
3
|
+
P2P file sync for AI agents. Two commands. No server.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# On machine A (your agent):
|
|
9
|
+
npx botsync init
|
|
10
|
+
# Share the passphrase with machine B
|
|
11
|
+
|
|
12
|
+
# On machine B (you):
|
|
13
|
+
npx botsync join <passphrase>
|
|
14
|
+
|
|
15
|
+
# That's it. ~/sync/ is now shared.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Folder Convention
|
|
19
|
+
|
|
20
|
+
| Folder | Purpose |
|
|
21
|
+
|--------|---------|
|
|
22
|
+
| `shared/` | Bidirectional — both sides read and write |
|
|
23
|
+
| `deliverables/` | Agent drops outputs here for human review |
|
|
24
|
+
| `inbox/` | Human drops files here for agent to process |
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
botsync init # Initialize and start syncing
|
|
30
|
+
botsync join <passphrase> # Connect to another botsync instance
|
|
31
|
+
botsync status # Show sync status
|
|
32
|
+
botsync stop # Stop the sync daemon
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How It Works
|
|
36
|
+
|
|
37
|
+
botsync wraps [Syncthing](https://syncthing.net/) — a battle-tested, open-source P2P sync engine. No central server, no cloud accounts, no configuration files to edit. Just a passphrase that encodes everything needed to connect two machines.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* cli.ts — Entry point for the botsync CLI.
|
|
5
|
+
*
|
|
6
|
+
* Routes commands to their handlers. The shebang line makes it
|
|
7
|
+
* executable as `npx botsync` or a globally installed CLI.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
const commander_1 = require("commander");
|
|
44
|
+
const init_js_1 = require("./commands/init.js");
|
|
45
|
+
const join_js_1 = require("./commands/join.js");
|
|
46
|
+
const status_js_1 = require("./commands/status.js");
|
|
47
|
+
const stop_js_1 = require("./commands/stop.js");
|
|
48
|
+
const ui = __importStar(require("./ui.js"));
|
|
49
|
+
const program = new commander_1.Command();
|
|
50
|
+
program
|
|
51
|
+
.name("botsync")
|
|
52
|
+
.description("P2P file sync for AI agents.")
|
|
53
|
+
.version("0.1.0");
|
|
54
|
+
program
|
|
55
|
+
.command("init")
|
|
56
|
+
.description("Initialize botsync and start syncing. Prints a passphrase for pairing.")
|
|
57
|
+
.action(async () => {
|
|
58
|
+
try {
|
|
59
|
+
await (0, init_js_1.init)();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
program
|
|
67
|
+
.command("join <passphrase>")
|
|
68
|
+
.description("Connect to another botsync instance using a passphrase.")
|
|
69
|
+
.action(async (passphrase) => {
|
|
70
|
+
try {
|
|
71
|
+
await (0, join_js_1.join)(passphrase);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
program
|
|
79
|
+
.command("status")
|
|
80
|
+
.description("Show sync status — peers, folders, sync progress.")
|
|
81
|
+
.action(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await (0, status_js_1.status)();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command("stop")
|
|
92
|
+
.description("Stop the botsync daemon.")
|
|
93
|
+
.action(async () => {
|
|
94
|
+
try {
|
|
95
|
+
await (0, stop_js_1.stop)();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
program.parse();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init.ts — The `botsync init` command.
|
|
3
|
+
*
|
|
4
|
+
* This is the "machine A" side of the pairing flow. It:
|
|
5
|
+
* 1. Creates the ~/sync/ directory structure
|
|
6
|
+
* 2. Downloads Syncthing if needed
|
|
7
|
+
* 3. Generates config with sane defaults
|
|
8
|
+
* 4. Starts the daemon
|
|
9
|
+
* 5. Waits for it to come online
|
|
10
|
+
* 6. Produces a passphrase that encodes everything "machine B" needs to connect
|
|
11
|
+
* 7. Polls for the joining device and auto-accepts it
|
|
12
|
+
*/
|
|
13
|
+
export declare function init(): Promise<void>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* init.ts — The `botsync init` command.
|
|
4
|
+
*
|
|
5
|
+
* This is the "machine A" side of the pairing flow. It:
|
|
6
|
+
* 1. Creates the ~/sync/ directory structure
|
|
7
|
+
* 2. Downloads Syncthing if needed
|
|
8
|
+
* 3. Generates config with sane defaults
|
|
9
|
+
* 4. Starts the daemon
|
|
10
|
+
* 5. Waits for it to come online
|
|
11
|
+
* 6. Produces a passphrase that encodes everything "machine B" needs to connect
|
|
12
|
+
* 7. Polls for the joining device and auto-accepts it
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.init = init;
|
|
49
|
+
const fs_1 = require("fs");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
51
|
+
const config_js_1 = require("../config.js");
|
|
52
|
+
const syncthing_js_1 = require("../syncthing.js");
|
|
53
|
+
const passphrase_js_1 = require("../passphrase.js");
|
|
54
|
+
const ui = __importStar(require("../ui.js"));
|
|
55
|
+
/**
|
|
56
|
+
* Pick a random port in the ephemeral range for the Syncthing REST API.
|
|
57
|
+
* We avoid 8384 (Syncthing's default) to not conflict with any existing
|
|
58
|
+
* Syncthing installation the user might have.
|
|
59
|
+
*/
|
|
60
|
+
function randomPort() {
|
|
61
|
+
return 27000 + Math.floor(Math.random() * 10000);
|
|
62
|
+
}
|
|
63
|
+
async function init() {
|
|
64
|
+
ui.header();
|
|
65
|
+
// Step 1: Create directory structure
|
|
66
|
+
for (const folder of config_js_1.FOLDERS) {
|
|
67
|
+
(0, fs_1.mkdirSync)(folder.path, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
(0, fs_1.mkdirSync)(config_js_1.BOTSYNC_DIR, { recursive: true });
|
|
70
|
+
(0, fs_1.mkdirSync)(config_js_1.SYNCTHING_CONFIG_DIR, { recursive: true });
|
|
71
|
+
ui.stepDone("Sync folders created");
|
|
72
|
+
// Step 2: Download Syncthing binary (skips if already present)
|
|
73
|
+
await (0, syncthing_js_1.downloadSyncthing)();
|
|
74
|
+
ui.stepDone("Syncthing ready");
|
|
75
|
+
// Step 3: Generate config
|
|
76
|
+
const apiKey = (0, crypto_1.randomUUID)();
|
|
77
|
+
const apiPort = randomPort();
|
|
78
|
+
const configXml = (0, syncthing_js_1.generateConfig)(apiKey, apiPort);
|
|
79
|
+
(0, fs_1.writeFileSync)(`${config_js_1.SYNCTHING_CONFIG_DIR}/config.xml`, configXml);
|
|
80
|
+
(0, config_js_1.writeConfig)({ apiKey, apiPort });
|
|
81
|
+
// Step 4: Start the daemon
|
|
82
|
+
const pid = (0, syncthing_js_1.startDaemon)();
|
|
83
|
+
ui.stepDone(`Daemon started (PID ${pid})`);
|
|
84
|
+
// Step 5: Wait for Syncthing to be ready and get our device ID
|
|
85
|
+
const spin = ui.spinner("Starting Syncthing...");
|
|
86
|
+
await (0, syncthing_js_1.waitForStart)();
|
|
87
|
+
spin.succeed();
|
|
88
|
+
const deviceId = await (0, syncthing_js_1.getDeviceId)();
|
|
89
|
+
(0, config_js_1.writeConfig)({ apiKey, apiPort, deviceId });
|
|
90
|
+
// Step 6: Register with relay and display the code
|
|
91
|
+
ui.gap();
|
|
92
|
+
const { code, isRelay } = await (0, passphrase_js_1.createCode)({
|
|
93
|
+
deviceId,
|
|
94
|
+
folders: config_js_1.FOLDERS.map((f) => f.id),
|
|
95
|
+
});
|
|
96
|
+
if (isRelay) {
|
|
97
|
+
ui.passphraseBox(code, `npx botsync join ${code}`);
|
|
98
|
+
ui.info("Code expires in 10 minutes.");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
ui.info("Relay unavailable — using offline passphrase:");
|
|
102
|
+
ui.gap();
|
|
103
|
+
ui.passphraseBox(code, `npx botsync join ${code.substring(0, 20)}...`);
|
|
104
|
+
}
|
|
105
|
+
ui.gap();
|
|
106
|
+
// Step 7: Wait for the joining device to connect and auto-accept it
|
|
107
|
+
const peerSpin = ui.spinner("Waiting for peer...");
|
|
108
|
+
const accepted = await waitForPeer(apiKey, apiPort, peerSpin);
|
|
109
|
+
if (accepted) {
|
|
110
|
+
peerSpin.stop();
|
|
111
|
+
ui.paired(accepted);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Poll for pending device connections and auto-accept the first one.
|
|
116
|
+
* After accepting the device, share all botsync folders with it.
|
|
117
|
+
*
|
|
118
|
+
* Times out after 5 minutes — if nobody joins by then, they can still
|
|
119
|
+
* join later (they'll just show up as pending until the user restarts
|
|
120
|
+
* init or manually approves via the API).
|
|
121
|
+
*/
|
|
122
|
+
async function waitForPeer(apiKey, apiPort, spin, timeoutMs = 300_000) {
|
|
123
|
+
const start = Date.now();
|
|
124
|
+
const pollInterval = 2000;
|
|
125
|
+
while (Date.now() - start < timeoutMs) {
|
|
126
|
+
try {
|
|
127
|
+
const pending = await (0, syncthing_js_1.apiCall)("GET", "/rest/cluster/pending/devices");
|
|
128
|
+
const deviceIds = Object.keys(pending);
|
|
129
|
+
if (deviceIds.length > 0) {
|
|
130
|
+
const peerId = deviceIds[0];
|
|
131
|
+
spin.text = `Accepting ${peerId.substring(0, 7)}...`;
|
|
132
|
+
// Add the device to our config
|
|
133
|
+
await (0, syncthing_js_1.addDevice)(peerId);
|
|
134
|
+
// Share all folders with it
|
|
135
|
+
for (const folder of config_js_1.FOLDERS) {
|
|
136
|
+
await (0, syncthing_js_1.addDeviceToFolder)(folder.id, peerId);
|
|
137
|
+
}
|
|
138
|
+
return peerId;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// API might hiccup during config changes — ignore and retry
|
|
143
|
+
}
|
|
144
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
145
|
+
}
|
|
146
|
+
spin.stop();
|
|
147
|
+
ui.info("No peer connected within 5 minutes.");
|
|
148
|
+
ui.info("The passphrase is still valid — join anytime, then restart init.");
|
|
149
|
+
ui.gap();
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* join.ts — The `botsync join <passphrase>` command.
|
|
3
|
+
*
|
|
4
|
+
* This is the "machine B" side of the pairing flow. It:
|
|
5
|
+
* 1. Decodes the passphrase to get the init side's device ID + folder list
|
|
6
|
+
* 2. Creates the local directory structure
|
|
7
|
+
* 3. Downloads Syncthing if needed
|
|
8
|
+
* 4. Generates config
|
|
9
|
+
* 5. Starts the daemon
|
|
10
|
+
* 6. Adds the init side as a known device
|
|
11
|
+
* 7. Shares all folders with the init side
|
|
12
|
+
*/
|
|
13
|
+
export declare function join(passphrase: string): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* join.ts — The `botsync join <passphrase>` command.
|
|
4
|
+
*
|
|
5
|
+
* This is the "machine B" side of the pairing flow. It:
|
|
6
|
+
* 1. Decodes the passphrase to get the init side's device ID + folder list
|
|
7
|
+
* 2. Creates the local directory structure
|
|
8
|
+
* 3. Downloads Syncthing if needed
|
|
9
|
+
* 4. Generates config
|
|
10
|
+
* 5. Starts the daemon
|
|
11
|
+
* 6. Adds the init side as a known device
|
|
12
|
+
* 7. Shares all folders with the init side
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.join = join;
|
|
49
|
+
const fs_1 = require("fs");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
51
|
+
const config_js_1 = require("../config.js");
|
|
52
|
+
const syncthing_js_1 = require("../syncthing.js");
|
|
53
|
+
const passphrase_js_1 = require("../passphrase.js");
|
|
54
|
+
const ui = __importStar(require("../ui.js"));
|
|
55
|
+
async function join(passphrase) {
|
|
56
|
+
ui.header();
|
|
57
|
+
// Resolve the code — tries relay first, falls back to base58
|
|
58
|
+
const spin0 = ui.spinner("Resolving pairing code...");
|
|
59
|
+
let remoteId;
|
|
60
|
+
let folders;
|
|
61
|
+
try {
|
|
62
|
+
const data = await (0, passphrase_js_1.resolveCode)(passphrase);
|
|
63
|
+
remoteId = data.deviceId;
|
|
64
|
+
folders = data.folders;
|
|
65
|
+
spin0.succeed();
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
spin0.fail();
|
|
69
|
+
ui.error(err instanceof Error ? err.message : "Failed to resolve code");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const short = remoteId.substring(0, 7);
|
|
73
|
+
ui.info(`Connecting to ${short}...`);
|
|
74
|
+
ui.gap();
|
|
75
|
+
// First time? Set up everything
|
|
76
|
+
const existing = (0, config_js_1.readConfig)();
|
|
77
|
+
if (!existing) {
|
|
78
|
+
for (const folder of config_js_1.FOLDERS) {
|
|
79
|
+
(0, fs_1.mkdirSync)(folder.path, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
(0, fs_1.mkdirSync)(config_js_1.BOTSYNC_DIR, { recursive: true });
|
|
82
|
+
(0, fs_1.mkdirSync)(config_js_1.SYNCTHING_CONFIG_DIR, { recursive: true });
|
|
83
|
+
await (0, syncthing_js_1.downloadSyncthing)();
|
|
84
|
+
ui.stepDone("Syncthing ready");
|
|
85
|
+
const apiKey = (0, crypto_1.randomUUID)();
|
|
86
|
+
const apiPort = 27000 + Math.floor(Math.random() * 10000);
|
|
87
|
+
const configXml = (0, syncthing_js_1.generateConfig)(apiKey, apiPort);
|
|
88
|
+
(0, fs_1.writeFileSync)(`${config_js_1.SYNCTHING_CONFIG_DIR}/config.xml`, configXml);
|
|
89
|
+
(0, config_js_1.writeConfig)({ apiKey, apiPort });
|
|
90
|
+
const pid = (0, syncthing_js_1.startDaemon)();
|
|
91
|
+
ui.stepDone(`Daemon started (PID ${pid})`);
|
|
92
|
+
const spin = ui.spinner("Starting Syncthing...");
|
|
93
|
+
await (0, syncthing_js_1.waitForStart)();
|
|
94
|
+
spin.succeed();
|
|
95
|
+
const myId = await (0, syncthing_js_1.getDeviceId)();
|
|
96
|
+
(0, config_js_1.writeConfig)({ apiKey, apiPort, deviceId: myId });
|
|
97
|
+
}
|
|
98
|
+
// Add the remote device and share folders
|
|
99
|
+
const spin2 = ui.spinner("Pairing...");
|
|
100
|
+
await (0, syncthing_js_1.addDevice)(remoteId);
|
|
101
|
+
for (const folderId of folders) {
|
|
102
|
+
await (0, syncthing_js_1.addDeviceToFolder)(folderId, remoteId);
|
|
103
|
+
}
|
|
104
|
+
spin2.stop();
|
|
105
|
+
ui.connected(remoteId);
|
|
106
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* status.ts — The `botsync status` command.
|
|
4
|
+
*
|
|
5
|
+
* Shows connected peers, device ID, and per-folder sync state.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.status = status;
|
|
42
|
+
const config_js_1 = require("../config.js");
|
|
43
|
+
const syncthing_js_1 = require("../syncthing.js");
|
|
44
|
+
const ui = __importStar(require("../ui.js"));
|
|
45
|
+
async function status() {
|
|
46
|
+
const config = (0, config_js_1.readConfig)();
|
|
47
|
+
if (!config) {
|
|
48
|
+
ui.notRunning();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Check if daemon is actually running
|
|
52
|
+
try {
|
|
53
|
+
await (0, syncthing_js_1.apiCall)("GET", "/rest/system/status");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
ui.notRunning();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Get connections
|
|
60
|
+
const connections = await (0, syncthing_js_1.apiCall)("GET", "/rest/system/connections");
|
|
61
|
+
const peers = Object.values(connections.connections).filter((c) => c.connected).length;
|
|
62
|
+
// Get folder statuses
|
|
63
|
+
const folders = [];
|
|
64
|
+
for (const f of config_js_1.FOLDERS) {
|
|
65
|
+
try {
|
|
66
|
+
const s = await (0, syncthing_js_1.apiCall)("GET", `/rest/db/status?folder=${f.id}`);
|
|
67
|
+
const synced = s.state === "idle" && s.needFiles === 0;
|
|
68
|
+
const state = synced ? "idle" : s.state;
|
|
69
|
+
folders.push({
|
|
70
|
+
name: f.id.replace("botsync-", ""),
|
|
71
|
+
synced,
|
|
72
|
+
state,
|
|
73
|
+
lastChange: s.stateChanged,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
folders.push({ name: f.id.replace("botsync-", ""), synced: false, state: "unknown" });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ui.statusTable(peers, config.deviceId || "unknown", folders);
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* stop.ts — The `botsync stop` command.
|
|
4
|
+
*
|
|
5
|
+
* Stops the Syncthing daemon.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.stop = stop;
|
|
42
|
+
const syncthing_js_1 = require("../syncthing.js");
|
|
43
|
+
const ui = __importStar(require("../ui.js"));
|
|
44
|
+
async function stop() {
|
|
45
|
+
const killed = (0, syncthing_js_1.stopDaemon)();
|
|
46
|
+
if (killed) {
|
|
47
|
+
ui.stopped();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ui.notRunning();
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — Central configuration and path management for botsync.
|
|
3
|
+
*
|
|
4
|
+
* All paths are derived from the user's home directory. The sync root is ~/sync/,
|
|
5
|
+
* and botsync's internal state lives in ~/sync/.botsync/. We also store the
|
|
6
|
+
* syncthing binary in ~/.botsync/bin/ (outside the sync dir so it doesn't get synced).
|
|
7
|
+
*
|
|
8
|
+
* config.json is the runtime state file — it stores the API key, port, device ID,
|
|
9
|
+
* and PID so that all commands can talk to the running Syncthing instance.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SYNC_DIR: string;
|
|
12
|
+
export declare const BOTSYNC_DIR: string;
|
|
13
|
+
export declare const SYNCTHING_CONFIG_DIR: string;
|
|
14
|
+
export declare const SYNCTHING_BIN_DIR: string;
|
|
15
|
+
export declare const SYNCTHING_BIN: string;
|
|
16
|
+
export declare const CONFIG_FILE: string;
|
|
17
|
+
export declare const PID_FILE: string;
|
|
18
|
+
export declare const FOLDERS: {
|
|
19
|
+
id: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Runtime config shape — everything we need to talk to Syncthing */
|
|
23
|
+
export interface BotsyncConfig {
|
|
24
|
+
apiKey: string;
|
|
25
|
+
apiPort: number;
|
|
26
|
+
deviceId?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Read the config file, or return null if it doesn't exist */
|
|
29
|
+
export declare function readConfig(): BotsyncConfig | null;
|
|
30
|
+
/** Write config to disk. Creates parent dirs if needed. */
|
|
31
|
+
export declare function writeConfig(config: BotsyncConfig): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* config.ts — Central configuration and path management for botsync.
|
|
4
|
+
*
|
|
5
|
+
* All paths are derived from the user's home directory. The sync root is ~/sync/,
|
|
6
|
+
* and botsync's internal state lives in ~/sync/.botsync/. We also store the
|
|
7
|
+
* syncthing binary in ~/.botsync/bin/ (outside the sync dir so it doesn't get synced).
|
|
8
|
+
*
|
|
9
|
+
* config.json is the runtime state file — it stores the API key, port, device ID,
|
|
10
|
+
* and PID so that all commands can talk to the running Syncthing instance.
|
|
11
|
+
*/
|
|
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;
|
|
14
|
+
exports.readConfig = readConfig;
|
|
15
|
+
exports.writeConfig = writeConfig;
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const os_1 = require("os");
|
|
19
|
+
// BOTSYNC_ROOT env var overrides the default ~/sync/ location.
|
|
20
|
+
// Useful for testing without touching a production sync folder.
|
|
21
|
+
const root = process.env.BOTSYNC_ROOT || (0, path_1.join)((0, os_1.homedir)(), "sync");
|
|
22
|
+
// Where synced files live — the user-facing directory
|
|
23
|
+
exports.SYNC_DIR = root;
|
|
24
|
+
// Internal botsync state — inside sync dir but gitignored by Syncthing
|
|
25
|
+
exports.BOTSYNC_DIR = (0, path_1.join)(exports.SYNC_DIR, ".botsync");
|
|
26
|
+
// Syncthing config/data lives here (inside .botsync so it's co-located)
|
|
27
|
+
exports.SYNCTHING_CONFIG_DIR = (0, path_1.join)(exports.BOTSYNC_DIR, "syncthing");
|
|
28
|
+
// Where we store the syncthing binary — outside sync dir to avoid syncing a binary
|
|
29
|
+
exports.SYNCTHING_BIN_DIR = (0, path_1.join)((0, os_1.homedir)(), ".botsync", "bin");
|
|
30
|
+
exports.SYNCTHING_BIN = (0, path_1.join)(exports.SYNCTHING_BIN_DIR, "syncthing");
|
|
31
|
+
// Runtime config file — API key, port, device ID, PID
|
|
32
|
+
exports.CONFIG_FILE = (0, path_1.join)(exports.BOTSYNC_DIR, "config.json");
|
|
33
|
+
// PID file for the daemon
|
|
34
|
+
exports.PID_FILE = (0, path_1.join)(exports.BOTSYNC_DIR, "daemon.pid");
|
|
35
|
+
// The three standard sync folders
|
|
36
|
+
exports.FOLDERS = [
|
|
37
|
+
{ id: "botsync-shared", path: (0, path_1.join)(exports.SYNC_DIR, "shared") },
|
|
38
|
+
{ id: "botsync-deliverables", path: (0, path_1.join)(exports.SYNC_DIR, "deliverables") },
|
|
39
|
+
{ id: "botsync-inbox", path: (0, path_1.join)(exports.SYNC_DIR, "inbox") },
|
|
40
|
+
];
|
|
41
|
+
/** Read the config file, or return null if it doesn't exist */
|
|
42
|
+
function readConfig() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = (0, fs_1.readFileSync)(exports.CONFIG_FILE, "utf-8");
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Write config to disk. Creates parent dirs if needed. */
|
|
52
|
+
function writeConfig(config) {
|
|
53
|
+
(0, fs_1.mkdirSync)(exports.BOTSYNC_DIR, { recursive: true });
|
|
54
|
+
(0, fs_1.writeFileSync)(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
55
|
+
}
|