@rigxyz/tapd 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +236 -34
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-N3KRCOBG.js → chunk-JBX2ZRMD.js} +739 -190
- package/dist/chunk-JBX2ZRMD.js.map +1 -0
- package/dist/index.d.ts +318 -19
- package/dist/index.js +35 -3
- package/package.json +12 -12
- package/LICENSE +0 -21
- package/dist/chunk-N3KRCOBG.js.map +0 -1
|
@@ -94,6 +94,17 @@ async function createBinding(opts) {
|
|
|
94
94
|
});
|
|
95
95
|
return expectJson(route, res);
|
|
96
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
|
+
}
|
|
97
108
|
var RelayClient = class {
|
|
98
109
|
baseUrl;
|
|
99
110
|
bindingId;
|
|
@@ -148,7 +159,14 @@ var RelayClient = class {
|
|
|
148
159
|
);
|
|
149
160
|
return expectJson(route, res);
|
|
150
161
|
}
|
|
151
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* PUT bytes to a presigned URL. Throws on non-2xx — except 409, which
|
|
164
|
+
* means the content-addressed object is already stored (same hash = same
|
|
165
|
+
* bytes). That's idempotent success, not a conflict: two files with
|
|
166
|
+
* identical content (e.g. several empty files), or an initial upload that
|
|
167
|
+
* overlaps the daemon's first scan, race to PUT the same key. Treating
|
|
168
|
+
* 409 as fatal aborts the whole push and wedges sync at "N pending".
|
|
169
|
+
*/
|
|
152
170
|
async putObjectBytes(uploadUrl, bytes) {
|
|
153
171
|
const route = "PUT <uploadUrl>";
|
|
154
172
|
const res = await this.fetch(uploadUrl, {
|
|
@@ -156,6 +174,7 @@ var RelayClient = class {
|
|
|
156
174
|
body: new Uint8Array(bytes),
|
|
157
175
|
headers: { "content-length": String(bytes.byteLength) }
|
|
158
176
|
});
|
|
177
|
+
if (res.status === 409) return;
|
|
159
178
|
if (!res.ok) {
|
|
160
179
|
throw new RelayError(route, res.status, await readJson(res));
|
|
161
180
|
}
|
|
@@ -426,6 +445,15 @@ function isAlwaysExcluded(posixPath) {
|
|
|
426
445
|
}
|
|
427
446
|
return false;
|
|
428
447
|
}
|
|
448
|
+
function isUnderAny(posixPath, prefixes) {
|
|
449
|
+
for (const raw of prefixes) {
|
|
450
|
+
const prefix = raw.replace(/\/+$/, "");
|
|
451
|
+
if (prefix === "") continue;
|
|
452
|
+
if (posixPath === prefix) return true;
|
|
453
|
+
if (posixPath.startsWith(prefix + "/")) return true;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
429
457
|
|
|
430
458
|
// src/walker.ts
|
|
431
459
|
import { readdirSync, statSync } from "fs";
|
|
@@ -462,6 +490,7 @@ function walk(rootDir, opts) {
|
|
|
462
490
|
const emptyDirs = [];
|
|
463
491
|
const directoriesScanned = [];
|
|
464
492
|
const warnings = [];
|
|
493
|
+
const excludePaths = opts.excludePaths ?? [];
|
|
465
494
|
walkDir(".");
|
|
466
495
|
return { files, emptyDirs, directoriesScanned, warnings };
|
|
467
496
|
function walkDir(rel) {
|
|
@@ -479,6 +508,10 @@ function walk(rootDir, opts) {
|
|
|
479
508
|
const posix = posixOf(childRel);
|
|
480
509
|
const isDir = ent.isDirectory();
|
|
481
510
|
if (isAlwaysExcluded(posix)) continue;
|
|
511
|
+
if (excludePaths.length > 0 && isUnderAny(posix, excludePaths)) {
|
|
512
|
+
hasVisible = true;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
482
515
|
if (opts.ignore.ignores(posix + (isDir ? "/" : ""))) continue;
|
|
483
516
|
if (ent.isSymbolicLink()) {
|
|
484
517
|
warnings.push({ path: posix, reason: "symlink" });
|
|
@@ -537,9 +570,33 @@ async function hashFile(absPath) {
|
|
|
537
570
|
import { createHash as createHash2 } from "crypto";
|
|
538
571
|
import { readFileSync as readFileSync3 } from "fs";
|
|
539
572
|
import { join as join4 } from "path";
|
|
573
|
+
|
|
574
|
+
// src/pool.ts
|
|
575
|
+
async function mapPool(items, limit, fn) {
|
|
576
|
+
let next = 0;
|
|
577
|
+
let failed = false;
|
|
578
|
+
const worker = async () => {
|
|
579
|
+
while (!failed) {
|
|
580
|
+
const idx = next++;
|
|
581
|
+
if (idx >= items.length) break;
|
|
582
|
+
const item = items[idx];
|
|
583
|
+
try {
|
|
584
|
+
await fn(item);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
failed = true;
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const n = Math.max(1, Math.min(limit, items.length));
|
|
592
|
+
await Promise.all(Array.from({ length: n }, worker));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/init.ts
|
|
540
596
|
function sha256OfBuffer(bytes) {
|
|
541
597
|
return `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
|
|
542
598
|
}
|
|
599
|
+
var UPLOAD_CONCURRENCY = 8;
|
|
543
600
|
var AlreadyInitializedError = class extends Error {
|
|
544
601
|
constructor(path) {
|
|
545
602
|
super(`already initialized \u2014 ${path} exists`);
|
|
@@ -584,27 +641,27 @@ async function init(opts) {
|
|
|
584
641
|
const mutatedDuringScan = /* @__PURE__ */ new Set();
|
|
585
642
|
let uploaded = 0;
|
|
586
643
|
let reused = 0;
|
|
587
|
-
|
|
588
|
-
const prep = await client.prepareUpload({ hash, size: f.size });
|
|
644
|
+
await mapPool([...uniqueHashes.values()], UPLOAD_CONCURRENCY, async (f) => {
|
|
645
|
+
const prep = await client.prepareUpload({ hash: f.hash, size: f.size });
|
|
589
646
|
if (prep.exists) {
|
|
590
647
|
reused += 1;
|
|
591
|
-
|
|
648
|
+
return;
|
|
592
649
|
}
|
|
593
650
|
const bytes = readFileSync3(join4(opts.rootDir, f.path));
|
|
594
651
|
const actual = sha256OfBuffer(bytes);
|
|
595
|
-
if (actual !== hash) {
|
|
652
|
+
if (actual !== f.hash) {
|
|
596
653
|
mutatedDuringScan.add(f.path);
|
|
597
654
|
scan.warnings.push({
|
|
598
655
|
path: f.path,
|
|
599
656
|
reason: "invalid_path"
|
|
600
657
|
// closest existing warning kind; semantics are file-mutated-mid-scan
|
|
601
658
|
});
|
|
602
|
-
|
|
659
|
+
return;
|
|
603
660
|
}
|
|
604
661
|
await client.putObjectBytes(prep.uploadUrl, bytes);
|
|
605
|
-
await client.completeUpload({ hash, size: f.size });
|
|
662
|
+
await client.completeUpload({ hash: f.hash, size: f.size });
|
|
606
663
|
uploaded += 1;
|
|
607
|
-
}
|
|
664
|
+
});
|
|
608
665
|
const events = [
|
|
609
666
|
...hashed.filter((f) => !mutatedDuringScan.has(f.path)).map((f) => ({
|
|
610
667
|
op: "write",
|
|
@@ -756,20 +813,347 @@ async function join5(opts) {
|
|
|
756
813
|
};
|
|
757
814
|
}
|
|
758
815
|
|
|
759
|
-
// src/
|
|
760
|
-
import { createHash as createHash3, randomBytes } from "crypto";
|
|
816
|
+
// src/imports.ts
|
|
761
817
|
import {
|
|
762
818
|
existsSync as existsSync3,
|
|
763
|
-
lstatSync,
|
|
764
819
|
mkdirSync as mkdirSync3,
|
|
765
820
|
readdirSync as readdirSync2,
|
|
766
821
|
readFileSync as readFileSync4,
|
|
822
|
+
realpathSync,
|
|
823
|
+
statSync as statSync2,
|
|
824
|
+
writeFileSync as writeFileSync2
|
|
825
|
+
} from "fs";
|
|
826
|
+
import { dirname as dirname3, isAbsolute, join as join6, relative } from "path";
|
|
827
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
828
|
+
function validateImportName(name) {
|
|
829
|
+
if (!name) return "empty_name";
|
|
830
|
+
if (!NAME_RE.test(name)) {
|
|
831
|
+
return "invalid_name (must match ^[a-z0-9][a-z0-9_-]*$ \u2014 lowercase slug, no dots/slashes/..)";
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
function normalizeImportPath(p) {
|
|
836
|
+
return p.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
837
|
+
}
|
|
838
|
+
function validateImportPath(rootDir, importPath) {
|
|
839
|
+
if (!importPath) return "empty_path";
|
|
840
|
+
if (isAbsolute(importPath)) return "absolute_path";
|
|
841
|
+
const norm = normalizeImportPath(importPath);
|
|
842
|
+
if (norm === "" || norm === ".") return "root_path (cannot mount over the rig root)";
|
|
843
|
+
const segs = norm.split("/");
|
|
844
|
+
if (segs.some((s) => s === "..")) return "dotdot_in_path";
|
|
845
|
+
if (segs.some((s) => s === "")) return "empty_segment";
|
|
846
|
+
if (segs[0] === ".rig") return "reserved_rig_path";
|
|
847
|
+
const realRoot = realpathSync(rootDir);
|
|
848
|
+
const abs = join6(realRoot, norm);
|
|
849
|
+
if (!isWithin(realRoot, abs)) return "escapes_root";
|
|
850
|
+
const realMount = realpathNearestExisting(abs);
|
|
851
|
+
if (!isWithin(realRoot, realMount)) return "symlink_escape";
|
|
852
|
+
const fileAncestor = firstExistingFileOnPath(abs);
|
|
853
|
+
if (fileAncestor) {
|
|
854
|
+
return fileAncestor === abs ? "mount_path_is_file" : "ancestor_is_file";
|
|
855
|
+
}
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
function validateImports(opts) {
|
|
859
|
+
const valid = [];
|
|
860
|
+
const errors = [];
|
|
861
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
862
|
+
const acceptedPaths = [];
|
|
863
|
+
for (const imp of opts.imports) {
|
|
864
|
+
const reject = (reason) => errors.push({ name: imp.name, path: imp.path, reason });
|
|
865
|
+
const nameErr = validateImportName(imp.name);
|
|
866
|
+
if (nameErr) {
|
|
867
|
+
reject(nameErr);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (seenNames.has(imp.name)) {
|
|
871
|
+
reject("duplicate_name");
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const pathErr = validateImportPath(opts.rootDir, imp.path);
|
|
875
|
+
if (pathErr) {
|
|
876
|
+
reject(pathErr);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const norm = normalizeImportPath(imp.path);
|
|
880
|
+
if (acceptedPaths.some((p) => pathsOverlap(p, norm))) {
|
|
881
|
+
reject("overlaps_other_import");
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (opts.primaryTrackedPaths.some((tp) => tp === norm || tp.startsWith(norm + "/"))) {
|
|
885
|
+
reject("over_primary_content");
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
seenNames.add(imp.name);
|
|
889
|
+
acceptedPaths.push(norm);
|
|
890
|
+
valid.push({ ...imp, path: norm });
|
|
891
|
+
}
|
|
892
|
+
return { valid, errors };
|
|
893
|
+
}
|
|
894
|
+
function primaryStateDbPath(rootDir) {
|
|
895
|
+
return join6(rootDir, ".rig", "tap", "state.local.db");
|
|
896
|
+
}
|
|
897
|
+
function importStateDbPath(rootDir, name) {
|
|
898
|
+
return join6(rootDir, ".rig", "tap", "imports", name, "state.local.db");
|
|
899
|
+
}
|
|
900
|
+
function importsDir(rootDir) {
|
|
901
|
+
return join6(rootDir, ".rig", "tap", "imports");
|
|
902
|
+
}
|
|
903
|
+
function importMountFile(rootDir, name) {
|
|
904
|
+
return join6(importsDir(rootDir), `${name}.local.json`);
|
|
905
|
+
}
|
|
906
|
+
var MOUNT_FILE_SUFFIX = ".local.json";
|
|
907
|
+
function writeImportMount(rootDir, name, mount) {
|
|
908
|
+
const path = importMountFile(rootDir, name);
|
|
909
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
910
|
+
writeFileSync2(path, JSON.stringify(mount, null, 2) + "\n", { mode: 384 });
|
|
911
|
+
}
|
|
912
|
+
function readImportMount(rootDir, name) {
|
|
913
|
+
const file = importMountFile(rootDir, name);
|
|
914
|
+
if (!existsSync3(file)) return { ok: false, reason: "missing_token_file" };
|
|
915
|
+
let parsed;
|
|
916
|
+
try {
|
|
917
|
+
parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
918
|
+
} catch {
|
|
919
|
+
return { ok: false, reason: "malformed_json" };
|
|
920
|
+
}
|
|
921
|
+
if (typeof parsed.bindingId !== "string" || parsed.bindingId === "") {
|
|
922
|
+
return { ok: false, reason: "missing_bindingId" };
|
|
923
|
+
}
|
|
924
|
+
if (typeof parsed.relayUrl !== "string" || parsed.relayUrl === "") {
|
|
925
|
+
return { ok: false, reason: "missing_relayUrl" };
|
|
926
|
+
}
|
|
927
|
+
if (typeof parsed.token !== "string" || parsed.token === "") {
|
|
928
|
+
return { ok: false, reason: "missing_token" };
|
|
929
|
+
}
|
|
930
|
+
if (typeof parsed.path !== "string" || parsed.path === "") {
|
|
931
|
+
return { ok: false, reason: "missing_path" };
|
|
932
|
+
}
|
|
933
|
+
const ops = Array.isArray(parsed.ops) ? parsed.ops : [];
|
|
934
|
+
if (!ops.includes("read") || !ops.includes("subscribe")) {
|
|
935
|
+
return { ok: false, reason: "insufficient_ops (need read+subscribe)" };
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
ok: true,
|
|
939
|
+
spec: {
|
|
940
|
+
name,
|
|
941
|
+
bindingId: parsed.bindingId,
|
|
942
|
+
relayUrl: parsed.relayUrl,
|
|
943
|
+
token: parsed.token,
|
|
944
|
+
path: parsed.path
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function loadImportMounts(rootDir) {
|
|
949
|
+
const dir = importsDir(rootDir);
|
|
950
|
+
const ready = [];
|
|
951
|
+
const broken = [];
|
|
952
|
+
if (!existsSync3(dir)) return { ready, broken };
|
|
953
|
+
for (const ent of readdirSync2(dir, { withFileTypes: true })) {
|
|
954
|
+
if (!ent.isFile() || !ent.name.endsWith(MOUNT_FILE_SUFFIX)) continue;
|
|
955
|
+
const name = ent.name.slice(0, -MOUNT_FILE_SUFFIX.length);
|
|
956
|
+
const nameErr = validateImportName(name);
|
|
957
|
+
if (nameErr) {
|
|
958
|
+
broken.push({ name, path: "", reason: nameErr });
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const res = readImportMount(rootDir, name);
|
|
962
|
+
if (!res.ok) {
|
|
963
|
+
broken.push({ name, path: "", reason: res.reason });
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
ready.push(res.spec);
|
|
967
|
+
}
|
|
968
|
+
return { ready, broken };
|
|
969
|
+
}
|
|
970
|
+
function isWithin(parent, child) {
|
|
971
|
+
const rel = relative(parent, child);
|
|
972
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
973
|
+
}
|
|
974
|
+
function realpathNearestExisting(abs) {
|
|
975
|
+
let cur = abs;
|
|
976
|
+
while (!existsSync3(cur)) {
|
|
977
|
+
const parent = dirname3(cur);
|
|
978
|
+
if (parent === cur) break;
|
|
979
|
+
cur = parent;
|
|
980
|
+
}
|
|
981
|
+
const realExisting = realpathSync(cur);
|
|
982
|
+
const remainder = relative(cur, abs);
|
|
983
|
+
return remainder ? join6(realExisting, remainder) : realExisting;
|
|
984
|
+
}
|
|
985
|
+
function firstExistingFileOnPath(abs) {
|
|
986
|
+
let cur = abs;
|
|
987
|
+
for (; ; ) {
|
|
988
|
+
if (existsSync3(cur)) {
|
|
989
|
+
return statSync2(cur).isFile() ? cur : null;
|
|
990
|
+
}
|
|
991
|
+
const parent = dirname3(cur);
|
|
992
|
+
if (parent === cur) return null;
|
|
993
|
+
cur = parent;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function pathsOverlap(a, b) {
|
|
997
|
+
return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/import-invite.ts
|
|
1001
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync } from "fs";
|
|
1002
|
+
import { dirname as dirname4, join as joinPath2 } from "path";
|
|
1003
|
+
var ImportValidationFailed = class extends Error {
|
|
1004
|
+
constructor(reason) {
|
|
1005
|
+
super(`import rejected: ${reason}`);
|
|
1006
|
+
this.reason = reason;
|
|
1007
|
+
this.name = "ImportValidationFailed";
|
|
1008
|
+
}
|
|
1009
|
+
reason;
|
|
1010
|
+
};
|
|
1011
|
+
var ImportTokenScopeError = class extends Error {
|
|
1012
|
+
constructor(ops) {
|
|
1013
|
+
super(
|
|
1014
|
+
ops ? `invite token has ops [${ops.join(", ")}] \u2014 a mirror needs read+subscribe` : `invite can't drive a mirror \u2014 it needs read+subscribe`
|
|
1015
|
+
);
|
|
1016
|
+
this.ops = ops;
|
|
1017
|
+
this.name = "ImportTokenScopeError";
|
|
1018
|
+
}
|
|
1019
|
+
ops;
|
|
1020
|
+
};
|
|
1021
|
+
function isRecord(v) {
|
|
1022
|
+
return typeof v === "object" && v !== null;
|
|
1023
|
+
}
|
|
1024
|
+
function deriveName(canonicalPath) {
|
|
1025
|
+
const segs = canonicalPath.split("/").filter(Boolean);
|
|
1026
|
+
return segs[segs.length - 1] ?? "";
|
|
1027
|
+
}
|
|
1028
|
+
function readPrimaryTrackedPaths(rootDir) {
|
|
1029
|
+
const dbPath = primaryStateDbPath(rootDir);
|
|
1030
|
+
if (!existsSync4(dbPath)) return [];
|
|
1031
|
+
const db = openStateDb(dbPath);
|
|
1032
|
+
try {
|
|
1033
|
+
return db.listPaths();
|
|
1034
|
+
} finally {
|
|
1035
|
+
db.close();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function addImport(opts) {
|
|
1039
|
+
const path = normalizeImportPath(opts.path);
|
|
1040
|
+
const name = opts.name && opts.name.length > 0 ? opts.name : deriveName(path);
|
|
1041
|
+
const existing = loadImportMounts(opts.rootDir).ready;
|
|
1042
|
+
const candidate = { name, bindingId: "", relayUrl: "", token: "", path };
|
|
1043
|
+
const { valid, errors } = validateImports({
|
|
1044
|
+
rootDir: opts.rootDir,
|
|
1045
|
+
imports: [...existing, candidate],
|
|
1046
|
+
primaryTrackedPaths: readPrimaryTrackedPaths(opts.rootDir)
|
|
1047
|
+
});
|
|
1048
|
+
if (!valid.some((v) => v.name === name && v.path === path)) {
|
|
1049
|
+
const err = errors.find((e) => e.name === name);
|
|
1050
|
+
throw new ImportValidationFailed(err?.reason ?? "invalid_import");
|
|
1051
|
+
}
|
|
1052
|
+
const primaryBindingId = readBindingConfig(opts.rootDir)?.bindingId;
|
|
1053
|
+
const { baseUrl, secret } = parseInviteUrl(opts.inviteUrl);
|
|
1054
|
+
let accepted;
|
|
1055
|
+
try {
|
|
1056
|
+
accepted = await acceptInvite({
|
|
1057
|
+
baseUrl,
|
|
1058
|
+
secret,
|
|
1059
|
+
accessToken: opts.accessToken ?? null,
|
|
1060
|
+
request: {
|
|
1061
|
+
...opts.deviceLabel ? { deviceLabel: opts.deviceLabel } : {},
|
|
1062
|
+
importer: { ...primaryBindingId ? { bindingId: primaryBindingId } : {}, path }
|
|
1063
|
+
},
|
|
1064
|
+
fetch: opts.fetch
|
|
1065
|
+
});
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
if (err instanceof RelayError && isRecord(err.body) && err.body.reason === "import_requires_read_subscribe") {
|
|
1068
|
+
throw new ImportTokenScopeError();
|
|
1069
|
+
}
|
|
1070
|
+
throw err;
|
|
1071
|
+
}
|
|
1072
|
+
const ops = accepted.token.ops;
|
|
1073
|
+
if (!ops.includes("read") || !ops.includes("subscribe")) {
|
|
1074
|
+
throw new ImportTokenScopeError(ops);
|
|
1075
|
+
}
|
|
1076
|
+
writeImportMount(opts.rootDir, name, {
|
|
1077
|
+
bindingId: accepted.bindingId,
|
|
1078
|
+
relayUrl: baseUrl,
|
|
1079
|
+
token: accepted.token.secret,
|
|
1080
|
+
ops,
|
|
1081
|
+
path,
|
|
1082
|
+
...accepted.importEdgeId ? { edgeId: accepted.importEdgeId } : {}
|
|
1083
|
+
});
|
|
1084
|
+
return {
|
|
1085
|
+
name,
|
|
1086
|
+
path,
|
|
1087
|
+
bindingId: accepted.bindingId,
|
|
1088
|
+
ops,
|
|
1089
|
+
...accepted.importEdgeId ? { edgeId: accepted.importEdgeId } : {},
|
|
1090
|
+
// Only meaningful when we actually sent a binding to verify.
|
|
1091
|
+
...primaryBindingId !== void 0 ? { importerBindingVerified: accepted.importerBindingVerified ?? false } : {}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function readRawMount(rootDir, name) {
|
|
1095
|
+
const file = importMountFile(rootDir, name);
|
|
1096
|
+
if (!existsSync4(file)) return null;
|
|
1097
|
+
try {
|
|
1098
|
+
return JSON.parse(readFileSync5(file, "utf8"));
|
|
1099
|
+
} catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function removeImport(opts) {
|
|
1104
|
+
const mountPath = importMountFile(opts.rootDir, opts.name);
|
|
1105
|
+
const res = readImportMount(opts.rootDir, opts.name);
|
|
1106
|
+
const raw = readRawMount(opts.rootDir, opts.name);
|
|
1107
|
+
const path = res.ok ? res.spec.path : raw?.path ?? null;
|
|
1108
|
+
let revoke2;
|
|
1109
|
+
if (!raw?.edgeId || !raw.relayUrl) {
|
|
1110
|
+
revoke2 = "no-edge";
|
|
1111
|
+
} else if (!opts.accessToken) {
|
|
1112
|
+
revoke2 = "no-token";
|
|
1113
|
+
} else {
|
|
1114
|
+
try {
|
|
1115
|
+
const r = await revokeImportEdge({
|
|
1116
|
+
baseUrl: raw.relayUrl,
|
|
1117
|
+
edgeId: raw.edgeId,
|
|
1118
|
+
accessToken: opts.accessToken,
|
|
1119
|
+
fetch: opts.fetch
|
|
1120
|
+
});
|
|
1121
|
+
revoke2 = r.revoked ? "revoked" : r.status === 403 ? "not-creator" : r.status === 404 ? "gone" : "offline";
|
|
1122
|
+
} catch {
|
|
1123
|
+
revoke2 = "offline";
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
let removed = false;
|
|
1127
|
+
if (existsSync4(mountPath)) {
|
|
1128
|
+
rmSync(mountPath, { force: true });
|
|
1129
|
+
removed = true;
|
|
1130
|
+
}
|
|
1131
|
+
rmSync(dirname4(importStateDbPath(opts.rootDir, opts.name)), {
|
|
1132
|
+
recursive: true,
|
|
1133
|
+
force: true
|
|
1134
|
+
});
|
|
1135
|
+
let purged = false;
|
|
1136
|
+
if (opts.purge && path) {
|
|
1137
|
+
rmSync(joinPath2(opts.rootDir, path), { recursive: true, force: true });
|
|
1138
|
+
purged = true;
|
|
1139
|
+
}
|
|
1140
|
+
return { removed, path, purged, revoke: revoke2 };
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/applier.ts
|
|
1144
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
1145
|
+
import {
|
|
1146
|
+
existsSync as existsSync5,
|
|
1147
|
+
lstatSync,
|
|
1148
|
+
mkdirSync as mkdirSync4,
|
|
1149
|
+
readdirSync as readdirSync3,
|
|
1150
|
+
readFileSync as readFileSync6,
|
|
767
1151
|
renameSync,
|
|
768
1152
|
rmdirSync,
|
|
769
|
-
rmSync,
|
|
770
|
-
writeFileSync as
|
|
1153
|
+
rmSync as rmSync2,
|
|
1154
|
+
writeFileSync as writeFileSync3
|
|
771
1155
|
} from "fs";
|
|
772
|
-
import { basename, dirname as
|
|
1156
|
+
import { basename, dirname as dirname5, extname, join as join7, sep as sep2 } from "path";
|
|
773
1157
|
var STATE_KEY_CONFLICT_COUNT = "conflict_count";
|
|
774
1158
|
var HashMismatchError = class extends Error {
|
|
775
1159
|
constructor(path, expectedHash, actualHash) {
|
|
@@ -789,10 +1173,10 @@ function sha256Hex(bytes) {
|
|
|
789
1173
|
return createHash3("sha256").update(bytes).digest("hex");
|
|
790
1174
|
}
|
|
791
1175
|
function sha256OfFile(absPath) {
|
|
792
|
-
return `sha256:${sha256Hex(
|
|
1176
|
+
return `sha256:${sha256Hex(readFileSync6(absPath))}`;
|
|
793
1177
|
}
|
|
794
1178
|
function conflictSidecarPath(path, ev) {
|
|
795
|
-
const dir =
|
|
1179
|
+
const dir = dirname5(path);
|
|
796
1180
|
const ext = extname(path);
|
|
797
1181
|
const stem = basename(path, ext);
|
|
798
1182
|
const actor = ev.actorDeviceId ?? "unknown";
|
|
@@ -811,16 +1195,16 @@ function findConflictSidecars(rootDir) {
|
|
|
811
1195
|
walk2(".");
|
|
812
1196
|
return out;
|
|
813
1197
|
function walk2(rel) {
|
|
814
|
-
const abs = rel === "." ? rootDir :
|
|
1198
|
+
const abs = rel === "." ? rootDir : join7(rootDir, rel);
|
|
815
1199
|
let entries;
|
|
816
1200
|
try {
|
|
817
|
-
entries =
|
|
1201
|
+
entries = readdirSync3(abs, { withFileTypes: true });
|
|
818
1202
|
} catch {
|
|
819
1203
|
return;
|
|
820
1204
|
}
|
|
821
1205
|
for (const ent of entries) {
|
|
822
1206
|
if (ent.isSymbolicLink()) continue;
|
|
823
|
-
const childRel = rel === "." ? ent.name :
|
|
1207
|
+
const childRel = rel === "." ? ent.name : join7(rel, ent.name);
|
|
824
1208
|
if (ent.isDirectory()) {
|
|
825
1209
|
if (SCAN_SKIP_DIRS.has(ent.name)) continue;
|
|
826
1210
|
walk2(childRel);
|
|
@@ -830,7 +1214,7 @@ function findConflictSidecars(rootDir) {
|
|
|
830
1214
|
const parsed = parseConflictSidecar(ent.name);
|
|
831
1215
|
if (!parsed) continue;
|
|
832
1216
|
const sidecarPosix = sep2 === "/" ? childRel : childRel.replaceAll(sep2, "/");
|
|
833
|
-
const dir =
|
|
1217
|
+
const dir = dirname5(sidecarPosix);
|
|
834
1218
|
const original = dir === "." ? `${parsed.stem}${parsed.ext}` : `${dir}/${parsed.stem}${parsed.ext}`;
|
|
835
1219
|
out.push({ path: original, sidecarPath: sidecarPosix });
|
|
836
1220
|
}
|
|
@@ -846,6 +1230,7 @@ function bumpConflictCount(stateDb) {
|
|
|
846
1230
|
var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
|
|
847
1231
|
var STATE_KEY_LAST_APPLY_ERROR_AT = "last_apply_error_at";
|
|
848
1232
|
async function applyOnce(opts) {
|
|
1233
|
+
const mode = opts.mode ?? "primary";
|
|
849
1234
|
const cursor = opts.stateDb.getCursor();
|
|
850
1235
|
const since = await opts.client.listChanges({ after: cursor, limit: opts.limit });
|
|
851
1236
|
let applied = 0;
|
|
@@ -858,7 +1243,8 @@ async function applyOnce(opts) {
|
|
|
858
1243
|
rootDir: opts.rootDir,
|
|
859
1244
|
client: opts.client,
|
|
860
1245
|
stateDb: opts.stateDb,
|
|
861
|
-
event: ev
|
|
1246
|
+
event: ev,
|
|
1247
|
+
mode
|
|
862
1248
|
});
|
|
863
1249
|
if (wasEcho) echoSkipped += 1;
|
|
864
1250
|
else applied += 1;
|
|
@@ -883,13 +1269,14 @@ async function applyOnce(opts) {
|
|
|
883
1269
|
return { applied, echoSkipped, errored, events: since.events, errors };
|
|
884
1270
|
}
|
|
885
1271
|
async function applyEvent(opts) {
|
|
886
|
-
const { rootDir, client, stateDb, event: ev } = opts;
|
|
887
|
-
const
|
|
1272
|
+
const { rootDir, client, stateDb, event: ev, mode } = opts;
|
|
1273
|
+
const mirror = mode === "mirror";
|
|
1274
|
+
const abs = join7(rootDir, ev.path);
|
|
888
1275
|
switch (ev.op) {
|
|
889
1276
|
case "write": {
|
|
890
1277
|
if (!ev.hash) throw new Error(`write event ${ev.id} missing hash`);
|
|
891
1278
|
const local = stateDb.getPath(ev.path);
|
|
892
|
-
if (local?.lastSeenHash === ev.hash &&
|
|
1279
|
+
if (local?.lastSeenHash === ev.hash && existsSync5(abs)) {
|
|
893
1280
|
stateDb.upsertPath({
|
|
894
1281
|
path: ev.path,
|
|
895
1282
|
lastAppliedChangeId: ev.id,
|
|
@@ -899,12 +1286,15 @@ async function applyEvent(opts) {
|
|
|
899
1286
|
});
|
|
900
1287
|
return true;
|
|
901
1288
|
}
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1289
|
+
if (existsSync5(abs) && lstatSync(abs).isDirectory()) {
|
|
1290
|
+
if (!mirror) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`file_vs_dir_collision: ${ev.path} is a local directory; refusing to overwrite with a file`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
906
1296
|
}
|
|
907
|
-
const localDirty =
|
|
1297
|
+
const localDirty = !mirror && existsSync5(abs) && local?.lastSeenHash != null && sha256OfFile(abs) !== local.lastSeenHash;
|
|
908
1298
|
const dl = await client.downloadUrl(ev.hash);
|
|
909
1299
|
const bytes = await client.getObjectBytes(dl.downloadUrl);
|
|
910
1300
|
const actualHash = `sha256:${sha256Hex(bytes)}`;
|
|
@@ -913,10 +1303,10 @@ async function applyEvent(opts) {
|
|
|
913
1303
|
}
|
|
914
1304
|
if (localDirty) {
|
|
915
1305
|
const sidecarPath = conflictSidecarPath(ev.path, ev);
|
|
916
|
-
const sidecarAbs =
|
|
917
|
-
|
|
1306
|
+
const sidecarAbs = join7(rootDir, sidecarPath);
|
|
1307
|
+
mkdirSync4(dirname5(sidecarAbs), { recursive: true });
|
|
918
1308
|
const tmp2 = `${sidecarAbs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
919
|
-
|
|
1309
|
+
writeFileSync3(tmp2, bytes, { mode: ev.executable ? 493 : 420 });
|
|
920
1310
|
renameSync(tmp2, sidecarAbs);
|
|
921
1311
|
stateDb.upsertPath({
|
|
922
1312
|
path: ev.path,
|
|
@@ -928,9 +1318,9 @@ async function applyEvent(opts) {
|
|
|
928
1318
|
bumpConflictCount(stateDb);
|
|
929
1319
|
return false;
|
|
930
1320
|
}
|
|
931
|
-
|
|
1321
|
+
mkdirSync4(dirname5(abs), { recursive: true });
|
|
932
1322
|
const tmp = `${abs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
933
|
-
|
|
1323
|
+
writeFileSync3(tmp, bytes, { mode: ev.executable ? 493 : 420 });
|
|
934
1324
|
renameSync(tmp, abs);
|
|
935
1325
|
stateDb.upsertPath({
|
|
936
1326
|
path: ev.path,
|
|
@@ -942,36 +1332,43 @@ async function applyEvent(opts) {
|
|
|
942
1332
|
return false;
|
|
943
1333
|
}
|
|
944
1334
|
case "delete": {
|
|
945
|
-
if (
|
|
1335
|
+
if (existsSync5(abs)) {
|
|
946
1336
|
if (lstatSync(abs).isDirectory()) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const localUntracked = localStateRow?.lastSeenHash == null;
|
|
955
|
-
if (localDirty || localUntracked) {
|
|
956
|
-
stateDb.upsertPath({
|
|
957
|
-
path: ev.path,
|
|
958
|
-
lastAppliedChangeId: ev.id,
|
|
959
|
-
// null so scanAndPush's `prev?.lastSeenHash === hash` check
|
|
960
|
-
// falls through and the file gets re-submitted as a write.
|
|
961
|
-
lastSeenHash: null,
|
|
962
|
-
localDirty: true,
|
|
963
|
-
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
964
|
-
});
|
|
965
|
-
bumpConflictCount(stateDb);
|
|
1337
|
+
if (!mirror) {
|
|
1338
|
+
throw new Error(
|
|
1339
|
+
`delete_vs_dir: ${ev.path} is a local directory; refusing recursive removal`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
1343
|
+
stateDb.deletePath(ev.path);
|
|
966
1344
|
return false;
|
|
967
1345
|
}
|
|
968
|
-
|
|
1346
|
+
if (!mirror) {
|
|
1347
|
+
const localStateRow = stateDb.getPath(ev.path);
|
|
1348
|
+
const localBytesHash = sha256OfFile(abs);
|
|
1349
|
+
const localDirty = localStateRow?.lastSeenHash != null && localBytesHash !== localStateRow.lastSeenHash;
|
|
1350
|
+
const localUntracked = localStateRow?.lastSeenHash == null;
|
|
1351
|
+
if (localDirty || localUntracked) {
|
|
1352
|
+
stateDb.upsertPath({
|
|
1353
|
+
path: ev.path,
|
|
1354
|
+
lastAppliedChangeId: ev.id,
|
|
1355
|
+
// null so scanAndPush's `prev?.lastSeenHash === hash` check
|
|
1356
|
+
// falls through and the file gets re-submitted as a write.
|
|
1357
|
+
lastSeenHash: null,
|
|
1358
|
+
localDirty: true,
|
|
1359
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1360
|
+
});
|
|
1361
|
+
bumpConflictCount(stateDb);
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
rmSync2(abs, { force: true });
|
|
969
1366
|
}
|
|
970
1367
|
stateDb.deletePath(ev.path);
|
|
971
1368
|
return false;
|
|
972
1369
|
}
|
|
973
1370
|
case "mkdir": {
|
|
974
|
-
|
|
1371
|
+
mkdirSync4(abs, { recursive: true });
|
|
975
1372
|
stateDb.upsertPath({
|
|
976
1373
|
path: ev.path,
|
|
977
1374
|
lastAppliedChangeId: ev.id,
|
|
@@ -982,9 +1379,13 @@ async function applyEvent(opts) {
|
|
|
982
1379
|
return false;
|
|
983
1380
|
}
|
|
984
1381
|
case "rmdir": {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
1382
|
+
if (mirror) {
|
|
1383
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
1384
|
+
} else {
|
|
1385
|
+
try {
|
|
1386
|
+
rmdirSync(abs);
|
|
1387
|
+
} catch {
|
|
1388
|
+
}
|
|
988
1389
|
}
|
|
989
1390
|
stateDb.deletePath(ev.path);
|
|
990
1391
|
return false;
|
|
@@ -1130,19 +1531,120 @@ function asError(err, fallbackName) {
|
|
|
1130
1531
|
return e;
|
|
1131
1532
|
}
|
|
1132
1533
|
|
|
1534
|
+
// src/subscription.ts
|
|
1535
|
+
var DEFAULT_POLL_SECONDS = 3;
|
|
1536
|
+
var SSE_APPLY_DEBOUNCE_MS = 50;
|
|
1537
|
+
async function startSubscription(opts) {
|
|
1538
|
+
const { mode } = opts;
|
|
1539
|
+
const stateDb = openStateDb(opts.stateDbPath);
|
|
1540
|
+
const client = new RelayClient({
|
|
1541
|
+
baseUrl: opts.config.relayUrl,
|
|
1542
|
+
bindingId: opts.config.bindingId,
|
|
1543
|
+
token: opts.config.token,
|
|
1544
|
+
fetch: opts.fetch
|
|
1545
|
+
});
|
|
1546
|
+
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS;
|
|
1547
|
+
let stopped = false;
|
|
1548
|
+
let pollTimer;
|
|
1549
|
+
let sseApplyTimer;
|
|
1550
|
+
let sseSubscriber;
|
|
1551
|
+
let applyInFlight = null;
|
|
1552
|
+
let rerunRequested = false;
|
|
1553
|
+
let lastApplyResult;
|
|
1554
|
+
const triggerApply = () => {
|
|
1555
|
+
if (applyInFlight) {
|
|
1556
|
+
rerunRequested = true;
|
|
1557
|
+
return applyInFlight;
|
|
1558
|
+
}
|
|
1559
|
+
const loop = (async () => {
|
|
1560
|
+
try {
|
|
1561
|
+
do {
|
|
1562
|
+
rerunRequested = false;
|
|
1563
|
+
try {
|
|
1564
|
+
lastApplyResult = await applyOnce({
|
|
1565
|
+
rootDir: opts.rootDir,
|
|
1566
|
+
client,
|
|
1567
|
+
stateDb,
|
|
1568
|
+
mode
|
|
1569
|
+
});
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
console.error("tapd apply error:", err);
|
|
1572
|
+
}
|
|
1573
|
+
} while (rerunRequested && !stopped);
|
|
1574
|
+
return lastApplyResult;
|
|
1575
|
+
} finally {
|
|
1576
|
+
applyInFlight = null;
|
|
1577
|
+
}
|
|
1578
|
+
})();
|
|
1579
|
+
applyInFlight = loop;
|
|
1580
|
+
return loop;
|
|
1581
|
+
};
|
|
1582
|
+
const schedulePoll = () => {
|
|
1583
|
+
if (stopped) return;
|
|
1584
|
+
pollTimer = setTimeout(() => {
|
|
1585
|
+
triggerApply().finally(() => schedulePoll());
|
|
1586
|
+
}, pollSeconds * 1e3);
|
|
1587
|
+
};
|
|
1588
|
+
const scheduleSseApply = () => {
|
|
1589
|
+
if (stopped) return;
|
|
1590
|
+
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1591
|
+
sseApplyTimer = setTimeout(() => {
|
|
1592
|
+
sseApplyTimer = void 0;
|
|
1593
|
+
void triggerApply();
|
|
1594
|
+
}, SSE_APPLY_DEBOUNCE_MS);
|
|
1595
|
+
};
|
|
1596
|
+
if (!opts.noSse) {
|
|
1597
|
+
sseSubscriber = subscribe({
|
|
1598
|
+
baseUrl: opts.config.relayUrl,
|
|
1599
|
+
bindingId: opts.config.bindingId,
|
|
1600
|
+
token: opts.config.token,
|
|
1601
|
+
fetch: opts.fetch,
|
|
1602
|
+
heartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs,
|
|
1603
|
+
onChange: () => scheduleSseApply(),
|
|
1604
|
+
onError: (err) => {
|
|
1605
|
+
console.error("tapd sse:", err.message);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
await triggerApply();
|
|
1610
|
+
schedulePoll();
|
|
1611
|
+
return {
|
|
1612
|
+
async stop() {
|
|
1613
|
+
stopped = true;
|
|
1614
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1615
|
+
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1616
|
+
if (sseSubscriber) await sseSubscriber.stop();
|
|
1617
|
+
await Promise.allSettled([applyInFlight ?? Promise.resolve()]);
|
|
1618
|
+
stateDb.close();
|
|
1619
|
+
},
|
|
1620
|
+
async applyNow() {
|
|
1621
|
+
const result = await triggerApply();
|
|
1622
|
+
return result ?? {
|
|
1623
|
+
applied: 0,
|
|
1624
|
+
echoSkipped: 0,
|
|
1625
|
+
errored: 0,
|
|
1626
|
+
events: [],
|
|
1627
|
+
errors: []
|
|
1628
|
+
};
|
|
1629
|
+
},
|
|
1630
|
+
stateDb
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1133
1634
|
// src/uploader.ts
|
|
1134
1635
|
import { createHash as createHash4 } from "crypto";
|
|
1135
|
-
import { readFileSync as
|
|
1136
|
-
import { join as
|
|
1636
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1637
|
+
import { join as join8 } from "path";
|
|
1638
|
+
var UPLOAD_CONCURRENCY2 = 8;
|
|
1137
1639
|
function sha256OfBuffer2(bytes) {
|
|
1138
1640
|
return `sha256:${createHash4("sha256").update(bytes).digest("hex")}`;
|
|
1139
1641
|
}
|
|
1140
1642
|
async function computeLocalDiff(opts) {
|
|
1141
|
-
const { rootDir, stateDb, ignore: ignore2 } = opts;
|
|
1142
|
-
const scan = walk(rootDir, { ignore: ignore2 });
|
|
1643
|
+
const { rootDir, stateDb, ignore: ignore2, excludePaths } = opts;
|
|
1644
|
+
const scan = walk(rootDir, { ignore: ignore2, excludePaths });
|
|
1143
1645
|
const writes = [];
|
|
1144
1646
|
for (const f of scan.files) {
|
|
1145
|
-
const hash = await hashFile(
|
|
1647
|
+
const hash = await hashFile(join8(rootDir, f.path));
|
|
1146
1648
|
const prev = stateDb.getPath(f.path);
|
|
1147
1649
|
if (prev?.lastSeenHash === hash) continue;
|
|
1148
1650
|
const baseChangeId = prev?.lastAppliedChangeId ?? void 0;
|
|
@@ -1205,23 +1707,23 @@ async function scanAndPush(opts) {
|
|
|
1205
1707
|
const skipWarnings = [];
|
|
1206
1708
|
let uploaded = 0;
|
|
1207
1709
|
let reused = 0;
|
|
1208
|
-
|
|
1209
|
-
const prep = await client.prepareUpload({ hash, size: f.size });
|
|
1710
|
+
await mapPool([...uniqueHashes.values()], UPLOAD_CONCURRENCY2, async (f) => {
|
|
1711
|
+
const prep = await client.prepareUpload({ hash: f.hash, size: f.size });
|
|
1210
1712
|
if (prep.exists) {
|
|
1211
1713
|
reused += 1;
|
|
1212
|
-
|
|
1714
|
+
return;
|
|
1213
1715
|
}
|
|
1214
|
-
const bytes =
|
|
1716
|
+
const bytes = readFileSync7(join8(rootDir, f.path));
|
|
1215
1717
|
const actual = sha256OfBuffer2(bytes);
|
|
1216
|
-
if (actual !== hash) {
|
|
1718
|
+
if (actual !== f.hash) {
|
|
1217
1719
|
mutatedPaths.add(f.path);
|
|
1218
1720
|
skipWarnings.push({ path: f.path, reason: `mutated_during_scan:${actual}` });
|
|
1219
|
-
|
|
1721
|
+
return;
|
|
1220
1722
|
}
|
|
1221
1723
|
await client.putObjectBytes(prep.uploadUrl, bytes);
|
|
1222
|
-
await client.completeUpload({ hash, size: f.size });
|
|
1724
|
+
await client.completeUpload({ hash: f.hash, size: f.size });
|
|
1223
1725
|
uploaded += 1;
|
|
1224
|
-
}
|
|
1726
|
+
});
|
|
1225
1727
|
const events = [];
|
|
1226
1728
|
for (const w of diff.writes) {
|
|
1227
1729
|
if (mutatedPaths.has(w.path)) continue;
|
|
@@ -1291,152 +1793,183 @@ function cursorLessThan(a, b) {
|
|
|
1291
1793
|
}
|
|
1292
1794
|
|
|
1293
1795
|
// src/start.ts
|
|
1294
|
-
import { join as
|
|
1796
|
+
import { join as join9 } from "path";
|
|
1295
1797
|
import chokidar from "chokidar";
|
|
1296
|
-
var
|
|
1798
|
+
var DEFAULT_POLL_SECONDS2 = 3;
|
|
1297
1799
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1800
|
+
var NOOP_APPLY = {
|
|
1801
|
+
applied: 0,
|
|
1802
|
+
echoSkipped: 0,
|
|
1803
|
+
errored: 0,
|
|
1804
|
+
events: [],
|
|
1805
|
+
errors: []
|
|
1806
|
+
};
|
|
1807
|
+
var NOOP_SCAN = {
|
|
1808
|
+
uploaded: 0,
|
|
1809
|
+
reused: 0,
|
|
1810
|
+
submitted: [],
|
|
1811
|
+
warnings: []
|
|
1812
|
+
};
|
|
1298
1813
|
async function start(opts) {
|
|
1299
1814
|
const config = readBindingConfig(opts.rootDir);
|
|
1300
|
-
|
|
1815
|
+
let importSpecs;
|
|
1816
|
+
let loaderBroken = [];
|
|
1817
|
+
if (opts.imports) {
|
|
1818
|
+
importSpecs = opts.imports;
|
|
1819
|
+
} else {
|
|
1820
|
+
const loaded = loadImportMounts(opts.rootDir);
|
|
1821
|
+
importSpecs = loaded.ready;
|
|
1822
|
+
loaderBroken = loaded.broken;
|
|
1823
|
+
}
|
|
1824
|
+
for (const e of loaderBroken) {
|
|
1825
|
+
console.error(`tapd import "${e.name}": skipped \u2014 ${e.reason}`);
|
|
1826
|
+
}
|
|
1827
|
+
if (!config && importSpecs.length === 0) {
|
|
1301
1828
|
throw new Error(
|
|
1302
|
-
`no binding config at ${opts.rootDir}/.rig/tap-binding.local.json \u2014 run \`tapd init\` first`
|
|
1829
|
+
`no binding config at ${opts.rootDir}/.rig/tap-binding.local.json and no imports \u2014 run \`tapd init\` first`
|
|
1303
1830
|
);
|
|
1304
1831
|
}
|
|
1305
|
-
const
|
|
1306
|
-
join8(opts.rootDir, ".rig", "tap", "state.local.db")
|
|
1307
|
-
);
|
|
1308
|
-
const ignore2 = loadIgnore(opts.rootDir);
|
|
1309
|
-
const client = new RelayClient({
|
|
1310
|
-
baseUrl: config.relayUrl,
|
|
1311
|
-
bindingId: config.bindingId,
|
|
1312
|
-
token: config.token,
|
|
1313
|
-
fetch: opts.fetch
|
|
1314
|
-
});
|
|
1315
|
-
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS;
|
|
1832
|
+
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS2;
|
|
1316
1833
|
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1317
1834
|
let stopped = false;
|
|
1318
|
-
let
|
|
1319
|
-
let scanTimer;
|
|
1320
|
-
let sseApplyTimer;
|
|
1835
|
+
let primary;
|
|
1321
1836
|
let watcher;
|
|
1837
|
+
let scanTimer;
|
|
1322
1838
|
let scanInFlight = Promise.resolve();
|
|
1323
|
-
let
|
|
1324
|
-
|
|
1325
|
-
let
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
applyInFlight = loop;
|
|
1352
|
-
return loop;
|
|
1353
|
-
};
|
|
1354
|
-
const schedulePoll = () => {
|
|
1355
|
-
if (stopped) return;
|
|
1356
|
-
pollTimer = setTimeout(() => {
|
|
1357
|
-
triggerApply().finally(() => schedulePoll());
|
|
1358
|
-
}, pollSeconds * 1e3);
|
|
1359
|
-
};
|
|
1360
|
-
const scheduleScan = () => {
|
|
1361
|
-
if (stopped) return;
|
|
1362
|
-
if (scanTimer) clearTimeout(scanTimer);
|
|
1363
|
-
scanTimer = setTimeout(() => {
|
|
1364
|
-
scanInFlight = scanAndPush({ rootDir: opts.rootDir, client, stateDb, ignore: ignore2 }).catch((err) => {
|
|
1365
|
-
console.error("tapd scan error:", err);
|
|
1366
|
-
});
|
|
1367
|
-
}, debounceMs);
|
|
1368
|
-
};
|
|
1369
|
-
const SSE_APPLY_DEBOUNCE_MS = 50;
|
|
1370
|
-
const scheduleSseApply = () => {
|
|
1371
|
-
if (stopped) return;
|
|
1372
|
-
if (sseApplyTimer) clearTimeout(sseApplyTimer);
|
|
1373
|
-
sseApplyTimer = setTimeout(() => {
|
|
1374
|
-
sseApplyTimer = void 0;
|
|
1375
|
-
void triggerApply();
|
|
1376
|
-
}, SSE_APPLY_DEBOUNCE_MS);
|
|
1377
|
-
};
|
|
1378
|
-
if (!opts.noSse) {
|
|
1379
|
-
sseSubscriber = subscribe({
|
|
1839
|
+
let writerClient;
|
|
1840
|
+
const ignore2 = loadIgnore(opts.rootDir);
|
|
1841
|
+
let importPaths = [];
|
|
1842
|
+
if (config) {
|
|
1843
|
+
primary = await startSubscription({
|
|
1844
|
+
rootDir: opts.rootDir,
|
|
1845
|
+
config,
|
|
1846
|
+
stateDbPath: primaryStateDbPath(opts.rootDir),
|
|
1847
|
+
mode: "primary",
|
|
1848
|
+
fetch: opts.fetch,
|
|
1849
|
+
pollSeconds,
|
|
1850
|
+
noSse: opts.noSse,
|
|
1851
|
+
sseHeartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
const primaryTrackedPaths = primary ? primary.stateDb.listPaths() : [];
|
|
1855
|
+
const { valid: validImports, errors: importErrors } = validateImports({
|
|
1856
|
+
rootDir: opts.rootDir,
|
|
1857
|
+
imports: importSpecs,
|
|
1858
|
+
primaryTrackedPaths
|
|
1859
|
+
});
|
|
1860
|
+
for (const e of importErrors) {
|
|
1861
|
+
console.error(`tapd import "${e.name}" (${e.path}): skipped \u2014 ${e.reason}`);
|
|
1862
|
+
}
|
|
1863
|
+
importPaths = validImports.map((i) => i.path);
|
|
1864
|
+
if (config && primary) {
|
|
1865
|
+
const primaryStateDb = primary.stateDb;
|
|
1866
|
+
writerClient = new RelayClient({
|
|
1380
1867
|
baseUrl: config.relayUrl,
|
|
1381
1868
|
bindingId: config.bindingId,
|
|
1382
1869
|
token: config.token,
|
|
1383
|
-
fetch: opts.fetch
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1870
|
+
fetch: opts.fetch
|
|
1871
|
+
});
|
|
1872
|
+
const scheduleScan = () => {
|
|
1873
|
+
if (stopped) return;
|
|
1874
|
+
if (scanTimer) clearTimeout(scanTimer);
|
|
1875
|
+
scanTimer = setTimeout(() => {
|
|
1876
|
+
scanInFlight = scanAndPush({
|
|
1877
|
+
rootDir: opts.rootDir,
|
|
1878
|
+
client: writerClient,
|
|
1879
|
+
stateDb: primaryStateDb,
|
|
1880
|
+
ignore: ignore2,
|
|
1881
|
+
excludePaths: importPaths
|
|
1882
|
+
}).catch((err) => {
|
|
1883
|
+
console.error("tapd scan error:", err);
|
|
1884
|
+
});
|
|
1885
|
+
}, debounceMs);
|
|
1886
|
+
};
|
|
1887
|
+
if (!opts.noWatch) {
|
|
1888
|
+
watcher = chokidar.watch(opts.rootDir, {
|
|
1889
|
+
ignoreInitial: true,
|
|
1890
|
+
// Hard-exclude tap metadata AND every import mount point — the
|
|
1891
|
+
// latter so a mirrored subtree never triggers a re-upload into
|
|
1892
|
+
// the primary binding.
|
|
1893
|
+
ignored: (path) => {
|
|
1894
|
+
const rel = path.startsWith(opts.rootDir) ? path.slice(opts.rootDir.length).replace(/^[/\\]/, "") : path;
|
|
1895
|
+
if (isAlwaysExcluded(rel)) return true;
|
|
1896
|
+
if (importPaths.length > 0 && isUnderAny(rel, importPaths)) return true;
|
|
1897
|
+
return false;
|
|
1898
|
+
},
|
|
1899
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
1900
|
+
atomic: true
|
|
1901
|
+
});
|
|
1902
|
+
for (const ev of ["add", "change", "unlink", "addDir", "unlinkDir"]) {
|
|
1903
|
+
watcher.on(ev, () => scheduleScan());
|
|
1388
1904
|
}
|
|
1905
|
+
}
|
|
1906
|
+
scanInFlight = scanAndPush({
|
|
1907
|
+
rootDir: opts.rootDir,
|
|
1908
|
+
client: writerClient,
|
|
1909
|
+
stateDb: primaryStateDb,
|
|
1910
|
+
ignore: ignore2,
|
|
1911
|
+
excludePaths: importPaths
|
|
1912
|
+
}).catch((err) => {
|
|
1913
|
+
console.error("tapd initial scan error:", err);
|
|
1389
1914
|
});
|
|
1915
|
+
await scanInFlight;
|
|
1390
1916
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1917
|
+
const imports = [];
|
|
1918
|
+
const mirrorSubs = [];
|
|
1919
|
+
for (const imp of validImports) {
|
|
1920
|
+
try {
|
|
1921
|
+
const sub = await startSubscription({
|
|
1922
|
+
rootDir: join9(opts.rootDir, imp.path),
|
|
1923
|
+
config: { bindingId: imp.bindingId, relayUrl: imp.relayUrl, token: imp.token },
|
|
1924
|
+
stateDbPath: importStateDbPath(opts.rootDir, imp.name),
|
|
1925
|
+
mode: "mirror",
|
|
1926
|
+
fetch: opts.fetch,
|
|
1927
|
+
pollSeconds,
|
|
1928
|
+
noSse: opts.noSse,
|
|
1929
|
+
sseHeartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs
|
|
1930
|
+
});
|
|
1931
|
+
mirrorSubs.push(sub);
|
|
1932
|
+
imports.push({
|
|
1933
|
+
name: imp.name,
|
|
1934
|
+
path: imp.path,
|
|
1935
|
+
applyNow: () => sub.applyNow(),
|
|
1936
|
+
stateDb: sub.stateDb
|
|
1937
|
+
});
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1940
|
+
console.error(`tapd import "${imp.name}" (${imp.path}): failed to start \u2014 ${reason}`);
|
|
1407
1941
|
}
|
|
1408
1942
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
await scanInFlight;
|
|
1414
|
-
schedulePoll();
|
|
1943
|
+
const stateDb = primary?.stateDb ?? imports[0]?.stateDb;
|
|
1944
|
+
if (!stateDb) {
|
|
1945
|
+
throw new Error("tapd start: no primary binding and no import mounted successfully");
|
|
1946
|
+
}
|
|
1415
1947
|
return {
|
|
1416
1948
|
async stop() {
|
|
1417
1949
|
stopped = true;
|
|
1418
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
1419
1950
|
if (scanTimer) clearTimeout(scanTimer);
|
|
1420
|
-
|
|
1421
|
-
if (sseSubscriber) await sseSubscriber.stop();
|
|
1422
|
-
await Promise.allSettled([applyInFlight ?? Promise.resolve(), scanInFlight]);
|
|
1951
|
+
await Promise.allSettled([scanInFlight]);
|
|
1423
1952
|
if (watcher) await watcher.close();
|
|
1424
|
-
|
|
1953
|
+
const subs = [];
|
|
1954
|
+
if (primary) subs.push(primary.stop());
|
|
1955
|
+
for (const sub of mirrorSubs) subs.push(sub.stop());
|
|
1956
|
+
await Promise.allSettled(subs);
|
|
1425
1957
|
},
|
|
1426
1958
|
async applyNow() {
|
|
1427
|
-
|
|
1428
|
-
return result ?? {
|
|
1429
|
-
applied: 0,
|
|
1430
|
-
echoSkipped: 0,
|
|
1431
|
-
errored: 0,
|
|
1432
|
-
events: [],
|
|
1433
|
-
errors: []
|
|
1434
|
-
};
|
|
1959
|
+
return primary ? primary.applyNow() : NOOP_APPLY;
|
|
1435
1960
|
},
|
|
1436
1961
|
async scanNow() {
|
|
1437
|
-
|
|
1962
|
+
if (!config || !primary || !writerClient) return NOOP_SCAN;
|
|
1963
|
+
return scanAndPush({
|
|
1964
|
+
rootDir: opts.rootDir,
|
|
1965
|
+
client: writerClient,
|
|
1966
|
+
stateDb: primary.stateDb,
|
|
1967
|
+
ignore: ignore2,
|
|
1968
|
+
excludePaths: importPaths
|
|
1969
|
+
});
|
|
1438
1970
|
},
|
|
1439
|
-
stateDb
|
|
1971
|
+
stateDb,
|
|
1972
|
+
imports
|
|
1440
1973
|
};
|
|
1441
1974
|
}
|
|
1442
1975
|
|
|
@@ -1464,13 +1997,29 @@ export {
|
|
|
1464
1997
|
InvalidInviteUrlError,
|
|
1465
1998
|
parseInviteUrl,
|
|
1466
1999
|
join5 as join,
|
|
2000
|
+
validateImportName,
|
|
2001
|
+
normalizeImportPath,
|
|
2002
|
+
validateImportPath,
|
|
2003
|
+
validateImports,
|
|
2004
|
+
primaryStateDbPath,
|
|
2005
|
+
importStateDbPath,
|
|
2006
|
+
importsDir,
|
|
2007
|
+
importMountFile,
|
|
2008
|
+
writeImportMount,
|
|
2009
|
+
readImportMount,
|
|
2010
|
+
loadImportMounts,
|
|
2011
|
+
ImportValidationFailed,
|
|
2012
|
+
ImportTokenScopeError,
|
|
2013
|
+
addImport,
|
|
2014
|
+
removeImport,
|
|
1467
2015
|
STATE_KEY_CONFLICT_COUNT,
|
|
1468
2016
|
conflictSidecarPath,
|
|
1469
2017
|
findConflictSidecars,
|
|
1470
2018
|
applyOnce,
|
|
1471
2019
|
subscribe,
|
|
2020
|
+
startSubscription,
|
|
1472
2021
|
computeLocalDiff,
|
|
1473
2022
|
scanAndPush,
|
|
1474
2023
|
start
|
|
1475
2024
|
};
|
|
1476
|
-
//# sourceMappingURL=chunk-
|
|
2025
|
+
//# sourceMappingURL=chunk-JBX2ZRMD.js.map
|