@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.
@@ -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
- "content-type": "application/json",
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
- userId: opts.ownerUserId,
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
- userId: opts.userId,
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/applier.ts
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 writeFileSync2
1121
+ rmSync as rmSync2,
1122
+ writeFileSync as writeFileSync3
747
1123
  } from "fs";
748
- import { basename, dirname as dirname3, extname, join as join6, sep as sep2 } from "path";
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(readFileSync4(absPath))}`;
1144
+ return `sha256:${sha256Hex(readFileSync6(absPath))}`;
769
1145
  }
770
1146
  function conflictSidecarPath(path, ev) {
771
- const dir = dirname3(path);
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 : join6(rootDir, rel);
1166
+ const abs = rel === "." ? rootDir : join7(rootDir, rel);
791
1167
  let entries;
792
1168
  try {
793
- entries = readdirSync2(abs, { withFileTypes: true });
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 : join6(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 = dirname3(sidecarPosix);
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 abs = join6(rootDir, ev.path);
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 && existsSync3(abs)) {
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 (existsSync3(abs) && lstatSync(abs).isDirectory()) {
879
- throw new Error(
880
- `file_vs_dir_collision: ${ev.path} is a local directory; refusing to overwrite with a file`
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 = existsSync3(abs) && local?.lastSeenHash != null && sha256OfFile(abs) !== local.lastSeenHash;
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 = join6(rootDir, sidecarPath);
893
- mkdirSync3(dirname3(sidecarAbs), { recursive: true });
1274
+ const sidecarAbs = join7(rootDir, sidecarPath);
1275
+ mkdirSync4(dirname5(sidecarAbs), { recursive: true });
894
1276
  const tmp2 = `${sidecarAbs}.tap-tmp.${randomBytes(6).toString("hex")}`;
895
- writeFileSync2(tmp2, bytes, { mode: ev.executable ? 493 : 420 });
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
- mkdirSync3(dirname3(abs), { recursive: true });
1289
+ mkdirSync4(dirname5(abs), { recursive: true });
908
1290
  const tmp = `${abs}.tap-tmp.${randomBytes(6).toString("hex")}`;
909
- writeFileSync2(tmp, bytes, { mode: ev.executable ? 493 : 420 });
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 (existsSync3(abs)) {
1303
+ if (existsSync5(abs)) {
922
1304
  if (lstatSync(abs).isDirectory()) {
923
- throw new Error(
924
- `delete_vs_dir: ${ev.path} is a local directory; refusing recursive removal`
925
- );
926
- }
927
- const localStateRow = stateDb.getPath(ev.path);
928
- const localBytesHash = sha256OfFile(abs);
929
- const localDirty = localStateRow?.lastSeenHash != null && localBytesHash !== localStateRow.lastSeenHash;
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
- rmSync(abs, { force: true });
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
- mkdirSync3(abs, { recursive: true });
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
- try {
962
- rmdirSync(abs);
963
- } catch {
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 readFileSync5 } from "fs";
1112
- import { join as join7 } from "path";
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(join7(rootDir, f.path));
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 = readFileSync5(join7(rootDir, f.path));
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 join8 } from "path";
1763
+ import { join as join9 } from "path";
1271
1764
  import chokidar from "chokidar";
1272
- var DEFAULT_POLL_SECONDS = 3;
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
- if (!config) {
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 stateDb = openStateDb(
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 pollTimer;
1295
- let scanTimer;
1296
- let sseApplyTimer;
1802
+ let primary;
1297
1803
  let watcher;
1804
+ let scanTimer;
1298
1805
  let scanInFlight = Promise.resolve();
1299
- let sseSubscriber;
1300
- let applyInFlight = null;
1301
- let rerunRequested = false;
1302
- let lastApplyResult;
1303
- const triggerApply = () => {
1304
- if (applyInFlight) {
1305
- rerunRequested = true;
1306
- return applyInFlight;
1307
- }
1308
- const loop = (async () => {
1309
- try {
1310
- do {
1311
- rerunRequested = false;
1312
- try {
1313
- lastApplyResult = await applyOnce({
1314
- rootDir: opts.rootDir,
1315
- client,
1316
- stateDb
1317
- });
1318
- } catch (err) {
1319
- console.error("tapd apply error:", err);
1320
- }
1321
- } while (rerunRequested && !stopped);
1322
- return lastApplyResult;
1323
- } finally {
1324
- applyInFlight = null;
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
- heartbeatTimeoutMs: opts.sseHeartbeatTimeoutMs,
1361
- onChange: () => scheduleSseApply(),
1362
- onError: (err) => {
1363
- console.error("tapd sse:", err.message);
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
- if (!opts.noWatch) {
1368
- watcher = chokidar.watch(opts.rootDir, {
1369
- ignoreInitial: true,
1370
- // Ignore tap's own state files at the watcher level too — saves
1371
- // wakeups from sqlite WAL churn.
1372
- ignored: (path) => {
1373
- const rel = path.startsWith(opts.rootDir) ? path.slice(opts.rootDir.length).replace(/^[/\\]/, "") : path;
1374
- if (isAlwaysExcluded(rel)) return true;
1375
- return false;
1376
- },
1377
- // Reasonable defaults for laptop workloads.
1378
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1379
- atomic: true
1380
- });
1381
- for (const ev of ["add", "change", "unlink", "addDir", "unlinkDir"]) {
1382
- watcher.on(ev, () => scheduleScan());
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
- await triggerApply();
1386
- scanInFlight = scanAndPush({ rootDir: opts.rootDir, client, stateDb, ignore: ignore2 }).catch((err) => {
1387
- console.error("tapd initial scan error:", err);
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
- if (sseApplyTimer) clearTimeout(sseApplyTimer);
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
- stateDb.close();
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
- const result = await triggerApply();
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
- return scanAndPush({ rootDir: opts.rootDir, client, stateDb, ignore: ignore2 });
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-RQC73B5Y.js.map
1992
+ //# sourceMappingURL=chunk-YPZ2UNI5.js.map