@rigxyz/tapd 0.1.0 → 0.4.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/dist/bin.js +698 -82
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-RQC73B5Y.js → chunk-YPZ2UNI5.js} +729 -189
- package/dist/chunk-YPZ2UNI5.js.map +1 -0
- package/dist/index.d.ts +345 -31
- package/dist/index.js +35 -3
- package/package.json +1 -1
- package/dist/chunk-RQC73B5Y.js.map +0 -1
|
@@ -69,11 +69,8 @@ async function expectJson(route, res) {
|
|
|
69
69
|
async function acceptInvite(opts) {
|
|
70
70
|
const fn = opts.fetch ?? fetch;
|
|
71
71
|
const route = "POST /v1/invites/:secret/accept";
|
|
72
|
-
const headers = {
|
|
73
|
-
|
|
74
|
-
"x-tap-user-id": opts.userId
|
|
75
|
-
};
|
|
76
|
-
if (opts.email) headers["x-tap-user-email"] = opts.email;
|
|
72
|
+
const headers = { "content-type": "application/json" };
|
|
73
|
+
if (opts.accessToken) headers.authorization = `Bearer ${opts.accessToken}`;
|
|
77
74
|
const res = await fn(
|
|
78
75
|
joinUrl(opts.baseUrl, `/v1/invites/${encodeURIComponent(opts.secret)}/accept`),
|
|
79
76
|
{
|
|
@@ -87,18 +84,27 @@ async function acceptInvite(opts) {
|
|
|
87
84
|
async function createBinding(opts) {
|
|
88
85
|
const fn = opts.fetch ?? fetch;
|
|
89
86
|
const route = "POST /v1/bindings";
|
|
90
|
-
const headers = {
|
|
91
|
-
"content-type": "application/json",
|
|
92
|
-
"x-tap-user-id": opts.userId
|
|
93
|
-
};
|
|
94
|
-
if (opts.email) headers["x-tap-user-email"] = opts.email;
|
|
95
87
|
const res = await fn(joinUrl(opts.baseUrl, "/v1/bindings"), {
|
|
96
88
|
method: "POST",
|
|
97
|
-
headers
|
|
89
|
+
headers: {
|
|
90
|
+
"content-type": "application/json",
|
|
91
|
+
authorization: `Bearer ${opts.accessToken}`
|
|
92
|
+
},
|
|
98
93
|
body: JSON.stringify(opts.request)
|
|
99
94
|
});
|
|
100
95
|
return expectJson(route, res);
|
|
101
96
|
}
|
|
97
|
+
async function revokeImportEdge(opts) {
|
|
98
|
+
const fn = opts.fetch ?? fetch;
|
|
99
|
+
const res = await fn(
|
|
100
|
+
joinUrl(opts.baseUrl, `/v1/me/imports/${encodeURIComponent(opts.edgeId)}`),
|
|
101
|
+
{ method: "DELETE", headers: { authorization: `Bearer ${opts.accessToken}` } }
|
|
102
|
+
);
|
|
103
|
+
if (res.ok) return { revoked: true, status: res.status };
|
|
104
|
+
const body = await readJson(res);
|
|
105
|
+
const reason = typeof body === "object" && body !== null && "reason" in body ? String(body.reason) : void 0;
|
|
106
|
+
return { revoked: false, status: res.status, ...reason ? { reason } : {} };
|
|
107
|
+
}
|
|
102
108
|
var RelayClient = class {
|
|
103
109
|
baseUrl;
|
|
104
110
|
bindingId;
|
|
@@ -190,6 +196,8 @@ var RelayClient = class {
|
|
|
190
196
|
const params = new URLSearchParams();
|
|
191
197
|
if (opts?.after) params.set("after", opts.after);
|
|
192
198
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
199
|
+
if (opts?.path) params.set("path", opts.path);
|
|
200
|
+
if (opts?.untilInclusive) params.set("untilInclusive", opts.untilInclusive);
|
|
193
201
|
const qs = params.toString();
|
|
194
202
|
const res = await this.fetch(
|
|
195
203
|
this.bindingPath("/changes") + (qs ? `?${qs}` : ""),
|
|
@@ -228,6 +236,34 @@ var RelayClient = class {
|
|
|
228
236
|
);
|
|
229
237
|
return expectJson(route, res);
|
|
230
238
|
}
|
|
239
|
+
// ────────── members ──────────
|
|
240
|
+
async listMembers() {
|
|
241
|
+
const route = "GET /v1/bindings/:id/members";
|
|
242
|
+
const res = await this.fetch(this.bindingPath("/members"), {
|
|
243
|
+
headers: this.authHeaders()
|
|
244
|
+
});
|
|
245
|
+
return expectJson(route, res);
|
|
246
|
+
}
|
|
247
|
+
async removeMember(userId) {
|
|
248
|
+
const route = "DELETE /v1/bindings/:id/members/:userId";
|
|
249
|
+
const res = await this.fetch(
|
|
250
|
+
this.bindingPath(`/members/${encodeURIComponent(userId)}`),
|
|
251
|
+
{ method: "DELETE", headers: this.authHeaders() }
|
|
252
|
+
);
|
|
253
|
+
return expectJson(route, res);
|
|
254
|
+
}
|
|
255
|
+
async setMemberRole(userId, role) {
|
|
256
|
+
const route = "PATCH /v1/bindings/:id/members/:userId";
|
|
257
|
+
const res = await this.fetch(
|
|
258
|
+
this.bindingPath(`/members/${encodeURIComponent(userId)}`),
|
|
259
|
+
{
|
|
260
|
+
method: "PATCH",
|
|
261
|
+
headers: this.authHeaders({ "content-type": "application/json" }),
|
|
262
|
+
body: JSON.stringify({ role })
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
return expectJson(route, res);
|
|
266
|
+
}
|
|
231
267
|
// ────────── manifest ──────────
|
|
232
268
|
async getManifest(opts) {
|
|
233
269
|
const route = "GET /v1/bindings/:id/manifest";
|
|
@@ -401,6 +437,15 @@ function isAlwaysExcluded(posixPath) {
|
|
|
401
437
|
}
|
|
402
438
|
return false;
|
|
403
439
|
}
|
|
440
|
+
function isUnderAny(posixPath, prefixes) {
|
|
441
|
+
for (const raw of prefixes) {
|
|
442
|
+
const prefix = raw.replace(/\/+$/, "");
|
|
443
|
+
if (prefix === "") continue;
|
|
444
|
+
if (posixPath === prefix) return true;
|
|
445
|
+
if (posixPath.startsWith(prefix + "/")) return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
404
449
|
|
|
405
450
|
// src/walker.ts
|
|
406
451
|
import { readdirSync, statSync } from "fs";
|
|
@@ -437,6 +482,7 @@ function walk(rootDir, opts) {
|
|
|
437
482
|
const emptyDirs = [];
|
|
438
483
|
const directoriesScanned = [];
|
|
439
484
|
const warnings = [];
|
|
485
|
+
const excludePaths = opts.excludePaths ?? [];
|
|
440
486
|
walkDir(".");
|
|
441
487
|
return { files, emptyDirs, directoriesScanned, warnings };
|
|
442
488
|
function walkDir(rel) {
|
|
@@ -454,6 +500,10 @@ function walk(rootDir, opts) {
|
|
|
454
500
|
const posix = posixOf(childRel);
|
|
455
501
|
const isDir = ent.isDirectory();
|
|
456
502
|
if (isAlwaysExcluded(posix)) continue;
|
|
503
|
+
if (excludePaths.length > 0 && isUnderAny(posix, excludePaths)) {
|
|
504
|
+
hasVisible = true;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
457
507
|
if (opts.ignore.ignores(posix + (isDir ? "/" : ""))) continue;
|
|
458
508
|
if (ent.isSymbolicLink()) {
|
|
459
509
|
warnings.push({ path: posix, reason: "symlink" });
|
|
@@ -537,7 +587,7 @@ async function init(opts) {
|
|
|
537
587
|
const created = await createBinding({
|
|
538
588
|
baseUrl: opts.relayUrl,
|
|
539
589
|
request: { name: opts.bindingName, deviceLabel: opts.deviceLabel },
|
|
540
|
-
|
|
590
|
+
accessToken: opts.accessToken,
|
|
541
591
|
fetch: opts.fetch
|
|
542
592
|
});
|
|
543
593
|
writeBindingConfig(opts.rootDir, {
|
|
@@ -704,8 +754,7 @@ async function join5(opts) {
|
|
|
704
754
|
const accepted = await acceptInvite({
|
|
705
755
|
baseUrl,
|
|
706
756
|
secret,
|
|
707
|
-
|
|
708
|
-
email: opts.email,
|
|
757
|
+
accessToken: opts.accessToken ?? null,
|
|
709
758
|
request: opts.deviceLabel ? { deviceLabel: opts.deviceLabel } : void 0,
|
|
710
759
|
fetch: opts.fetch
|
|
711
760
|
});
|
|
@@ -732,20 +781,347 @@ async function join5(opts) {
|
|
|
732
781
|
};
|
|
733
782
|
}
|
|
734
783
|
|
|
735
|
-
// src/
|
|
736
|
-
import { createHash as createHash3, randomBytes } from "crypto";
|
|
784
|
+
// src/imports.ts
|
|
737
785
|
import {
|
|
738
786
|
existsSync as existsSync3,
|
|
739
|
-
lstatSync,
|
|
740
787
|
mkdirSync as mkdirSync3,
|
|
741
788
|
readdirSync as readdirSync2,
|
|
742
789
|
readFileSync as readFileSync4,
|
|
790
|
+
realpathSync,
|
|
791
|
+
statSync as statSync2,
|
|
792
|
+
writeFileSync as writeFileSync2
|
|
793
|
+
} from "fs";
|
|
794
|
+
import { dirname as dirname3, isAbsolute, join as join6, relative } from "path";
|
|
795
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
796
|
+
function validateImportName(name) {
|
|
797
|
+
if (!name) return "empty_name";
|
|
798
|
+
if (!NAME_RE.test(name)) {
|
|
799
|
+
return "invalid_name (must match ^[a-z0-9][a-z0-9_-]*$ \u2014 lowercase slug, no dots/slashes/..)";
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
function normalizeImportPath(p) {
|
|
804
|
+
return p.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
805
|
+
}
|
|
806
|
+
function validateImportPath(rootDir, importPath) {
|
|
807
|
+
if (!importPath) return "empty_path";
|
|
808
|
+
if (isAbsolute(importPath)) return "absolute_path";
|
|
809
|
+
const norm = normalizeImportPath(importPath);
|
|
810
|
+
if (norm === "" || norm === ".") return "root_path (cannot mount over the rig root)";
|
|
811
|
+
const segs = norm.split("/");
|
|
812
|
+
if (segs.some((s) => s === "..")) return "dotdot_in_path";
|
|
813
|
+
if (segs.some((s) => s === "")) return "empty_segment";
|
|
814
|
+
if (segs[0] === ".rig") return "reserved_rig_path";
|
|
815
|
+
const realRoot = realpathSync(rootDir);
|
|
816
|
+
const abs = join6(realRoot, norm);
|
|
817
|
+
if (!isWithin(realRoot, abs)) return "escapes_root";
|
|
818
|
+
const realMount = realpathNearestExisting(abs);
|
|
819
|
+
if (!isWithin(realRoot, realMount)) return "symlink_escape";
|
|
820
|
+
const fileAncestor = firstExistingFileOnPath(abs);
|
|
821
|
+
if (fileAncestor) {
|
|
822
|
+
return fileAncestor === abs ? "mount_path_is_file" : "ancestor_is_file";
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
function validateImports(opts) {
|
|
827
|
+
const valid = [];
|
|
828
|
+
const errors = [];
|
|
829
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
830
|
+
const acceptedPaths = [];
|
|
831
|
+
for (const imp of opts.imports) {
|
|
832
|
+
const reject = (reason) => errors.push({ name: imp.name, path: imp.path, reason });
|
|
833
|
+
const nameErr = validateImportName(imp.name);
|
|
834
|
+
if (nameErr) {
|
|
835
|
+
reject(nameErr);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (seenNames.has(imp.name)) {
|
|
839
|
+
reject("duplicate_name");
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const pathErr = validateImportPath(opts.rootDir, imp.path);
|
|
843
|
+
if (pathErr) {
|
|
844
|
+
reject(pathErr);
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const norm = normalizeImportPath(imp.path);
|
|
848
|
+
if (acceptedPaths.some((p) => pathsOverlap(p, norm))) {
|
|
849
|
+
reject("overlaps_other_import");
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (opts.primaryTrackedPaths.some((tp) => tp === norm || tp.startsWith(norm + "/"))) {
|
|
853
|
+
reject("over_primary_content");
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
seenNames.add(imp.name);
|
|
857
|
+
acceptedPaths.push(norm);
|
|
858
|
+
valid.push({ ...imp, path: norm });
|
|
859
|
+
}
|
|
860
|
+
return { valid, errors };
|
|
861
|
+
}
|
|
862
|
+
function primaryStateDbPath(rootDir) {
|
|
863
|
+
return join6(rootDir, ".rig", "tap", "state.local.db");
|
|
864
|
+
}
|
|
865
|
+
function importStateDbPath(rootDir, name) {
|
|
866
|
+
return join6(rootDir, ".rig", "tap", "imports", name, "state.local.db");
|
|
867
|
+
}
|
|
868
|
+
function importsDir(rootDir) {
|
|
869
|
+
return join6(rootDir, ".rig", "tap", "imports");
|
|
870
|
+
}
|
|
871
|
+
function importMountFile(rootDir, name) {
|
|
872
|
+
return join6(importsDir(rootDir), `${name}.local.json`);
|
|
873
|
+
}
|
|
874
|
+
var MOUNT_FILE_SUFFIX = ".local.json";
|
|
875
|
+
function writeImportMount(rootDir, name, mount) {
|
|
876
|
+
const path = importMountFile(rootDir, name);
|
|
877
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
878
|
+
writeFileSync2(path, JSON.stringify(mount, null, 2) + "\n", { mode: 384 });
|
|
879
|
+
}
|
|
880
|
+
function readImportMount(rootDir, name) {
|
|
881
|
+
const file = importMountFile(rootDir, name);
|
|
882
|
+
if (!existsSync3(file)) return { ok: false, reason: "missing_token_file" };
|
|
883
|
+
let parsed;
|
|
884
|
+
try {
|
|
885
|
+
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
886
|
+
} catch {
|
|
887
|
+
return { ok: false, reason: "malformed_json" };
|
|
888
|
+
}
|
|
889
|
+
if (typeof parsed.bindingId !== "string" || parsed.bindingId === "") {
|
|
890
|
+
return { ok: false, reason: "missing_bindingId" };
|
|
891
|
+
}
|
|
892
|
+
if (typeof parsed.relayUrl !== "string" || parsed.relayUrl === "") {
|
|
893
|
+
return { ok: false, reason: "missing_relayUrl" };
|
|
894
|
+
}
|
|
895
|
+
if (typeof parsed.token !== "string" || parsed.token === "") {
|
|
896
|
+
return { ok: false, reason: "missing_token" };
|
|
897
|
+
}
|
|
898
|
+
if (typeof parsed.path !== "string" || parsed.path === "") {
|
|
899
|
+
return { ok: false, reason: "missing_path" };
|
|
900
|
+
}
|
|
901
|
+
const ops = Array.isArray(parsed.ops) ? parsed.ops : [];
|
|
902
|
+
if (!ops.includes("read") || !ops.includes("subscribe")) {
|
|
903
|
+
return { ok: false, reason: "insufficient_ops (need read+subscribe)" };
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
ok: true,
|
|
907
|
+
spec: {
|
|
908
|
+
name,
|
|
909
|
+
bindingId: parsed.bindingId,
|
|
910
|
+
relayUrl: parsed.relayUrl,
|
|
911
|
+
token: parsed.token,
|
|
912
|
+
path: parsed.path
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function loadImportMounts(rootDir) {
|
|
917
|
+
const dir = importsDir(rootDir);
|
|
918
|
+
const ready = [];
|
|
919
|
+
const broken = [];
|
|
920
|
+
if (!existsSync3(dir)) return { ready, broken };
|
|
921
|
+
for (const ent of readdirSync2(dir, { withFileTypes: true })) {
|
|
922
|
+
if (!ent.isFile() || !ent.name.endsWith(MOUNT_FILE_SUFFIX)) continue;
|
|
923
|
+
const name = ent.name.slice(0, -MOUNT_FILE_SUFFIX.length);
|
|
924
|
+
const nameErr = validateImportName(name);
|
|
925
|
+
if (nameErr) {
|
|
926
|
+
broken.push({ name, path: "", reason: nameErr });
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const res = readImportMount(rootDir, name);
|
|
930
|
+
if (!res.ok) {
|
|
931
|
+
broken.push({ name, path: "", reason: res.reason });
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
ready.push(res.spec);
|
|
935
|
+
}
|
|
936
|
+
return { ready, broken };
|
|
937
|
+
}
|
|
938
|
+
function isWithin(parent, child) {
|
|
939
|
+
const rel = relative(parent, child);
|
|
940
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
941
|
+
}
|
|
942
|
+
function realpathNearestExisting(abs) {
|
|
943
|
+
let cur = abs;
|
|
944
|
+
while (!existsSync3(cur)) {
|
|
945
|
+
const parent = dirname3(cur);
|
|
946
|
+
if (parent === cur) break;
|
|
947
|
+
cur = parent;
|
|
948
|
+
}
|
|
949
|
+
const realExisting = realpathSync(cur);
|
|
950
|
+
const remainder = relative(cur, abs);
|
|
951
|
+
return remainder ? join6(realExisting, remainder) : realExisting;
|
|
952
|
+
}
|
|
953
|
+
function firstExistingFileOnPath(abs) {
|
|
954
|
+
let cur = abs;
|
|
955
|
+
for (; ; ) {
|
|
956
|
+
if (existsSync3(cur)) {
|
|
957
|
+
return statSync2(cur).isFile() ? cur : null;
|
|
958
|
+
}
|
|
959
|
+
const parent = dirname3(cur);
|
|
960
|
+
if (parent === cur) return null;
|
|
961
|
+
cur = parent;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function pathsOverlap(a, b) {
|
|
965
|
+
return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/import-invite.ts
|
|
969
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync } from "fs";
|
|
970
|
+
import { dirname as dirname4, join as joinPath2 } from "path";
|
|
971
|
+
var ImportValidationFailed = class extends Error {
|
|
972
|
+
constructor(reason) {
|
|
973
|
+
super(`import rejected: ${reason}`);
|
|
974
|
+
this.reason = reason;
|
|
975
|
+
this.name = "ImportValidationFailed";
|
|
976
|
+
}
|
|
977
|
+
reason;
|
|
978
|
+
};
|
|
979
|
+
var ImportTokenScopeError = class extends Error {
|
|
980
|
+
constructor(ops) {
|
|
981
|
+
super(
|
|
982
|
+
ops ? `invite token has ops [${ops.join(", ")}] \u2014 a mirror needs read+subscribe` : `invite can't drive a mirror \u2014 it needs read+subscribe`
|
|
983
|
+
);
|
|
984
|
+
this.ops = ops;
|
|
985
|
+
this.name = "ImportTokenScopeError";
|
|
986
|
+
}
|
|
987
|
+
ops;
|
|
988
|
+
};
|
|
989
|
+
function isRecord(v) {
|
|
990
|
+
return typeof v === "object" && v !== null;
|
|
991
|
+
}
|
|
992
|
+
function deriveName(canonicalPath) {
|
|
993
|
+
const segs = canonicalPath.split("/").filter(Boolean);
|
|
994
|
+
return segs[segs.length - 1] ?? "";
|
|
995
|
+
}
|
|
996
|
+
function readPrimaryTrackedPaths(rootDir) {
|
|
997
|
+
const dbPath = primaryStateDbPath(rootDir);
|
|
998
|
+
if (!existsSync4(dbPath)) return [];
|
|
999
|
+
const db = openStateDb(dbPath);
|
|
1000
|
+
try {
|
|
1001
|
+
return db.listPaths();
|
|
1002
|
+
} finally {
|
|
1003
|
+
db.close();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function addImport(opts) {
|
|
1007
|
+
const path = normalizeImportPath(opts.path);
|
|
1008
|
+
const name = opts.name && opts.name.length > 0 ? opts.name : deriveName(path);
|
|
1009
|
+
const existing = loadImportMounts(opts.rootDir).ready;
|
|
1010
|
+
const candidate = { name, bindingId: "", relayUrl: "", token: "", path };
|
|
1011
|
+
const { valid, errors } = validateImports({
|
|
1012
|
+
rootDir: opts.rootDir,
|
|
1013
|
+
imports: [...existing, candidate],
|
|
1014
|
+
primaryTrackedPaths: readPrimaryTrackedPaths(opts.rootDir)
|
|
1015
|
+
});
|
|
1016
|
+
if (!valid.some((v) => v.name === name && v.path === path)) {
|
|
1017
|
+
const err = errors.find((e) => e.name === name);
|
|
1018
|
+
throw new ImportValidationFailed(err?.reason ?? "invalid_import");
|
|
1019
|
+
}
|
|
1020
|
+
const primaryBindingId = readBindingConfig(opts.rootDir)?.bindingId;
|
|
1021
|
+
const { baseUrl, secret } = parseInviteUrl(opts.inviteUrl);
|
|
1022
|
+
let accepted;
|
|
1023
|
+
try {
|
|
1024
|
+
accepted = await acceptInvite({
|
|
1025
|
+
baseUrl,
|
|
1026
|
+
secret,
|
|
1027
|
+
accessToken: opts.accessToken ?? null,
|
|
1028
|
+
request: {
|
|
1029
|
+
...opts.deviceLabel ? { deviceLabel: opts.deviceLabel } : {},
|
|
1030
|
+
importer: { ...primaryBindingId ? { bindingId: primaryBindingId } : {}, path }
|
|
1031
|
+
},
|
|
1032
|
+
fetch: opts.fetch
|
|
1033
|
+
});
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
if (err instanceof RelayError && isRecord(err.body) && err.body.reason === "import_requires_read_subscribe") {
|
|
1036
|
+
throw new ImportTokenScopeError();
|
|
1037
|
+
}
|
|
1038
|
+
throw err;
|
|
1039
|
+
}
|
|
1040
|
+
const ops = accepted.token.ops;
|
|
1041
|
+
if (!ops.includes("read") || !ops.includes("subscribe")) {
|
|
1042
|
+
throw new ImportTokenScopeError(ops);
|
|
1043
|
+
}
|
|
1044
|
+
writeImportMount(opts.rootDir, name, {
|
|
1045
|
+
bindingId: accepted.bindingId,
|
|
1046
|
+
relayUrl: baseUrl,
|
|
1047
|
+
token: accepted.token.secret,
|
|
1048
|
+
ops,
|
|
1049
|
+
path,
|
|
1050
|
+
...accepted.importEdgeId ? { edgeId: accepted.importEdgeId } : {}
|
|
1051
|
+
});
|
|
1052
|
+
return {
|
|
1053
|
+
name,
|
|
1054
|
+
path,
|
|
1055
|
+
bindingId: accepted.bindingId,
|
|
1056
|
+
ops,
|
|
1057
|
+
...accepted.importEdgeId ? { edgeId: accepted.importEdgeId } : {},
|
|
1058
|
+
// Only meaningful when we actually sent a binding to verify.
|
|
1059
|
+
...primaryBindingId !== void 0 ? { importerBindingVerified: accepted.importerBindingVerified ?? false } : {}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function readRawMount(rootDir, name) {
|
|
1063
|
+
const file = importMountFile(rootDir, name);
|
|
1064
|
+
if (!existsSync4(file)) return null;
|
|
1065
|
+
try {
|
|
1066
|
+
return JSON.parse(readFileSync5(file, "utf8"));
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function removeImport(opts) {
|
|
1072
|
+
const mountPath = importMountFile(opts.rootDir, opts.name);
|
|
1073
|
+
const res = readImportMount(opts.rootDir, opts.name);
|
|
1074
|
+
const raw = readRawMount(opts.rootDir, opts.name);
|
|
1075
|
+
const path = res.ok ? res.spec.path : raw?.path ?? null;
|
|
1076
|
+
let revoke2;
|
|
1077
|
+
if (!raw?.edgeId || !raw.relayUrl) {
|
|
1078
|
+
revoke2 = "no-edge";
|
|
1079
|
+
} else if (!opts.accessToken) {
|
|
1080
|
+
revoke2 = "no-token";
|
|
1081
|
+
} else {
|
|
1082
|
+
try {
|
|
1083
|
+
const r = await revokeImportEdge({
|
|
1084
|
+
baseUrl: raw.relayUrl,
|
|
1085
|
+
edgeId: raw.edgeId,
|
|
1086
|
+
accessToken: opts.accessToken,
|
|
1087
|
+
fetch: opts.fetch
|
|
1088
|
+
});
|
|
1089
|
+
revoke2 = r.revoked ? "revoked" : r.status === 403 ? "not-creator" : r.status === 404 ? "gone" : "offline";
|
|
1090
|
+
} catch {
|
|
1091
|
+
revoke2 = "offline";
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
let removed = false;
|
|
1095
|
+
if (existsSync4(mountPath)) {
|
|
1096
|
+
rmSync(mountPath, { force: true });
|
|
1097
|
+
removed = true;
|
|
1098
|
+
}
|
|
1099
|
+
rmSync(dirname4(importStateDbPath(opts.rootDir, opts.name)), {
|
|
1100
|
+
recursive: true,
|
|
1101
|
+
force: true
|
|
1102
|
+
});
|
|
1103
|
+
let purged = false;
|
|
1104
|
+
if (opts.purge && path) {
|
|
1105
|
+
rmSync(joinPath2(opts.rootDir, path), { recursive: true, force: true });
|
|
1106
|
+
purged = true;
|
|
1107
|
+
}
|
|
1108
|
+
return { removed, path, purged, revoke: revoke2 };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/applier.ts
|
|
1112
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
1113
|
+
import {
|
|
1114
|
+
existsSync as existsSync5,
|
|
1115
|
+
lstatSync,
|
|
1116
|
+
mkdirSync as mkdirSync4,
|
|
1117
|
+
readdirSync as readdirSync3,
|
|
1118
|
+
readFileSync as readFileSync6,
|
|
743
1119
|
renameSync,
|
|
744
1120
|
rmdirSync,
|
|
745
|
-
rmSync,
|
|
746
|
-
writeFileSync as
|
|
1121
|
+
rmSync as rmSync2,
|
|
1122
|
+
writeFileSync as writeFileSync3
|
|
747
1123
|
} from "fs";
|
|
748
|
-
import { basename, dirname as
|
|
1124
|
+
import { basename, dirname as dirname5, extname, join as join7, sep as sep2 } from "path";
|
|
749
1125
|
var STATE_KEY_CONFLICT_COUNT = "conflict_count";
|
|
750
1126
|
var HashMismatchError = class extends Error {
|
|
751
1127
|
constructor(path, expectedHash, actualHash) {
|
|
@@ -765,10 +1141,10 @@ function sha256Hex(bytes) {
|
|
|
765
1141
|
return createHash3("sha256").update(bytes).digest("hex");
|
|
766
1142
|
}
|
|
767
1143
|
function sha256OfFile(absPath) {
|
|
768
|
-
return `sha256:${sha256Hex(
|
|
1144
|
+
return `sha256:${sha256Hex(readFileSync6(absPath))}`;
|
|
769
1145
|
}
|
|
770
1146
|
function conflictSidecarPath(path, ev) {
|
|
771
|
-
const dir =
|
|
1147
|
+
const dir = dirname5(path);
|
|
772
1148
|
const ext = extname(path);
|
|
773
1149
|
const stem = basename(path, ext);
|
|
774
1150
|
const actor = ev.actorDeviceId ?? "unknown";
|
|
@@ -787,16 +1163,16 @@ function findConflictSidecars(rootDir) {
|
|
|
787
1163
|
walk2(".");
|
|
788
1164
|
return out;
|
|
789
1165
|
function walk2(rel) {
|
|
790
|
-
const abs = rel === "." ? rootDir :
|
|
1166
|
+
const abs = rel === "." ? rootDir : join7(rootDir, rel);
|
|
791
1167
|
let entries;
|
|
792
1168
|
try {
|
|
793
|
-
entries =
|
|
1169
|
+
entries = readdirSync3(abs, { withFileTypes: true });
|
|
794
1170
|
} catch {
|
|
795
1171
|
return;
|
|
796
1172
|
}
|
|
797
1173
|
for (const ent of entries) {
|
|
798
1174
|
if (ent.isSymbolicLink()) continue;
|
|
799
|
-
const childRel = rel === "." ? ent.name :
|
|
1175
|
+
const childRel = rel === "." ? ent.name : join7(rel, ent.name);
|
|
800
1176
|
if (ent.isDirectory()) {
|
|
801
1177
|
if (SCAN_SKIP_DIRS.has(ent.name)) continue;
|
|
802
1178
|
walk2(childRel);
|
|
@@ -806,7 +1182,7 @@ function findConflictSidecars(rootDir) {
|
|
|
806
1182
|
const parsed = parseConflictSidecar(ent.name);
|
|
807
1183
|
if (!parsed) continue;
|
|
808
1184
|
const sidecarPosix = sep2 === "/" ? childRel : childRel.replaceAll(sep2, "/");
|
|
809
|
-
const dir =
|
|
1185
|
+
const dir = dirname5(sidecarPosix);
|
|
810
1186
|
const original = dir === "." ? `${parsed.stem}${parsed.ext}` : `${dir}/${parsed.stem}${parsed.ext}`;
|
|
811
1187
|
out.push({ path: original, sidecarPath: sidecarPosix });
|
|
812
1188
|
}
|
|
@@ -822,6 +1198,7 @@ function bumpConflictCount(stateDb) {
|
|
|
822
1198
|
var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
|
|
823
1199
|
var STATE_KEY_LAST_APPLY_ERROR_AT = "last_apply_error_at";
|
|
824
1200
|
async function applyOnce(opts) {
|
|
1201
|
+
const mode = opts.mode ?? "primary";
|
|
825
1202
|
const cursor = opts.stateDb.getCursor();
|
|
826
1203
|
const since = await opts.client.listChanges({ after: cursor, limit: opts.limit });
|
|
827
1204
|
let applied = 0;
|
|
@@ -834,7 +1211,8 @@ async function applyOnce(opts) {
|
|
|
834
1211
|
rootDir: opts.rootDir,
|
|
835
1212
|
client: opts.client,
|
|
836
1213
|
stateDb: opts.stateDb,
|
|
837
|
-
event: ev
|
|
1214
|
+
event: ev,
|
|
1215
|
+
mode
|
|
838
1216
|
});
|
|
839
1217
|
if (wasEcho) echoSkipped += 1;
|
|
840
1218
|
else applied += 1;
|
|
@@ -859,13 +1237,14 @@ async function applyOnce(opts) {
|
|
|
859
1237
|
return { applied, echoSkipped, errored, events: since.events, errors };
|
|
860
1238
|
}
|
|
861
1239
|
async function applyEvent(opts) {
|
|
862
|
-
const { rootDir, client, stateDb, event: ev } = opts;
|
|
863
|
-
const
|
|
1240
|
+
const { rootDir, client, stateDb, event: ev, mode } = opts;
|
|
1241
|
+
const mirror = mode === "mirror";
|
|
1242
|
+
const abs = join7(rootDir, ev.path);
|
|
864
1243
|
switch (ev.op) {
|
|
865
1244
|
case "write": {
|
|
866
1245
|
if (!ev.hash) throw new Error(`write event ${ev.id} missing hash`);
|
|
867
1246
|
const local = stateDb.getPath(ev.path);
|
|
868
|
-
if (local?.lastSeenHash === ev.hash &&
|
|
1247
|
+
if (local?.lastSeenHash === ev.hash && existsSync5(abs)) {
|
|
869
1248
|
stateDb.upsertPath({
|
|
870
1249
|
path: ev.path,
|
|
871
1250
|
lastAppliedChangeId: ev.id,
|
|
@@ -875,12 +1254,15 @@ async function applyEvent(opts) {
|
|
|
875
1254
|
});
|
|
876
1255
|
return true;
|
|
877
1256
|
}
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1257
|
+
if (existsSync5(abs) && lstatSync(abs).isDirectory()) {
|
|
1258
|
+
if (!mirror) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`file_vs_dir_collision: ${ev.path} is a local directory; refusing to overwrite with a file`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
882
1264
|
}
|
|
883
|
-
const localDirty =
|
|
1265
|
+
const localDirty = !mirror && existsSync5(abs) && local?.lastSeenHash != null && sha256OfFile(abs) !== local.lastSeenHash;
|
|
884
1266
|
const dl = await client.downloadUrl(ev.hash);
|
|
885
1267
|
const bytes = await client.getObjectBytes(dl.downloadUrl);
|
|
886
1268
|
const actualHash = `sha256:${sha256Hex(bytes)}`;
|
|
@@ -889,10 +1271,10 @@ async function applyEvent(opts) {
|
|
|
889
1271
|
}
|
|
890
1272
|
if (localDirty) {
|
|
891
1273
|
const sidecarPath = conflictSidecarPath(ev.path, ev);
|
|
892
|
-
const sidecarAbs =
|
|
893
|
-
|
|
1274
|
+
const sidecarAbs = join7(rootDir, sidecarPath);
|
|
1275
|
+
mkdirSync4(dirname5(sidecarAbs), { recursive: true });
|
|
894
1276
|
const tmp2 = `${sidecarAbs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
895
|
-
|
|
1277
|
+
writeFileSync3(tmp2, bytes, { mode: ev.executable ? 493 : 420 });
|
|
896
1278
|
renameSync(tmp2, sidecarAbs);
|
|
897
1279
|
stateDb.upsertPath({
|
|
898
1280
|
path: ev.path,
|
|
@@ -904,9 +1286,9 @@ async function applyEvent(opts) {
|
|
|
904
1286
|
bumpConflictCount(stateDb);
|
|
905
1287
|
return false;
|
|
906
1288
|
}
|
|
907
|
-
|
|
1289
|
+
mkdirSync4(dirname5(abs), { recursive: true });
|
|
908
1290
|
const tmp = `${abs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
909
|
-
|
|
1291
|
+
writeFileSync3(tmp, bytes, { mode: ev.executable ? 493 : 420 });
|
|
910
1292
|
renameSync(tmp, abs);
|
|
911
1293
|
stateDb.upsertPath({
|
|
912
1294
|
path: ev.path,
|
|
@@ -918,36 +1300,43 @@ async function applyEvent(opts) {
|
|
|
918
1300
|
return false;
|
|
919
1301
|
}
|
|
920
1302
|
case "delete": {
|
|
921
|
-
if (
|
|
1303
|
+
if (existsSync5(abs)) {
|
|
922
1304
|
if (lstatSync(abs).isDirectory()) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const localUntracked = localStateRow?.lastSeenHash == null;
|
|
931
|
-
if (localDirty || localUntracked) {
|
|
932
|
-
stateDb.upsertPath({
|
|
933
|
-
path: ev.path,
|
|
934
|
-
lastAppliedChangeId: ev.id,
|
|
935
|
-
// null so scanAndPush's `prev?.lastSeenHash === hash` check
|
|
936
|
-
// falls through and the file gets re-submitted as a write.
|
|
937
|
-
lastSeenHash: null,
|
|
938
|
-
localDirty: true,
|
|
939
|
-
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
940
|
-
});
|
|
941
|
-
bumpConflictCount(stateDb);
|
|
1305
|
+
if (!mirror) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`delete_vs_dir: ${ev.path} is a local directory; refusing recursive removal`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
1311
|
+
stateDb.deletePath(ev.path);
|
|
942
1312
|
return false;
|
|
943
1313
|
}
|
|
944
|
-
|
|
1314
|
+
if (!mirror) {
|
|
1315
|
+
const localStateRow = stateDb.getPath(ev.path);
|
|
1316
|
+
const localBytesHash = sha256OfFile(abs);
|
|
1317
|
+
const localDirty = localStateRow?.lastSeenHash != null && localBytesHash !== localStateRow.lastSeenHash;
|
|
1318
|
+
const localUntracked = localStateRow?.lastSeenHash == null;
|
|
1319
|
+
if (localDirty || localUntracked) {
|
|
1320
|
+
stateDb.upsertPath({
|
|
1321
|
+
path: ev.path,
|
|
1322
|
+
lastAppliedChangeId: ev.id,
|
|
1323
|
+
// null so scanAndPush's `prev?.lastSeenHash === hash` check
|
|
1324
|
+
// falls through and the file gets re-submitted as a write.
|
|
1325
|
+
lastSeenHash: null,
|
|
1326
|
+
localDirty: true,
|
|
1327
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1328
|
+
});
|
|
1329
|
+
bumpConflictCount(stateDb);
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
rmSync2(abs, { force: true });
|
|
945
1334
|
}
|
|
946
1335
|
stateDb.deletePath(ev.path);
|
|
947
1336
|
return false;
|
|
948
1337
|
}
|
|
949
1338
|
case "mkdir": {
|
|
950
|
-
|
|
1339
|
+
mkdirSync4(abs, { recursive: true });
|
|
951
1340
|
stateDb.upsertPath({
|
|
952
1341
|
path: ev.path,
|
|
953
1342
|
lastAppliedChangeId: ev.id,
|
|
@@ -958,9 +1347,13 @@ async function applyEvent(opts) {
|
|
|
958
1347
|
return false;
|
|
959
1348
|
}
|
|
960
1349
|
case "rmdir": {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
1350
|
+
if (mirror) {
|
|
1351
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
1352
|
+
} else {
|
|
1353
|
+
try {
|
|
1354
|
+
rmdirSync(abs);
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
964
1357
|
}
|
|
965
1358
|
stateDb.deletePath(ev.path);
|
|
966
1359
|
return false;
|
|
@@ -1106,19 +1499,119 @@ function asError(err, fallbackName) {
|
|
|
1106
1499
|
return e;
|
|
1107
1500
|
}
|
|
1108
1501
|
|
|
1502
|
+
// src/subscription.ts
|
|
1503
|
+
var DEFAULT_POLL_SECONDS = 3;
|
|
1504
|
+
var SSE_APPLY_DEBOUNCE_MS = 50;
|
|
1505
|
+
async function startSubscription(opts) {
|
|
1506
|
+
const { mode } = opts;
|
|
1507
|
+
const stateDb = openStateDb(opts.stateDbPath);
|
|
1508
|
+
const client = new RelayClient({
|
|
1509
|
+
baseUrl: opts.config.relayUrl,
|
|
1510
|
+
bindingId: opts.config.bindingId,
|
|
1511
|
+
token: opts.config.token,
|
|
1512
|
+
fetch: opts.fetch
|
|
1513
|
+
});
|
|
1514
|
+
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS;
|
|
1515
|
+
let stopped = false;
|
|
1516
|
+
let pollTimer;
|
|
1517
|
+
let sseApplyTimer;
|
|
1518
|
+
let sseSubscriber;
|
|
1519
|
+
let applyInFlight = null;
|
|
1520
|
+
let rerunRequested = false;
|
|
1521
|
+
let lastApplyResult;
|
|
1522
|
+
const triggerApply = () => {
|
|
1523
|
+
if (applyInFlight) {
|
|
1524
|
+
rerunRequested = true;
|
|
1525
|
+
return applyInFlight;
|
|
1526
|
+
}
|
|
1527
|
+
const loop = (async () => {
|
|
1528
|
+
try {
|
|
1529
|
+
do {
|
|
1530
|
+
rerunRequested = false;
|
|
1531
|
+
try {
|
|
1532
|
+
lastApplyResult = await applyOnce({
|
|
1533
|
+
rootDir: opts.rootDir,
|
|
1534
|
+
client,
|
|
1535
|
+
stateDb,
|
|
1536
|
+
mode
|
|
1537
|
+
});
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
console.error("tapd apply error:", err);
|
|
1540
|
+
}
|
|
1541
|
+
} while (rerunRequested && !stopped);
|
|
1542
|
+
return lastApplyResult;
|
|
1543
|
+
} finally {
|
|
1544
|
+
applyInFlight = null;
|
|
1545
|
+
}
|
|
1546
|
+
})();
|
|
1547
|
+
applyInFlight = loop;
|
|
1548
|
+
return loop;
|
|
1549
|
+
};
|
|
1550
|
+
const schedulePoll = () => {
|
|
1551
|
+
if (stopped) return;
|
|
1552
|
+
pollTimer = setTimeout(() => {
|
|
1553
|
+
triggerApply().finally(() => schedulePoll());
|
|
1554
|
+
}, pollSeconds * 1e3);
|
|
1555
|
+
};
|
|
1556
|
+
const scheduleSseApply = () => {
|
|
1557
|
+
if (stopped) return;
|
|
1558
|
+
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1559
|
+
sseApplyTimer = setTimeout(() => {
|
|
1560
|
+
sseApplyTimer = void 0;
|
|
1561
|
+
void triggerApply();
|
|
1562
|
+
}, SSE_APPLY_DEBOUNCE_MS);
|
|
1563
|
+
};
|
|
1564
|
+
if (!opts.noSse) {
|
|
1565
|
+
sseSubscriber = subscribe({
|
|
1566
|
+
baseUrl: opts.config.relayUrl,
|
|
1567
|
+
bindingId: opts.config.bindingId,
|
|
1568
|
+
token: opts.config.token,
|
|
1569
|
+
fetch: opts.fetch,
|
|
1570
|
+
heartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs,
|
|
1571
|
+
onChange: () => scheduleSseApply(),
|
|
1572
|
+
onError: (err) => {
|
|
1573
|
+
console.error("tapd sse:", err.message);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
await triggerApply();
|
|
1578
|
+
schedulePoll();
|
|
1579
|
+
return {
|
|
1580
|
+
async stop() {
|
|
1581
|
+
stopped = true;
|
|
1582
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1583
|
+
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1584
|
+
if (sseSubscriber) await sseSubscriber.stop();
|
|
1585
|
+
await Promise.allSettled([applyInFlight ?? Promise.resolve()]);
|
|
1586
|
+
stateDb.close();
|
|
1587
|
+
},
|
|
1588
|
+
async applyNow() {
|
|
1589
|
+
const result = await triggerApply();
|
|
1590
|
+
return result ?? {
|
|
1591
|
+
applied: 0,
|
|
1592
|
+
echoSkipped: 0,
|
|
1593
|
+
errored: 0,
|
|
1594
|
+
events: [],
|
|
1595
|
+
errors: []
|
|
1596
|
+
};
|
|
1597
|
+
},
|
|
1598
|
+
stateDb
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1109
1602
|
// src/uploader.ts
|
|
1110
1603
|
import { createHash as createHash4 } from "crypto";
|
|
1111
|
-
import { readFileSync as
|
|
1112
|
-
import { join as
|
|
1604
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1605
|
+
import { join as join8 } from "path";
|
|
1113
1606
|
function sha256OfBuffer2(bytes) {
|
|
1114
1607
|
return `sha256:${createHash4("sha256").update(bytes).digest("hex")}`;
|
|
1115
1608
|
}
|
|
1116
1609
|
async function computeLocalDiff(opts) {
|
|
1117
|
-
const { rootDir, stateDb, ignore: ignore2 } = opts;
|
|
1118
|
-
const scan = walk(rootDir, { ignore: ignore2 });
|
|
1610
|
+
const { rootDir, stateDb, ignore: ignore2, excludePaths } = opts;
|
|
1611
|
+
const scan = walk(rootDir, { ignore: ignore2, excludePaths });
|
|
1119
1612
|
const writes = [];
|
|
1120
1613
|
for (const f of scan.files) {
|
|
1121
|
-
const hash = await hashFile(
|
|
1614
|
+
const hash = await hashFile(join8(rootDir, f.path));
|
|
1122
1615
|
const prev = stateDb.getPath(f.path);
|
|
1123
1616
|
if (prev?.lastSeenHash === hash) continue;
|
|
1124
1617
|
const baseChangeId = prev?.lastAppliedChangeId ?? void 0;
|
|
@@ -1187,7 +1680,7 @@ async function scanAndPush(opts) {
|
|
|
1187
1680
|
reused += 1;
|
|
1188
1681
|
continue;
|
|
1189
1682
|
}
|
|
1190
|
-
const bytes =
|
|
1683
|
+
const bytes = readFileSync7(join8(rootDir, f.path));
|
|
1191
1684
|
const actual = sha256OfBuffer2(bytes);
|
|
1192
1685
|
if (actual !== hash) {
|
|
1193
1686
|
mutatedPaths.add(f.path);
|
|
@@ -1267,152 +1760,183 @@ function cursorLessThan(a, b) {
|
|
|
1267
1760
|
}
|
|
1268
1761
|
|
|
1269
1762
|
// src/start.ts
|
|
1270
|
-
import { join as
|
|
1763
|
+
import { join as join9 } from "path";
|
|
1271
1764
|
import chokidar from "chokidar";
|
|
1272
|
-
var
|
|
1765
|
+
var DEFAULT_POLL_SECONDS2 = 3;
|
|
1273
1766
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1767
|
+
var NOOP_APPLY = {
|
|
1768
|
+
applied: 0,
|
|
1769
|
+
echoSkipped: 0,
|
|
1770
|
+
errored: 0,
|
|
1771
|
+
events: [],
|
|
1772
|
+
errors: []
|
|
1773
|
+
};
|
|
1774
|
+
var NOOP_SCAN = {
|
|
1775
|
+
uploaded: 0,
|
|
1776
|
+
reused: 0,
|
|
1777
|
+
submitted: [],
|
|
1778
|
+
warnings: []
|
|
1779
|
+
};
|
|
1274
1780
|
async function start(opts) {
|
|
1275
1781
|
const config = readBindingConfig(opts.rootDir);
|
|
1276
|
-
|
|
1782
|
+
let importSpecs;
|
|
1783
|
+
let loaderBroken = [];
|
|
1784
|
+
if (opts.imports) {
|
|
1785
|
+
importSpecs = opts.imports;
|
|
1786
|
+
} else {
|
|
1787
|
+
const loaded = loadImportMounts(opts.rootDir);
|
|
1788
|
+
importSpecs = loaded.ready;
|
|
1789
|
+
loaderBroken = loaded.broken;
|
|
1790
|
+
}
|
|
1791
|
+
for (const e of loaderBroken) {
|
|
1792
|
+
console.error(`tapd import "${e.name}": skipped \u2014 ${e.reason}`);
|
|
1793
|
+
}
|
|
1794
|
+
if (!config && importSpecs.length === 0) {
|
|
1277
1795
|
throw new Error(
|
|
1278
|
-
`no binding config at ${opts.rootDir}/.rig/tap-binding.local.json \u2014 run \`tapd init\` first`
|
|
1796
|
+
`no binding config at ${opts.rootDir}/.rig/tap-binding.local.json and no imports \u2014 run \`tapd init\` first`
|
|
1279
1797
|
);
|
|
1280
1798
|
}
|
|
1281
|
-
const
|
|
1282
|
-
join8(opts.rootDir, ".rig", "tap", "state.local.db")
|
|
1283
|
-
);
|
|
1284
|
-
const ignore2 = loadIgnore(opts.rootDir);
|
|
1285
|
-
const client = new RelayClient({
|
|
1286
|
-
baseUrl: config.relayUrl,
|
|
1287
|
-
bindingId: config.bindingId,
|
|
1288
|
-
token: config.token,
|
|
1289
|
-
fetch: opts.fetch
|
|
1290
|
-
});
|
|
1291
|
-
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS;
|
|
1799
|
+
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS2;
|
|
1292
1800
|
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1293
1801
|
let stopped = false;
|
|
1294
|
-
let
|
|
1295
|
-
let scanTimer;
|
|
1296
|
-
let sseApplyTimer;
|
|
1802
|
+
let primary;
|
|
1297
1803
|
let watcher;
|
|
1804
|
+
let scanTimer;
|
|
1298
1805
|
let scanInFlight = Promise.resolve();
|
|
1299
|
-
let
|
|
1300
|
-
|
|
1301
|
-
let
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
applyInFlight = loop;
|
|
1328
|
-
return loop;
|
|
1329
|
-
};
|
|
1330
|
-
const schedulePoll = () => {
|
|
1331
|
-
if (stopped) return;
|
|
1332
|
-
pollTimer = setTimeout(() => {
|
|
1333
|
-
triggerApply().finally(() => schedulePoll());
|
|
1334
|
-
}, pollSeconds * 1e3);
|
|
1335
|
-
};
|
|
1336
|
-
const scheduleScan = () => {
|
|
1337
|
-
if (stopped) return;
|
|
1338
|
-
if (scanTimer) clearTimeout(scanTimer);
|
|
1339
|
-
scanTimer = setTimeout(() => {
|
|
1340
|
-
scanInFlight = scanAndPush({ rootDir: opts.rootDir, client, stateDb, ignore: ignore2 }).catch((err) => {
|
|
1341
|
-
console.error("tapd scan error:", err);
|
|
1342
|
-
});
|
|
1343
|
-
}, debounceMs);
|
|
1344
|
-
};
|
|
1345
|
-
const SSE_APPLY_DEBOUNCE_MS = 50;
|
|
1346
|
-
const scheduleSseApply = () => {
|
|
1347
|
-
if (stopped) return;
|
|
1348
|
-
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1349
|
-
sseApplyTimer = setTimeout(() => {
|
|
1350
|
-
sseApplyTimer = void 0;
|
|
1351
|
-
void triggerApply();
|
|
1352
|
-
}, SSE_APPLY_DEBOUNCE_MS);
|
|
1353
|
-
};
|
|
1354
|
-
if (!opts.noSse) {
|
|
1355
|
-
sseSubscriber = subscribe({
|
|
1806
|
+
let writerClient;
|
|
1807
|
+
const ignore2 = loadIgnore(opts.rootDir);
|
|
1808
|
+
let importPaths = [];
|
|
1809
|
+
if (config) {
|
|
1810
|
+
primary = await startSubscription({
|
|
1811
|
+
rootDir: opts.rootDir,
|
|
1812
|
+
config,
|
|
1813
|
+
stateDbPath: primaryStateDbPath(opts.rootDir),
|
|
1814
|
+
mode: "primary",
|
|
1815
|
+
fetch: opts.fetch,
|
|
1816
|
+
pollSeconds,
|
|
1817
|
+
noSse: opts.noSse,
|
|
1818
|
+
sseHeartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
const primaryTrackedPaths = primary ? primary.stateDb.listPaths() : [];
|
|
1822
|
+
const { valid: validImports, errors: importErrors } = validateImports({
|
|
1823
|
+
rootDir: opts.rootDir,
|
|
1824
|
+
imports: importSpecs,
|
|
1825
|
+
primaryTrackedPaths
|
|
1826
|
+
});
|
|
1827
|
+
for (const e of importErrors) {
|
|
1828
|
+
console.error(`tapd import "${e.name}" (${e.path}): skipped \u2014 ${e.reason}`);
|
|
1829
|
+
}
|
|
1830
|
+
importPaths = validImports.map((i) => i.path);
|
|
1831
|
+
if (config && primary) {
|
|
1832
|
+
const primaryStateDb = primary.stateDb;
|
|
1833
|
+
writerClient = new RelayClient({
|
|
1356
1834
|
baseUrl: config.relayUrl,
|
|
1357
1835
|
bindingId: config.bindingId,
|
|
1358
1836
|
token: config.token,
|
|
1359
|
-
fetch: opts.fetch
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1837
|
+
fetch: opts.fetch
|
|
1838
|
+
});
|
|
1839
|
+
const scheduleScan = () => {
|
|
1840
|
+
if (stopped) return;
|
|
1841
|
+
if (scanTimer) clearTimeout(scanTimer);
|
|
1842
|
+
scanTimer = setTimeout(() => {
|
|
1843
|
+
scanInFlight = scanAndPush({
|
|
1844
|
+
rootDir: opts.rootDir,
|
|
1845
|
+
client: writerClient,
|
|
1846
|
+
stateDb: primaryStateDb,
|
|
1847
|
+
ignore: ignore2,
|
|
1848
|
+
excludePaths: importPaths
|
|
1849
|
+
}).catch((err) => {
|
|
1850
|
+
console.error("tapd scan error:", err);
|
|
1851
|
+
});
|
|
1852
|
+
}, debounceMs);
|
|
1853
|
+
};
|
|
1854
|
+
if (!opts.noWatch) {
|
|
1855
|
+
watcher = chokidar.watch(opts.rootDir, {
|
|
1856
|
+
ignoreInitial: true,
|
|
1857
|
+
// Hard-exclude tap metadata AND every import mount point — the
|
|
1858
|
+
// latter so a mirrored subtree never triggers a re-upload into
|
|
1859
|
+
// the primary binding.
|
|
1860
|
+
ignored: (path) => {
|
|
1861
|
+
const rel = path.startsWith(opts.rootDir) ? path.slice(opts.rootDir.length).replace(/^[/\\]/, "") : path;
|
|
1862
|
+
if (isAlwaysExcluded(rel)) return true;
|
|
1863
|
+
if (importPaths.length > 0 && isUnderAny(rel, importPaths)) return true;
|
|
1864
|
+
return false;
|
|
1865
|
+
},
|
|
1866
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
1867
|
+
atomic: true
|
|
1868
|
+
});
|
|
1869
|
+
for (const ev of ["add", "change", "unlink", "addDir", "unlinkDir"]) {
|
|
1870
|
+
watcher.on(ev, () => scheduleScan());
|
|
1364
1871
|
}
|
|
1872
|
+
}
|
|
1873
|
+
scanInFlight = scanAndPush({
|
|
1874
|
+
rootDir: opts.rootDir,
|
|
1875
|
+
client: writerClient,
|
|
1876
|
+
stateDb: primaryStateDb,
|
|
1877
|
+
ignore: ignore2,
|
|
1878
|
+
excludePaths: importPaths
|
|
1879
|
+
}).catch((err) => {
|
|
1880
|
+
console.error("tapd initial scan error:", err);
|
|
1365
1881
|
});
|
|
1882
|
+
await scanInFlight;
|
|
1366
1883
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1884
|
+
const imports = [];
|
|
1885
|
+
const mirrorSubs = [];
|
|
1886
|
+
for (const imp of validImports) {
|
|
1887
|
+
try {
|
|
1888
|
+
const sub = await startSubscription({
|
|
1889
|
+
rootDir: join9(opts.rootDir, imp.path),
|
|
1890
|
+
config: { bindingId: imp.bindingId, relayUrl: imp.relayUrl, token: imp.token },
|
|
1891
|
+
stateDbPath: importStateDbPath(opts.rootDir, imp.name),
|
|
1892
|
+
mode: "mirror",
|
|
1893
|
+
fetch: opts.fetch,
|
|
1894
|
+
pollSeconds,
|
|
1895
|
+
noSse: opts.noSse,
|
|
1896
|
+
sseHeartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs
|
|
1897
|
+
});
|
|
1898
|
+
mirrorSubs.push(sub);
|
|
1899
|
+
imports.push({
|
|
1900
|
+
name: imp.name,
|
|
1901
|
+
path: imp.path,
|
|
1902
|
+
applyNow: () => sub.applyNow(),
|
|
1903
|
+
stateDb: sub.stateDb
|
|
1904
|
+
});
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1907
|
+
console.error(`tapd import "${imp.name}" (${imp.path}): failed to start \u2014 ${reason}`);
|
|
1383
1908
|
}
|
|
1384
1909
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
await scanInFlight;
|
|
1390
|
-
schedulePoll();
|
|
1910
|
+
const stateDb = primary?.stateDb ?? imports[0]?.stateDb;
|
|
1911
|
+
if (!stateDb) {
|
|
1912
|
+
throw new Error("tapd start: no primary binding and no import mounted successfully");
|
|
1913
|
+
}
|
|
1391
1914
|
return {
|
|
1392
1915
|
async stop() {
|
|
1393
1916
|
stopped = true;
|
|
1394
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
1395
1917
|
if (scanTimer) clearTimeout(scanTimer);
|
|
1396
|
-
|
|
1397
|
-
if (sseSubscriber) await sseSubscriber.stop();
|
|
1398
|
-
await Promise.allSettled([applyInFlight ?? Promise.resolve(), scanInFlight]);
|
|
1918
|
+
await Promise.allSettled([scanInFlight]);
|
|
1399
1919
|
if (watcher) await watcher.close();
|
|
1400
|
-
|
|
1920
|
+
const subs = [];
|
|
1921
|
+
if (primary) subs.push(primary.stop());
|
|
1922
|
+
for (const sub of mirrorSubs) subs.push(sub.stop());
|
|
1923
|
+
await Promise.allSettled(subs);
|
|
1401
1924
|
},
|
|
1402
1925
|
async applyNow() {
|
|
1403
|
-
|
|
1404
|
-
return result ?? {
|
|
1405
|
-
applied: 0,
|
|
1406
|
-
echoSkipped: 0,
|
|
1407
|
-
errored: 0,
|
|
1408
|
-
events: [],
|
|
1409
|
-
errors: []
|
|
1410
|
-
};
|
|
1926
|
+
return primary ? primary.applyNow() : NOOP_APPLY;
|
|
1411
1927
|
},
|
|
1412
1928
|
async scanNow() {
|
|
1413
|
-
|
|
1929
|
+
if (!config || !primary || !writerClient) return NOOP_SCAN;
|
|
1930
|
+
return scanAndPush({
|
|
1931
|
+
rootDir: opts.rootDir,
|
|
1932
|
+
client: writerClient,
|
|
1933
|
+
stateDb: primary.stateDb,
|
|
1934
|
+
ignore: ignore2,
|
|
1935
|
+
excludePaths: importPaths
|
|
1936
|
+
});
|
|
1414
1937
|
},
|
|
1415
|
-
stateDb
|
|
1938
|
+
stateDb,
|
|
1939
|
+
imports
|
|
1416
1940
|
};
|
|
1417
1941
|
}
|
|
1418
1942
|
|
|
@@ -1440,13 +1964,29 @@ export {
|
|
|
1440
1964
|
InvalidInviteUrlError,
|
|
1441
1965
|
parseInviteUrl,
|
|
1442
1966
|
join5 as join,
|
|
1967
|
+
validateImportName,
|
|
1968
|
+
normalizeImportPath,
|
|
1969
|
+
validateImportPath,
|
|
1970
|
+
validateImports,
|
|
1971
|
+
primaryStateDbPath,
|
|
1972
|
+
importStateDbPath,
|
|
1973
|
+
importsDir,
|
|
1974
|
+
importMountFile,
|
|
1975
|
+
writeImportMount,
|
|
1976
|
+
readImportMount,
|
|
1977
|
+
loadImportMounts,
|
|
1978
|
+
ImportValidationFailed,
|
|
1979
|
+
ImportTokenScopeError,
|
|
1980
|
+
addImport,
|
|
1981
|
+
removeImport,
|
|
1443
1982
|
STATE_KEY_CONFLICT_COUNT,
|
|
1444
1983
|
conflictSidecarPath,
|
|
1445
1984
|
findConflictSidecars,
|
|
1446
1985
|
applyOnce,
|
|
1447
1986
|
subscribe,
|
|
1987
|
+
startSubscription,
|
|
1448
1988
|
computeLocalDiff,
|
|
1449
1989
|
scanAndPush,
|
|
1450
1990
|
start
|
|
1451
1991
|
};
|
|
1452
|
-
//# sourceMappingURL=chunk-
|
|
1992
|
+
//# sourceMappingURL=chunk-YPZ2UNI5.js.map
|