tether-sync 0.1.0 → 0.2.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 CHANGED
@@ -1,21 +1,27 @@
1
- # tether-sync
2
-
3
- Keep files and folders identical to a central source of truth, wherever they live on your machine.
4
-
5
- ```sh
6
- npm install -g tether-sync
7
- tether login # opens the browser to sign in
8
- tether pull <share-id> # get a managed copy of a file or folder
9
- tether status
10
- ```
11
-
12
- Owners publish files with `tether manage <path> --repo <repo>` and share the printed share ID. A managed copy:
13
- - follows the file when you move or rename it
14
- - reverts local edits until you run `tether edit <path>`
15
- - pushes your changes with `tether done <path>` (if you are an editor)
16
-
17
- Run `tether --help` for every command. The web console is at https://tether-b10cf.web.app.
18
-
19
- Requires Node.js 22.13 or newer. Windows (NTFS/ReFS) is fully supported; Linux support is experimental.
20
-
21
- Source: https://github.com/SaiEthihasChanda/Tether
1
+ # tether-sync
2
+
3
+ Keep files and folders identical to a central source of truth, wherever they live on your machine.
4
+
5
+ ```sh
6
+ npm install -g tether-sync
7
+ tether login # opens the browser to sign in
8
+ tether pull <share-id> # get a managed copy of a file or folder
9
+ tether status
10
+ ```
11
+
12
+ Owners publish files with `tether manage <path> --repo <repo>` and share the printed share ID. A managed copy:
13
+ - follows the file when you move or rename it
14
+ - reverts local edits until you run `tether edit <path>`
15
+ - pushes your changes with `tether done <path>` (if you are an editor)
16
+
17
+ Editors can also run `tether edit <path> --realtime` to push every save live, one live editor per file. `tether update` installs the latest version.
18
+
19
+ Inside a git repository, Tether asks whether git's changes should be reverted (keep), or accepted and published after pull, checkout and merge (follow, for editors). Change it with `tether git <path> --mode`.
20
+
21
+ Desktop notifications cover reverted changes, conflicts, lost live sessions and revoked access. Set them with `tether config set notifications all|errors|off`.
22
+
23
+ Run `tether --help` for every command. The web console is at https://tether-b10cf.web.app.
24
+
25
+ Requires Node.js 22.13 or newer. Windows (NTFS/ReFS) is fully supported; Linux support is experimental.
26
+
27
+ Source: https://github.com/SaiEthihasChanda/Tether
@@ -0,0 +1,248 @@
1
+ // packages/client/src/paths.js
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ var isWindows = process.platform === "win32";
6
+ function tetherHome() {
7
+ if (process.env.TETHER_HOME) return path.resolve(process.env.TETHER_HOME);
8
+ if (isWindows) return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "Tether");
9
+ return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"), "tether");
10
+ }
11
+ function ipcEndpoint(home = tetherHome()) {
12
+ const tag = crypto.createHash("sha1").update(home.toLowerCase()).digest("hex").slice(0, 12);
13
+ if (isWindows) return `\\\\.\\pipe\\tether-${os.userInfo().username}-${tag}`;
14
+ return path.join(home, "tetherd.sock");
15
+ }
16
+ var files = (home = tetherHome()) => ({
17
+ db: path.join(home, "tether.db"),
18
+ blobs: path.join(home, "blobs"),
19
+ config: path.join(home, "config.json"),
20
+ credentials: path.join(home, isWindows ? "credentials.bin" : "credentials.json"),
21
+ log: path.join(home, "tetherd.log"),
22
+ pid: path.join(home, "tetherd.pid")
23
+ });
24
+
25
+ // packages/client/src/ipc.js
26
+ import net from "node:net";
27
+ import fs from "node:fs";
28
+ var IpcError = class extends Error {
29
+ constructor(message, code) {
30
+ super(message);
31
+ this.code = code;
32
+ }
33
+ };
34
+ async function listen(endpoint, handler) {
35
+ if (!isWindows && fs.existsSync(endpoint)) {
36
+ const alive = await call(endpoint, "ping", {}, 1e3).then(() => true, () => false);
37
+ if (alive) throw new IpcError("another tetherd is already running", "EADDRINUSE");
38
+ fs.rmSync(endpoint, { force: true });
39
+ }
40
+ const server = net.createServer((socket) => {
41
+ let buf = "";
42
+ socket.setEncoding("utf8");
43
+ socket.on("data", async (chunk) => {
44
+ buf += chunk;
45
+ const nl = buf.indexOf("\n");
46
+ if (nl < 0) return;
47
+ let req;
48
+ try {
49
+ req = JSON.parse(buf.slice(0, nl));
50
+ } catch {
51
+ socket.end(JSON.stringify({ error: { message: "bad request" } }) + "\n");
52
+ return;
53
+ }
54
+ buf = "";
55
+ let res;
56
+ try {
57
+ res = { result: await handler(req.method, req.params ?? {}) ?? null };
58
+ } catch (e) {
59
+ res = { error: { message: e.message, code: e.code ?? e.name, status: e.status } };
60
+ }
61
+ socket.end(JSON.stringify(res) + "\n");
62
+ });
63
+ socket.on("error", () => {
64
+ });
65
+ });
66
+ await new Promise((resolve, reject) => {
67
+ server.once("error", (e) => reject(e.code === "EADDRINUSE" ? new IpcError("another tetherd is already running", "EADDRINUSE") : e));
68
+ server.listen(endpoint, resolve);
69
+ });
70
+ if (!isWindows) fs.chmodSync(endpoint, 384);
71
+ return server;
72
+ }
73
+ function call(endpoint, method, params = {}, timeoutMs = 10 * 60 * 1e3) {
74
+ return new Promise((resolve, reject) => {
75
+ const socket = net.connect(endpoint);
76
+ let buf = "";
77
+ const timer = setTimeout(() => {
78
+ socket.destroy();
79
+ reject(new IpcError("the daemon did not answer in time", "ETIMEDOUT"));
80
+ }, timeoutMs);
81
+ socket.setEncoding("utf8");
82
+ socket.on("connect", () => socket.write(JSON.stringify({ method, params }) + "\n"));
83
+ socket.on("data", (c) => buf += c);
84
+ socket.on("error", (e) => {
85
+ clearTimeout(timer);
86
+ reject(new IpcError("the Tether daemon is not running", e.code === "ENOENT" || e.code === "ECONNREFUSED" ? "ENODAEMON" : e.code));
87
+ });
88
+ socket.on("end", () => {
89
+ clearTimeout(timer);
90
+ try {
91
+ const res = JSON.parse(buf);
92
+ if (res.error) reject(new IpcError(res.error.message, res.error.code));
93
+ else resolve(res.result);
94
+ } catch {
95
+ reject(new IpcError("invalid response from the daemon"));
96
+ }
97
+ });
98
+ });
99
+ }
100
+
101
+ // packages/client/src/version.js
102
+ var VERSION = "0.2.0";
103
+
104
+ // packages/client/src/update.js
105
+ import fs2 from "node:fs";
106
+ import path2 from "node:path";
107
+ import { spawnSync } from "node:child_process";
108
+ import { fileURLToPath } from "node:url";
109
+ var PACKAGE = "tether-sync";
110
+ var registry = () => (process.env.TETHER_UPDATE_REGISTRY || "https://registry.npmjs.org").replace(/\/$/, "");
111
+ async function latestVersion() {
112
+ try {
113
+ const res = await fetch(`${registry()}/${PACKAGE}/latest`, { signal: AbortSignal.timeout(1e4) });
114
+ if (!res.ok) return null;
115
+ return (await res.json()).version ?? null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ function isNewer(a, b) {
121
+ if (!a || !b) return false;
122
+ const parse = (v) => {
123
+ const [core, pre] = v.split("-");
124
+ return { nums: core.split(".").map((n) => Number(n) || 0), pre: pre ?? null };
125
+ };
126
+ const x = parse(a);
127
+ const y = parse(b);
128
+ for (let i = 0; i < 3; i++) if ((x.nums[i] ?? 0) !== (y.nums[i] ?? 0)) return (x.nums[i] ?? 0) > (y.nums[i] ?? 0);
129
+ if (x.pre === y.pre) return false;
130
+ if (x.pre === null) return true;
131
+ if (y.pre === null) return false;
132
+ return x.pre > y.pre;
133
+ }
134
+ function installDir(from = fileURLToPath(import.meta.url)) {
135
+ const dir = path2.resolve(path2.dirname(from), "..");
136
+ try {
137
+ return JSON.parse(fs2.readFileSync(path2.join(dir, "package.json"), "utf8")).name === PACKAGE ? dir : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function prefixOf(pkgDir, windows = isWindows) {
143
+ const parent = path2.dirname(path2.dirname(pkgDir));
144
+ return !windows && path2.basename(parent) === "lib" ? path2.dirname(parent) : parent;
145
+ }
146
+ function npmInstall(spec, prefix) {
147
+ const args = ["install", "-g", "--prefix", prefix, spec];
148
+ const bin = path2.dirname(process.execPath);
149
+ const npmCli = [path2.join(bin, "node_modules", "npm", "bin", "npm-cli.js"), path2.join(bin, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")].find((p) => fs2.existsSync(p));
150
+ const r = npmCli ? spawnSync(process.execPath, [npmCli, ...args], { stdio: "inherit" }) : spawnSync(isWindows ? "npm.cmd" : "npm", args.map((a) => isWindows ? `"${a}"` : a), { stdio: "inherit", shell: isWindows });
151
+ return r.status ?? 1;
152
+ }
153
+
154
+ // packages/client/src/git.js
155
+ import fs3 from "node:fs";
156
+ import path3 from "node:path";
157
+ import { execFileSync } from "node:child_process";
158
+ var GIT_MODES = ["keep", "follow"];
159
+ function git(cwd, args) {
160
+ return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
161
+ }
162
+ function findRepo(p) {
163
+ let dir = path3.resolve(p);
164
+ while (!fs3.existsSync(dir) || !fs3.statSync(dir).isDirectory()) {
165
+ const up = path3.dirname(dir);
166
+ if (up === dir) return null;
167
+ dir = up;
168
+ }
169
+ try {
170
+ const [root, gitDir, hooks] = git(dir, ["rev-parse", "--show-toplevel", "--absolute-git-dir", "--git-path", "hooks"]).split(/\r?\n/);
171
+ return { root: path3.resolve(root), gitDir: path3.resolve(gitDir), hooksDir: path3.resolve(dir, hooks) };
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ function gitBusy(gitDir) {
177
+ if (!gitDir) return null;
178
+ const has = (f) => fs3.existsSync(path3.join(gitDir, f));
179
+ if (has("index.lock")) return "git is running";
180
+ if (has("MERGE_HEAD")) return "merge in progress";
181
+ if (has("rebase-merge") || has("rebase-apply")) return "rebase in progress";
182
+ if (has("CHERRY_PICK_HEAD")) return "cherry-pick in progress";
183
+ if (has("REVERT_HEAD")) return "revert in progress";
184
+ return null;
185
+ }
186
+ var HOOKS = ["post-merge", "post-checkout", "post-rewrite"];
187
+ var quote = (s) => `'${s.replace(/\\/g, "/").replace(/'/g, `'\\''`)}'`;
188
+ var begin = (home) => `# >>> tether (${home.replace(/\\/g, "/")}) >>>`;
189
+ var end = (home) => `# <<< tether (${home.replace(/\\/g, "/")}) <<<`;
190
+ function installHooks(hooksDir, { home, node, cli }) {
191
+ fs3.mkdirSync(hooksDir, { recursive: true });
192
+ for (const hook of HOOKS) {
193
+ const file = path3.join(hooksDir, hook);
194
+ let text = fs3.existsSync(file) ? fs3.readFileSync(file, "utf8") : "#!/bin/sh\n";
195
+ text = stripBlock(text, home);
196
+ const block = [
197
+ begin(home),
198
+ "# Lets Tether pick up files git changed (follow mode). Remove with `tether git <path> --mode keep`.",
199
+ `TETHER_HOME=${quote(home)} ${quote(node)} ${quote(cli)} git-sync --hook ${hook} --repo "$(pwd)" >/dev/null 2>&1 || true`,
200
+ end(home)
201
+ ].join("\n");
202
+ fs3.writeFileSync(file, `${text.replace(/\s*$/, "")}
203
+
204
+ ${block}
205
+ `);
206
+ try {
207
+ fs3.chmodSync(file, 493);
208
+ } catch {
209
+ }
210
+ }
211
+ }
212
+ function removeHooks(hooksDir, home) {
213
+ for (const hook of HOOKS) {
214
+ const file = path3.join(hooksDir, hook);
215
+ if (!fs3.existsSync(file)) continue;
216
+ const text = stripBlock(fs3.readFileSync(file, "utf8"), home);
217
+ if (/^(#!.*\n)?\s*$/.test(text)) fs3.rmSync(file);
218
+ else fs3.writeFileSync(file, text.replace(/\s*$/, "\n"));
219
+ }
220
+ }
221
+ function stripBlock(text, home) {
222
+ const b = text.indexOf(begin(home));
223
+ if (b < 0) return text;
224
+ const e = text.indexOf(end(home), b);
225
+ return (text.slice(0, b) + (e < 0 ? "" : text.slice(e + end(home).length))).replace(/\n{3,}/g, "\n\n");
226
+ }
227
+
228
+ export {
229
+ isWindows,
230
+ tetherHome,
231
+ ipcEndpoint,
232
+ files,
233
+ IpcError,
234
+ listen,
235
+ call,
236
+ VERSION,
237
+ PACKAGE,
238
+ latestVersion,
239
+ isNewer,
240
+ installDir,
241
+ prefixOf,
242
+ npmInstall,
243
+ GIT_MODES,
244
+ findRepo,
245
+ gitBusy,
246
+ installHooks,
247
+ removeHooks
248
+ };