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 +27 -21
- package/bin/chunk-NDKBAHAG.js +248 -0
- package/bin/{daemon-WGNVBSEJ.js → daemon-KQ4ZTBYG.js} +510 -35
- package/bin/tether.js +85 -8
- package/bin/tetherd.js +1 -1
- package/package.json +1 -1
- package/bin/chunk-MGMU33LE.js +0 -109
package/bin/tether.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
IpcError,
|
|
4
|
+
PACKAGE,
|
|
5
|
+
VERSION,
|
|
4
6
|
call,
|
|
7
|
+
findRepo,
|
|
8
|
+
installDir,
|
|
5
9
|
ipcEndpoint,
|
|
10
|
+
isNewer,
|
|
6
11
|
isWindows,
|
|
12
|
+
latestVersion,
|
|
13
|
+
npmInstall,
|
|
14
|
+
prefixOf,
|
|
7
15
|
tetherHome
|
|
8
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-NDKBAHAG.js";
|
|
9
17
|
|
|
10
18
|
// packages/client/src/cli.js
|
|
11
19
|
import path2 from "node:path";
|
|
@@ -169,8 +177,29 @@ async function resolveFlow(p, from, opts) {
|
|
|
169
177
|
}
|
|
170
178
|
if (results.some((r) => r.error)) process.exitCode = 1;
|
|
171
179
|
}
|
|
180
|
+
async function chooseGitMode(target, flag, canFollow) {
|
|
181
|
+
const repo = findRepo(target);
|
|
182
|
+
if (!repo) return void 0;
|
|
183
|
+
if (flag) {
|
|
184
|
+
if (flag === "follow" && !canFollow) throw new Error("--git follow needs editor access (you are a viewer); use --git keep or --git skip");
|
|
185
|
+
return flag;
|
|
186
|
+
}
|
|
187
|
+
if (!process.stdin.isTTY) {
|
|
188
|
+
out(`note: ${target} is inside the git repository ${repo.root}; Tether will revert git's changes to it. Choose with --git keep|follow|skip.`);
|
|
189
|
+
return "keep";
|
|
190
|
+
}
|
|
191
|
+
out(`${target} is inside the git repository ${repo.root}.`);
|
|
192
|
+
out("How should Tether treat it when git changes it?");
|
|
193
|
+
out(" [1] keep managing it: git's changes (pull, checkout, switch, merge) may get reverted to Tether's version");
|
|
194
|
+
out(" [2] do not let Tether manage it");
|
|
195
|
+
if (canFollow) out(" [3] manage it, and after a git pull, checkout, switch or merge accept git's version and publish it to Tether");
|
|
196
|
+
else out(" (option 3, accepting git's changes, needs editor access)");
|
|
197
|
+
const choice = await ask(" > ", canFollow ? ["1", "2", "3"] : ["1", "2"]);
|
|
198
|
+
return { 1: "keep", 2: "skip", 3: "follow" }[choice];
|
|
199
|
+
}
|
|
172
200
|
function printStatus(s, verbose) {
|
|
173
201
|
out(`server: ${s.server} user: ${s.user ? s.user.email : "(not signed in)"}${s.offline ? ` OFFLINE: ${s.offline}` : ""}`);
|
|
202
|
+
if (isNewer(s.latest, s.version)) out(`update available: ${s.version} -> ${s.latest} (run \`tether update\`)`);
|
|
174
203
|
if (!s.instances.length) return out("nothing managed yet");
|
|
175
204
|
const folders = new Map(s.instances.filter((i) => i.kind === "folder").map((i) => [i.instanceId, i]));
|
|
176
205
|
for (const i of s.instances) {
|
|
@@ -197,7 +226,7 @@ async function main(argv) {
|
|
|
197
226
|
if (e.code === "EPIPE") process.exit(0);
|
|
198
227
|
throw e;
|
|
199
228
|
});
|
|
200
|
-
const program = new Command("tether").description("Keep files in sync with a central source of truth, wherever they live.").version(
|
|
229
|
+
const program = new Command("tether").description("Keep files in sync with a central source of truth, wherever they live.").version(VERSION);
|
|
201
230
|
program.showHelpAfterError();
|
|
202
231
|
program.command("login").description("sign in (opens the browser)").option("--email <email>", "sign in with email and password instead (local development)").option("--password <password>").option("--signup", "create the account (with --email)").action(async (o) => {
|
|
203
232
|
if (o.email) {
|
|
@@ -252,8 +281,10 @@ async function main(argv) {
|
|
|
252
281
|
const r = await daemon("repo.eol", { ref, glob, mode });
|
|
253
282
|
out(`eol rules: ${JSON.stringify(r.eolDefaults)}`);
|
|
254
283
|
});
|
|
255
|
-
program.command("manage <path>").description("upload a file or folder into a repo; you become its owner").requiredOption("--repo <repo>").addOption(new Option("--eol <mode>").choices(["auto", "lf", "crlf", "binary"])).action(async (p, o) => {
|
|
256
|
-
const
|
|
284
|
+
program.command("manage <path>").description("upload a file or folder into a repo; you become its owner").requiredOption("--repo <repo>").addOption(new Option("--eol <mode>").choices(["auto", "lf", "crlf", "binary"])).addOption(new Option("--git <mode>", "inside a git repo: keep (Tether reverts git changes), follow (publish git changes), skip").choices(["keep", "follow", "skip"])).action(async (p, o) => {
|
|
285
|
+
const gitMode = await chooseGitMode(path2.resolve(p), o.git, true);
|
|
286
|
+
if (gitMode === "skip") return out("not managed");
|
|
287
|
+
const r = await daemon("manage", { path: path2.resolve(p), repo: o.repo, eol: o.eol, gitMode });
|
|
257
288
|
out(`managing ${r.path} (${r.kind}${r.kind === "folder" ? `, ${r.files} files` : ""})`);
|
|
258
289
|
if (r.skippedSymlinks?.length) out(`skipped symlinks: ${r.skippedSymlinks.join(", ")}`);
|
|
259
290
|
out(`share ID: ${r.shareId}`);
|
|
@@ -262,8 +293,11 @@ async function main(argv) {
|
|
|
262
293
|
const r = await daemon("add", { path: path2.resolve(p) });
|
|
263
294
|
out(`added ${r.relPath} to ${r.folder}`);
|
|
264
295
|
});
|
|
265
|
-
program.command("pull <shareId> [dest]").description("get a managed copy").action(async (shareId, dest) => {
|
|
266
|
-
const
|
|
296
|
+
program.command("pull <shareId> [dest]").description("get a managed copy").addOption(new Option("--git <mode>", "inside a git repo: keep (Tether reverts git changes), follow (publish git changes; editors), skip").choices(["keep", "follow", "skip"])).action(async (shareId, dest, o) => {
|
|
297
|
+
const peek = await daemon("pull", { shareId, dest, cwd: process.cwd(), peek: true });
|
|
298
|
+
const gitMode = await chooseGitMode(peek.path, o.git, peek.role !== "viewer");
|
|
299
|
+
if (gitMode === "skip") return out("not pulled");
|
|
300
|
+
const r = await daemon("pull", { shareId, dest, cwd: process.cwd(), gitMode });
|
|
267
301
|
out(`pulled ${r.path} (${r.kind}${r.kind === "folder" ? `, ${r.files} files` : ""}, v${r.version}) as ${r.role}`);
|
|
268
302
|
});
|
|
269
303
|
program.command("status [path]").description("modes, pending changes, problems").option("-v, --verbose", "show every file and instance IDs").option("--json").action(async (p, o) => {
|
|
@@ -271,9 +305,18 @@ async function main(argv) {
|
|
|
271
305
|
if (o.json) return json(s);
|
|
272
306
|
printStatus(s, o.verbose);
|
|
273
307
|
});
|
|
274
|
-
program.command("edit <path>").description("start editing: local changes are kept until `tether done`").action(async (p) => {
|
|
275
|
-
const r = await daemon("edit", { path: path2.resolve(p) });
|
|
308
|
+
program.command("edit <path>").description("start editing: local changes are kept until `tether done`").option("--realtime", "push every save immediately (editors only, one realtime editor per file)").action(async (p, o) => {
|
|
309
|
+
const r = await daemon("edit", { path: path2.resolve(p), realtime: !!o.realtime });
|
|
310
|
+
if (r.realtime) {
|
|
311
|
+
out(`editing ${r.changed.length} file(s) in realtime: every save is pushed to everyone within seconds.`);
|
|
312
|
+
out(`Other editors cannot start realtime on them until you run \`tether done ${p}\`.`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
276
315
|
out(r.changed.length ? `editing ${r.changed.length} file(s); run \`tether done ${p}\` to finish` : "nothing to change (already editing?)");
|
|
316
|
+
for (const w of r.warnings ?? []) {
|
|
317
|
+
out(`warning: ${w.path} is being edited in realtime by ${w.email} (since ${new Date(w.since).toLocaleTimeString()}).`);
|
|
318
|
+
out(" Their changes keep arriving on the server; expect merge conflicts when you run `tether done`.");
|
|
319
|
+
}
|
|
277
320
|
});
|
|
278
321
|
program.command("done [path]").description("finish editing: push changes and resolve conflicts").addOption(new Option("--resolve <choice>", "answer every question with this choice").choices(["push", "accept", "keep-both", "cancel", "delete", "stop"])).action((p, o) => resolveFlow(p, "editing", o));
|
|
279
322
|
program.command("unmanage <path>").description("stop syncing (location is still followed)").action(async (p) => {
|
|
@@ -359,6 +402,40 @@ async function main(argv) {
|
|
|
359
402
|
uninstall();
|
|
360
403
|
out("removed from login startup");
|
|
361
404
|
});
|
|
405
|
+
program.command("update").description(`update tether to the latest version on npm (${PACKAGE})`).option("--check", "only check whether an update is available").action(async (o) => {
|
|
406
|
+
const latest = await latestVersion();
|
|
407
|
+
if (!latest) throw new Error("could not reach the npm registry");
|
|
408
|
+
if (!isNewer(latest, VERSION)) return out(`tether ${VERSION} is up to date`);
|
|
409
|
+
if (o.check) return out(`update available: ${VERSION} -> ${latest} (run \`tether update\`)`);
|
|
410
|
+
const pkgDir = installDir();
|
|
411
|
+
if (!pkgDir) throw new Error("this tether is a development copy (not installed from npm); update it with `git pull` and `npm install`");
|
|
412
|
+
out(`updating tether ${VERSION} -> ${latest}`);
|
|
413
|
+
let wasRunning = false;
|
|
414
|
+
try {
|
|
415
|
+
await call(endpoint(), "shutdown", {}, 3e3);
|
|
416
|
+
wasRunning = true;
|
|
417
|
+
for (let i = 0; i < 50; i++) {
|
|
418
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
419
|
+
await call(endpoint(), "ping", {}, 500);
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
const code = npmInstall(process.env.TETHER_UPDATE_SPEC || `${PACKAGE}@${latest}`, prefixOf(pkgDir));
|
|
424
|
+
if (code !== 0) {
|
|
425
|
+
if (wasRunning) spawnDaemon();
|
|
426
|
+
throw new Error("npm install failed; tether was not updated. If another Tether daemon from this install is running (another TETHER_HOME), stop it and try again.");
|
|
427
|
+
}
|
|
428
|
+
if (wasRunning) spawnDaemon();
|
|
429
|
+
out(`updated to ${latest}`);
|
|
430
|
+
});
|
|
431
|
+
program.command("git <path>").description("show or change how a managed file shares its git repository").addOption(new Option("--mode <mode>", "keep: Tether reverts git's changes; follow: git's changes are published (editors)").choices(["keep", "follow"])).action(async (p, o) => {
|
|
432
|
+
for (const i of await daemon("git.mode", { path: path2.resolve(p), mode: o.mode })) {
|
|
433
|
+
out(`${(i.gitMode ?? (i.gitRoot ? "keep" : "-")).padEnd(7)} ${(i.role ?? "").padEnd(6)} ${i.path}${i.gitRoot ? "" : " (not in a git repo)"}`);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
program.command("git-sync", { hidden: true }).description("used by git hooks").requiredOption("--repo <dir>").option("--hook <name>").action(async (o) => {
|
|
437
|
+
await daemon("git.sync", { repo: path2.resolve(o.repo), hook: o.hook });
|
|
438
|
+
});
|
|
362
439
|
program.command("sync").description("run a sync pass now").action(async () => {
|
|
363
440
|
const r = await daemon("sync");
|
|
364
441
|
out(r.offline ? `offline: ${r.offline}` : "synced");
|
package/bin/tetherd.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
process.removeAllListeners("warning");
|
|
5
5
|
var i = process.argv.indexOf("--home");
|
|
6
6
|
if (i > 0 && process.argv[i + 1]) process.env.TETHER_HOME = process.argv[i + 1];
|
|
7
|
-
var { runDaemon } = await import("./daemon-
|
|
7
|
+
var { runDaemon } = await import("./daemon-KQ4ZTBYG.js");
|
|
8
8
|
runDaemon().catch((e) => {
|
|
9
9
|
if (e.code === "EADDRINUSE") {
|
|
10
10
|
console.error("tetherd is already running");
|
package/package.json
CHANGED
package/bin/chunk-MGMU33LE.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
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
|
-
export {
|
|
102
|
-
isWindows,
|
|
103
|
-
tetherHome,
|
|
104
|
-
ipcEndpoint,
|
|
105
|
-
files,
|
|
106
|
-
IpcError,
|
|
107
|
-
listen,
|
|
108
|
-
call
|
|
109
|
-
};
|