@rigxyz/tapd 0.3.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 +236 -34
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-N3KRCOBG.js → chunk-YPZ2UNI5.js} +691 -175
- package/dist/chunk-YPZ2UNI5.js.map +1 -0
- package/dist/index.d.ts +310 -18
- package/dist/index.js +35 -3
- package/package.json +14 -23
- 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;
|
|
@@ -426,6 +437,15 @@ function isAlwaysExcluded(posixPath) {
|
|
|
426
437
|
}
|
|
427
438
|
return false;
|
|
428
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
|
+
}
|
|
429
449
|
|
|
430
450
|
// src/walker.ts
|
|
431
451
|
import { readdirSync, statSync } from "fs";
|
|
@@ -462,6 +482,7 @@ function walk(rootDir, opts) {
|
|
|
462
482
|
const emptyDirs = [];
|
|
463
483
|
const directoriesScanned = [];
|
|
464
484
|
const warnings = [];
|
|
485
|
+
const excludePaths = opts.excludePaths ?? [];
|
|
465
486
|
walkDir(".");
|
|
466
487
|
return { files, emptyDirs, directoriesScanned, warnings };
|
|
467
488
|
function walkDir(rel) {
|
|
@@ -479,6 +500,10 @@ function walk(rootDir, opts) {
|
|
|
479
500
|
const posix = posixOf(childRel);
|
|
480
501
|
const isDir = ent.isDirectory();
|
|
481
502
|
if (isAlwaysExcluded(posix)) continue;
|
|
503
|
+
if (excludePaths.length > 0 && isUnderAny(posix, excludePaths)) {
|
|
504
|
+
hasVisible = true;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
482
507
|
if (opts.ignore.ignores(posix + (isDir ? "/" : ""))) continue;
|
|
483
508
|
if (ent.isSymbolicLink()) {
|
|
484
509
|
warnings.push({ path: posix, reason: "symlink" });
|
|
@@ -756,20 +781,347 @@ async function join5(opts) {
|
|
|
756
781
|
};
|
|
757
782
|
}
|
|
758
783
|
|
|
759
|
-
// src/
|
|
760
|
-
import { createHash as createHash3, randomBytes } from "crypto";
|
|
784
|
+
// src/imports.ts
|
|
761
785
|
import {
|
|
762
786
|
existsSync as existsSync3,
|
|
763
|
-
lstatSync,
|
|
764
787
|
mkdirSync as mkdirSync3,
|
|
765
788
|
readdirSync as readdirSync2,
|
|
766
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,
|
|
767
1119
|
renameSync,
|
|
768
1120
|
rmdirSync,
|
|
769
|
-
rmSync,
|
|
770
|
-
writeFileSync as
|
|
1121
|
+
rmSync as rmSync2,
|
|
1122
|
+
writeFileSync as writeFileSync3
|
|
771
1123
|
} from "fs";
|
|
772
|
-
import { basename, dirname as
|
|
1124
|
+
import { basename, dirname as dirname5, extname, join as join7, sep as sep2 } from "path";
|
|
773
1125
|
var STATE_KEY_CONFLICT_COUNT = "conflict_count";
|
|
774
1126
|
var HashMismatchError = class extends Error {
|
|
775
1127
|
constructor(path, expectedHash, actualHash) {
|
|
@@ -789,10 +1141,10 @@ function sha256Hex(bytes) {
|
|
|
789
1141
|
return createHash3("sha256").update(bytes).digest("hex");
|
|
790
1142
|
}
|
|
791
1143
|
function sha256OfFile(absPath) {
|
|
792
|
-
return `sha256:${sha256Hex(
|
|
1144
|
+
return `sha256:${sha256Hex(readFileSync6(absPath))}`;
|
|
793
1145
|
}
|
|
794
1146
|
function conflictSidecarPath(path, ev) {
|
|
795
|
-
const dir =
|
|
1147
|
+
const dir = dirname5(path);
|
|
796
1148
|
const ext = extname(path);
|
|
797
1149
|
const stem = basename(path, ext);
|
|
798
1150
|
const actor = ev.actorDeviceId ?? "unknown";
|
|
@@ -811,16 +1163,16 @@ function findConflictSidecars(rootDir) {
|
|
|
811
1163
|
walk2(".");
|
|
812
1164
|
return out;
|
|
813
1165
|
function walk2(rel) {
|
|
814
|
-
const abs = rel === "." ? rootDir :
|
|
1166
|
+
const abs = rel === "." ? rootDir : join7(rootDir, rel);
|
|
815
1167
|
let entries;
|
|
816
1168
|
try {
|
|
817
|
-
entries =
|
|
1169
|
+
entries = readdirSync3(abs, { withFileTypes: true });
|
|
818
1170
|
} catch {
|
|
819
1171
|
return;
|
|
820
1172
|
}
|
|
821
1173
|
for (const ent of entries) {
|
|
822
1174
|
if (ent.isSymbolicLink()) continue;
|
|
823
|
-
const childRel = rel === "." ? ent.name :
|
|
1175
|
+
const childRel = rel === "." ? ent.name : join7(rel, ent.name);
|
|
824
1176
|
if (ent.isDirectory()) {
|
|
825
1177
|
if (SCAN_SKIP_DIRS.has(ent.name)) continue;
|
|
826
1178
|
walk2(childRel);
|
|
@@ -830,7 +1182,7 @@ function findConflictSidecars(rootDir) {
|
|
|
830
1182
|
const parsed = parseConflictSidecar(ent.name);
|
|
831
1183
|
if (!parsed) continue;
|
|
832
1184
|
const sidecarPosix = sep2 === "/" ? childRel : childRel.replaceAll(sep2, "/");
|
|
833
|
-
const dir =
|
|
1185
|
+
const dir = dirname5(sidecarPosix);
|
|
834
1186
|
const original = dir === "." ? `${parsed.stem}${parsed.ext}` : `${dir}/${parsed.stem}${parsed.ext}`;
|
|
835
1187
|
out.push({ path: original, sidecarPath: sidecarPosix });
|
|
836
1188
|
}
|
|
@@ -846,6 +1198,7 @@ function bumpConflictCount(stateDb) {
|
|
|
846
1198
|
var STATE_KEY_LAST_APPLY_ERROR = "last_apply_error";
|
|
847
1199
|
var STATE_KEY_LAST_APPLY_ERROR_AT = "last_apply_error_at";
|
|
848
1200
|
async function applyOnce(opts) {
|
|
1201
|
+
const mode = opts.mode ?? "primary";
|
|
849
1202
|
const cursor = opts.stateDb.getCursor();
|
|
850
1203
|
const since = await opts.client.listChanges({ after: cursor, limit: opts.limit });
|
|
851
1204
|
let applied = 0;
|
|
@@ -858,7 +1211,8 @@ async function applyOnce(opts) {
|
|
|
858
1211
|
rootDir: opts.rootDir,
|
|
859
1212
|
client: opts.client,
|
|
860
1213
|
stateDb: opts.stateDb,
|
|
861
|
-
event: ev
|
|
1214
|
+
event: ev,
|
|
1215
|
+
mode
|
|
862
1216
|
});
|
|
863
1217
|
if (wasEcho) echoSkipped += 1;
|
|
864
1218
|
else applied += 1;
|
|
@@ -883,13 +1237,14 @@ async function applyOnce(opts) {
|
|
|
883
1237
|
return { applied, echoSkipped, errored, events: since.events, errors };
|
|
884
1238
|
}
|
|
885
1239
|
async function applyEvent(opts) {
|
|
886
|
-
const { rootDir, client, stateDb, event: ev } = opts;
|
|
887
|
-
const
|
|
1240
|
+
const { rootDir, client, stateDb, event: ev, mode } = opts;
|
|
1241
|
+
const mirror = mode === "mirror";
|
|
1242
|
+
const abs = join7(rootDir, ev.path);
|
|
888
1243
|
switch (ev.op) {
|
|
889
1244
|
case "write": {
|
|
890
1245
|
if (!ev.hash) throw new Error(`write event ${ev.id} missing hash`);
|
|
891
1246
|
const local = stateDb.getPath(ev.path);
|
|
892
|
-
if (local?.lastSeenHash === ev.hash &&
|
|
1247
|
+
if (local?.lastSeenHash === ev.hash && existsSync5(abs)) {
|
|
893
1248
|
stateDb.upsertPath({
|
|
894
1249
|
path: ev.path,
|
|
895
1250
|
lastAppliedChangeId: ev.id,
|
|
@@ -899,12 +1254,15 @@ async function applyEvent(opts) {
|
|
|
899
1254
|
});
|
|
900
1255
|
return true;
|
|
901
1256
|
}
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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 });
|
|
906
1264
|
}
|
|
907
|
-
const localDirty =
|
|
1265
|
+
const localDirty = !mirror && existsSync5(abs) && local?.lastSeenHash != null && sha256OfFile(abs) !== local.lastSeenHash;
|
|
908
1266
|
const dl = await client.downloadUrl(ev.hash);
|
|
909
1267
|
const bytes = await client.getObjectBytes(dl.downloadUrl);
|
|
910
1268
|
const actualHash = `sha256:${sha256Hex(bytes)}`;
|
|
@@ -913,10 +1271,10 @@ async function applyEvent(opts) {
|
|
|
913
1271
|
}
|
|
914
1272
|
if (localDirty) {
|
|
915
1273
|
const sidecarPath = conflictSidecarPath(ev.path, ev);
|
|
916
|
-
const sidecarAbs =
|
|
917
|
-
|
|
1274
|
+
const sidecarAbs = join7(rootDir, sidecarPath);
|
|
1275
|
+
mkdirSync4(dirname5(sidecarAbs), { recursive: true });
|
|
918
1276
|
const tmp2 = `${sidecarAbs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
919
|
-
|
|
1277
|
+
writeFileSync3(tmp2, bytes, { mode: ev.executable ? 493 : 420 });
|
|
920
1278
|
renameSync(tmp2, sidecarAbs);
|
|
921
1279
|
stateDb.upsertPath({
|
|
922
1280
|
path: ev.path,
|
|
@@ -928,9 +1286,9 @@ async function applyEvent(opts) {
|
|
|
928
1286
|
bumpConflictCount(stateDb);
|
|
929
1287
|
return false;
|
|
930
1288
|
}
|
|
931
|
-
|
|
1289
|
+
mkdirSync4(dirname5(abs), { recursive: true });
|
|
932
1290
|
const tmp = `${abs}.tap-tmp.${randomBytes(6).toString("hex")}`;
|
|
933
|
-
|
|
1291
|
+
writeFileSync3(tmp, bytes, { mode: ev.executable ? 493 : 420 });
|
|
934
1292
|
renameSync(tmp, abs);
|
|
935
1293
|
stateDb.upsertPath({
|
|
936
1294
|
path: ev.path,
|
|
@@ -942,36 +1300,43 @@ async function applyEvent(opts) {
|
|
|
942
1300
|
return false;
|
|
943
1301
|
}
|
|
944
1302
|
case "delete": {
|
|
945
|
-
if (
|
|
1303
|
+
if (existsSync5(abs)) {
|
|
946
1304
|
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);
|
|
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);
|
|
966
1312
|
return false;
|
|
967
1313
|
}
|
|
968
|
-
|
|
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 });
|
|
969
1334
|
}
|
|
970
1335
|
stateDb.deletePath(ev.path);
|
|
971
1336
|
return false;
|
|
972
1337
|
}
|
|
973
1338
|
case "mkdir": {
|
|
974
|
-
|
|
1339
|
+
mkdirSync4(abs, { recursive: true });
|
|
975
1340
|
stateDb.upsertPath({
|
|
976
1341
|
path: ev.path,
|
|
977
1342
|
lastAppliedChangeId: ev.id,
|
|
@@ -982,9 +1347,13 @@ async function applyEvent(opts) {
|
|
|
982
1347
|
return false;
|
|
983
1348
|
}
|
|
984
1349
|
case "rmdir": {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
}
|
|
1350
|
+
if (mirror) {
|
|
1351
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
1352
|
+
} else {
|
|
1353
|
+
try {
|
|
1354
|
+
rmdirSync(abs);
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
988
1357
|
}
|
|
989
1358
|
stateDb.deletePath(ev.path);
|
|
990
1359
|
return false;
|
|
@@ -1130,19 +1499,119 @@ function asError(err, fallbackName) {
|
|
|
1130
1499
|
return e;
|
|
1131
1500
|
}
|
|
1132
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
|
+
|
|
1133
1602
|
// src/uploader.ts
|
|
1134
1603
|
import { createHash as createHash4 } from "crypto";
|
|
1135
|
-
import { readFileSync as
|
|
1136
|
-
import { join as
|
|
1604
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1605
|
+
import { join as join8 } from "path";
|
|
1137
1606
|
function sha256OfBuffer2(bytes) {
|
|
1138
1607
|
return `sha256:${createHash4("sha256").update(bytes).digest("hex")}`;
|
|
1139
1608
|
}
|
|
1140
1609
|
async function computeLocalDiff(opts) {
|
|
1141
|
-
const { rootDir, stateDb, ignore: ignore2 } = opts;
|
|
1142
|
-
const scan = walk(rootDir, { ignore: ignore2 });
|
|
1610
|
+
const { rootDir, stateDb, ignore: ignore2, excludePaths } = opts;
|
|
1611
|
+
const scan = walk(rootDir, { ignore: ignore2, excludePaths });
|
|
1143
1612
|
const writes = [];
|
|
1144
1613
|
for (const f of scan.files) {
|
|
1145
|
-
const hash = await hashFile(
|
|
1614
|
+
const hash = await hashFile(join8(rootDir, f.path));
|
|
1146
1615
|
const prev = stateDb.getPath(f.path);
|
|
1147
1616
|
if (prev?.lastSeenHash === hash) continue;
|
|
1148
1617
|
const baseChangeId = prev?.lastAppliedChangeId ?? void 0;
|
|
@@ -1211,7 +1680,7 @@ async function scanAndPush(opts) {
|
|
|
1211
1680
|
reused += 1;
|
|
1212
1681
|
continue;
|
|
1213
1682
|
}
|
|
1214
|
-
const bytes =
|
|
1683
|
+
const bytes = readFileSync7(join8(rootDir, f.path));
|
|
1215
1684
|
const actual = sha256OfBuffer2(bytes);
|
|
1216
1685
|
if (actual !== hash) {
|
|
1217
1686
|
mutatedPaths.add(f.path);
|
|
@@ -1291,152 +1760,183 @@ function cursorLessThan(a, b) {
|
|
|
1291
1760
|
}
|
|
1292
1761
|
|
|
1293
1762
|
// src/start.ts
|
|
1294
|
-
import { join as
|
|
1763
|
+
import { join as join9 } from "path";
|
|
1295
1764
|
import chokidar from "chokidar";
|
|
1296
|
-
var
|
|
1765
|
+
var DEFAULT_POLL_SECONDS2 = 3;
|
|
1297
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
|
+
};
|
|
1298
1780
|
async function start(opts) {
|
|
1299
1781
|
const config = readBindingConfig(opts.rootDir);
|
|
1300
|
-
|
|
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) {
|
|
1301
1795
|
throw new Error(
|
|
1302
|
-
`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`
|
|
1303
1797
|
);
|
|
1304
1798
|
}
|
|
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;
|
|
1799
|
+
const pollSeconds = opts.pollSeconds ?? DEFAULT_POLL_SECONDS2;
|
|
1316
1800
|
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1317
1801
|
let stopped = false;
|
|
1318
|
-
let
|
|
1319
|
-
let scanTimer;
|
|
1320
|
-
let sseApplyTimer;
|
|
1802
|
+
let primary;
|
|
1321
1803
|
let watcher;
|
|
1804
|
+
let scanTimer;
|
|
1322
1805
|
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({
|
|
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({
|
|
1380
1834
|
baseUrl: config.relayUrl,
|
|
1381
1835
|
bindingId: config.bindingId,
|
|
1382
1836
|
token: config.token,
|
|
1383
|
-
fetch: opts.fetch
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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());
|
|
1388
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);
|
|
1389
1881
|
});
|
|
1882
|
+
await scanInFlight;
|
|
1390
1883
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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}`);
|
|
1407
1908
|
}
|
|
1408
1909
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
await scanInFlight;
|
|
1414
|
-
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
|
+
}
|
|
1415
1914
|
return {
|
|
1416
1915
|
async stop() {
|
|
1417
1916
|
stopped = true;
|
|
1418
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
1419
1917
|
if (scanTimer) clearTimeout(scanTimer);
|
|
1420
|
-
|
|
1421
|
-
if (sseSubscriber) await sseSubscriber.stop();
|
|
1422
|
-
await Promise.allSettled([applyInFlight ?? Promise.resolve(), scanInFlight]);
|
|
1918
|
+
await Promise.allSettled([scanInFlight]);
|
|
1423
1919
|
if (watcher) await watcher.close();
|
|
1424
|
-
|
|
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);
|
|
1425
1924
|
},
|
|
1426
1925
|
async applyNow() {
|
|
1427
|
-
|
|
1428
|
-
return result ?? {
|
|
1429
|
-
applied: 0,
|
|
1430
|
-
echoSkipped: 0,
|
|
1431
|
-
errored: 0,
|
|
1432
|
-
events: [],
|
|
1433
|
-
errors: []
|
|
1434
|
-
};
|
|
1926
|
+
return primary ? primary.applyNow() : NOOP_APPLY;
|
|
1435
1927
|
},
|
|
1436
1928
|
async scanNow() {
|
|
1437
|
-
|
|
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
|
+
});
|
|
1438
1937
|
},
|
|
1439
|
-
stateDb
|
|
1938
|
+
stateDb,
|
|
1939
|
+
imports
|
|
1440
1940
|
};
|
|
1441
1941
|
}
|
|
1442
1942
|
|
|
@@ -1464,13 +1964,29 @@ export {
|
|
|
1464
1964
|
InvalidInviteUrlError,
|
|
1465
1965
|
parseInviteUrl,
|
|
1466
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,
|
|
1467
1982
|
STATE_KEY_CONFLICT_COUNT,
|
|
1468
1983
|
conflictSidecarPath,
|
|
1469
1984
|
findConflictSidecars,
|
|
1470
1985
|
applyOnce,
|
|
1471
1986
|
subscribe,
|
|
1987
|
+
startSubscription,
|
|
1472
1988
|
computeLocalDiff,
|
|
1473
1989
|
scanAndPush,
|
|
1474
1990
|
start
|
|
1475
1991
|
};
|
|
1476
|
-
//# sourceMappingURL=chunk-
|
|
1992
|
+
//# sourceMappingURL=chunk-YPZ2UNI5.js.map
|