zen-fs-config 0.5.41 → 0.5.43

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/index.js CHANGED
@@ -1427,6 +1427,8 @@ var APP_DATA_GROUPS_DIR = `${META_DIR}/app-data-groups`;
1427
1427
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
1428
1428
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
1429
1429
  var NODES_DIR = "/nodes";
1430
+ var SHARED_DIR = "/shared";
1431
+ var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
1430
1432
  var LOCAL_IDB_BACKEND_ID = "local-idb";
1431
1433
  function tombstoneFileName(filePath) {
1432
1434
  return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
@@ -2945,7 +2947,8 @@ var AppDataGroupImpl = class {
2945
2947
  {
2946
2948
  direction: import_zen_fs_sync.SyncDirection.BiDirectional,
2947
2949
  conflictStrategy: "source-wins",
2948
- pollIntervalMs: this._pollIntervalMs
2950
+ pollIntervalMs: this._pollIntervalMs,
2951
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
2949
2952
  },
2950
2953
  "/"
2951
2954
  );
@@ -2991,7 +2994,8 @@ var AppDataGroupImpl = class {
2991
2994
  {
2992
2995
  direction: import_zen_fs_sync.SyncDirection.BiDirectional,
2993
2996
  conflictStrategy: "source-wins",
2994
- pollIntervalMs: this._pollIntervalMs
2997
+ pollIntervalMs: this._pollIntervalMs,
2998
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
2995
2999
  },
2996
3000
  "/"
2997
3001
  );
package/dist/index.mjs CHANGED
@@ -887,6 +887,8 @@ var APP_DATA_GROUPS_DIR = `${META_DIR}/app-data-groups`;
887
887
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
888
888
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
889
889
  var NODES_DIR = "/nodes";
890
+ var SHARED_DIR = "/shared";
891
+ var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
890
892
  var LOCAL_IDB_BACKEND_ID = "local-idb";
891
893
  function tombstoneFileName(filePath) {
892
894
  return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
@@ -2405,7 +2407,8 @@ var AppDataGroupImpl = class {
2405
2407
  {
2406
2408
  direction: SyncDirection.BiDirectional,
2407
2409
  conflictStrategy: "source-wins",
2408
- pollIntervalMs: this._pollIntervalMs
2410
+ pollIntervalMs: this._pollIntervalMs,
2411
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
2409
2412
  },
2410
2413
  "/"
2411
2414
  );
@@ -2451,7 +2454,8 @@ var AppDataGroupImpl = class {
2451
2454
  {
2452
2455
  direction: SyncDirection.BiDirectional,
2453
2456
  conflictStrategy: "source-wins",
2454
- pollIntervalMs: this._pollIntervalMs
2457
+ pollIntervalMs: this._pollIntervalMs,
2458
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
2455
2459
  },
2456
2460
  "/"
2457
2461
  );
@@ -1163,7 +1163,7 @@ var ZenFSConfig = (() => {
1163
1163
  fancy: () => fancy,
1164
1164
  format: () => format,
1165
1165
  info: () => info,
1166
- isEnabled: () => isEnabled,
1166
+ isEnabled: () => isEnabled2,
1167
1167
  levelOf: () => levelOf,
1168
1168
  levels: () => levels,
1169
1169
  log: () => log4,
@@ -1174,7 +1174,7 @@ var ZenFSConfig = (() => {
1174
1174
  return levels.indexOf(value);
1175
1175
  }
1176
1176
  function log4(level, message) {
1177
- if (!isEnabled)
1177
+ if (!isEnabled2)
1178
1178
  return;
1179
1179
  const entry = {
1180
1180
  level,
@@ -1232,7 +1232,7 @@ var ZenFSConfig = (() => {
1232
1232
  _format = options.format ?? _format;
1233
1233
  _output = options.output ?? _output;
1234
1234
  minLevel = typeof options.level == "string" ? levelOf(options.level) : options.level ?? minLevel;
1235
- isEnabled = options.enabled ?? isEnabled;
1235
+ isEnabled2 = options.enabled ?? isEnabled2;
1236
1236
  includeStack = options.stack ?? includeStack;
1237
1237
  if (!options.dumpBacklog)
1238
1238
  return;
@@ -1240,7 +1240,7 @@ var ZenFSConfig = (() => {
1240
1240
  output(entry);
1241
1241
  }
1242
1242
  }
1243
- var Level, levels, entries, emerg, alert, crit, err, warn, notice, info, debug, levelColor, messageColor, _format, _output, minLevel, includeStack, isEnabled;
1243
+ var Level, levels, entries, emerg, alert, crit, err, warn, notice, info, debug, levelColor, messageColor, _format, _output, minLevel, includeStack, isEnabled2;
1244
1244
  var init_log = __esm({
1245
1245
  "node_modules/kerium/dist/log.js"() {
1246
1246
  "use strict";
@@ -1322,7 +1322,7 @@ var ZenFSConfig = (() => {
1322
1322
  _output = console.error;
1323
1323
  minLevel = Level.ALERT;
1324
1324
  includeStack = false;
1325
- isEnabled = true;
1325
+ isEnabled2 = true;
1326
1326
  }
1327
1327
  });
1328
1328
 
@@ -25298,6 +25298,66 @@ var ZenFSConfig = (() => {
25298
25298
  writeVersion: () => writeVersion
25299
25299
  });
25300
25300
 
25301
+ // node_modules/@richard432/localstorage-logger/dist/index.mjs
25302
+ var PREFIX = "debug:";
25303
+ function hasLocalStorage() {
25304
+ try {
25305
+ return typeof localStorage !== "undefined" && localStorage !== null;
25306
+ } catch {
25307
+ return false;
25308
+ }
25309
+ }
25310
+ function getProcessEnv() {
25311
+ try {
25312
+ if (typeof process !== "undefined" && process.env !== void 0) {
25313
+ return process.env;
25314
+ }
25315
+ } catch {
25316
+ }
25317
+ return null;
25318
+ }
25319
+ function moduleToEnvKey(module) {
25320
+ return "DEBUG_" + module.replace(/[:-]/g, "_").toUpperCase();
25321
+ }
25322
+ function isEnabled(module) {
25323
+ const key = `${PREFIX}${module}`;
25324
+ if (hasLocalStorage()) {
25325
+ const val = localStorage.getItem(key);
25326
+ if (val === null) {
25327
+ localStorage.setItem(key, "1");
25328
+ return true;
25329
+ }
25330
+ return val === "1";
25331
+ }
25332
+ const env = getProcessEnv();
25333
+ if (env) {
25334
+ const envKey = moduleToEnvKey(module);
25335
+ const val = env[envKey];
25336
+ if (val === void 0) return true;
25337
+ return val === "1";
25338
+ }
25339
+ return true;
25340
+ }
25341
+ function createLogger(module) {
25342
+ return {
25343
+ log(...args) {
25344
+ if (isEnabled(module)) {
25345
+ console.log(`[${module}]`, ...args);
25346
+ }
25347
+ },
25348
+ warn(...args) {
25349
+ if (isEnabled(module)) {
25350
+ console.warn(`[${module}]`, ...args);
25351
+ }
25352
+ },
25353
+ error(...args) {
25354
+ if (isEnabled(module)) {
25355
+ console.error(`[${module}]`, ...args);
25356
+ }
25357
+ }
25358
+ };
25359
+ }
25360
+
25301
25361
  // node_modules/zen-fs-sync/dist/index.mjs
25302
25362
  function isDirectory(stat5) {
25303
25363
  return stat5.mode !== void 0 && (stat5.mode & 16384) === 16384;
@@ -25310,16 +25370,12 @@ var ZenFSConfig = (() => {
25310
25370
  SyncDirection2["BiDirectional"] = "bi-directional";
25311
25371
  return SyncDirection2;
25312
25372
  })(SyncDirection || {});
25313
- var enabled = false;
25314
- var tagFilter = /* @__PURE__ */ new Set();
25315
- function createLogger(tag) {
25316
- return (...args) => {
25317
- if (!enabled) return;
25318
- if (tagFilter.size > 0 && !tagFilter.has(tag)) return;
25319
- console.log(`[zen-fs-sync:${tag}]`, ...args);
25320
- };
25373
+ var MODULE_PREFIX = "zen-fs-sync";
25374
+ function createLogger2(tag) {
25375
+ const logger = createLogger(`${MODULE_PREFIX}:${tag}`);
25376
+ return (...args) => logger.log(...args);
25321
25377
  }
25322
- var log = createLogger("detector");
25378
+ var log = createLogger2("detector");
25323
25379
  function normalizePath(p) {
25324
25380
  let s = p.replace(/\\/g, "/");
25325
25381
  if (s.length > 1 && s.endsWith("/")) {
@@ -25367,6 +25423,11 @@ var ZenFSConfig = (() => {
25367
25423
  );
25368
25424
  return regex.test(str);
25369
25425
  }
25426
+ function describeFs(fs) {
25427
+ if (fs.backendName) return fs.backendName;
25428
+ const ctorName = fs.constructor?.name;
25429
+ return ctorName && ctorName !== "Object" ? ctorName : "unknown";
25430
+ }
25370
25431
  async function walkFiles(fs, root, filter) {
25371
25432
  const results = [];
25372
25433
  const normalizedRoot = normalizePath(root);
@@ -25392,6 +25453,18 @@ var ZenFSConfig = (() => {
25392
25453
  await visit(fullPath);
25393
25454
  } else if (isFile(stat5)) {
25394
25455
  if (!isPathAllowed(relPath, filter)) continue;
25456
+ if (relPath.endsWith(".mtime")) {
25457
+ console.warn(
25458
+ `[zen-fs-sync] mtime sidecar leaked from backend "${describeFs(fs)}" - its readdir()/createSnapshot() must filter sidecars; skipping ${fullPath} (rel ${relPath})`
25459
+ );
25460
+ continue;
25461
+ }
25462
+ if (relPath === "/.keep" || relPath.endsWith("/.keep")) {
25463
+ console.warn(
25464
+ `[zen-fs-sync] '.keep' placeholder leaked from backend "${describeFs(fs)}" - its readdir() must filter the internal placeholder; skipping ${fullPath} (rel ${relPath})`
25465
+ );
25466
+ continue;
25467
+ }
25395
25468
  results.push(relPath);
25396
25469
  }
25397
25470
  }
@@ -25399,13 +25472,48 @@ var ZenFSConfig = (() => {
25399
25472
  await visit(normalizedRoot);
25400
25473
  return results;
25401
25474
  }
25475
+ async function collectDirectories(fs, root, filter) {
25476
+ const results = [];
25477
+ const normalizedRoot = normalizePath(root);
25478
+ async function visit(dir) {
25479
+ let entries2;
25480
+ try {
25481
+ entries2 = await fs.readdir(dir);
25482
+ } catch {
25483
+ return;
25484
+ }
25485
+ for (const entry of entries2) {
25486
+ if (entry === ".zenfs-sync") continue;
25487
+ const fullPath = resolvePath(dir, entry);
25488
+ let relPath = fullPath.slice(normalizedRoot.length) || "/";
25489
+ if (!relPath.startsWith("/")) relPath = "/" + relPath;
25490
+ let stat5;
25491
+ try {
25492
+ stat5 = await fs.stat(fullPath);
25493
+ } catch {
25494
+ continue;
25495
+ }
25496
+ if (isDirectory(stat5)) {
25497
+ if (isPathAllowed(relPath, filter)) results.push(relPath);
25498
+ await visit(fullPath);
25499
+ }
25500
+ }
25501
+ }
25502
+ await visit(normalizedRoot);
25503
+ return results;
25504
+ }
25402
25505
  async function buildSnapshot(fs, root, filter) {
25403
25506
  const normalizedRoot = normalizePath(root);
25404
25507
  const fsName = fs.backendName || "unknown";
25405
25508
  try {
25406
25509
  await fs.readdir(normalizedRoot);
25407
- } catch {
25408
- console.warn(`[zen-fs-sync] buildSnapshot(${fsName}): unreachable, returning null`);
25510
+ } catch (err2) {
25511
+ const e = err2;
25512
+ if (e && (e.name === "DirectoryNotFoundError" || e.code === "ENOENT")) {
25513
+ log(`buildSnapshot(${fsName}): root missing, treating as empty`);
25514
+ return /* @__PURE__ */ new Map();
25515
+ }
25516
+ log(`buildSnapshot(${fsName}): unreachable, returning null`);
25409
25517
  return null;
25410
25518
  }
25411
25519
  const files = await walkFiles(fs, root, filter);
@@ -25466,7 +25574,7 @@ var ZenFSConfig = (() => {
25466
25574
  }
25467
25575
  function diffSnapshots(source, target) {
25468
25576
  if (source === null || target === null) {
25469
- console.log(`[zen-fs-sync] diffSnapshots: one side is null (unreachable) \u2014 skipping to prevent false deletions`);
25577
+ log(`diffSnapshots: one side is null (unreachable) \u2014 skipping to prevent false deletions`);
25470
25578
  return [];
25471
25579
  }
25472
25580
  const changes = [];
@@ -25492,7 +25600,7 @@ var ZenFSConfig = (() => {
25492
25600
  }
25493
25601
  return changes;
25494
25602
  }
25495
- var log2 = createLogger("detector");
25603
+ var log2 = createLogger2("detector");
25496
25604
  var IncrementalDetector = class {
25497
25605
  async detect(source, target, root, prevSnapshots, filter) {
25498
25606
  if (!prevSnapshots || prevSnapshots.size === 0) {
@@ -25559,7 +25667,7 @@ var ZenFSConfig = (() => {
25559
25667
  }
25560
25668
  }
25561
25669
  };
25562
- var log3 = createLogger("sync");
25670
+ var log3 = createLogger2("sync");
25563
25671
  var SyncPair = class {
25564
25672
  constructor(source, target, options = {}, syncRoot = "/") {
25565
25673
  __publicField(this, "syncRoot");
@@ -25572,6 +25680,7 @@ var ZenFSConfig = (() => {
25572
25680
  __publicField(this, "resolver");
25573
25681
  __publicField(this, "state", "idle");
25574
25682
  __publicField(this, "lastResult");
25683
+ __publicField(this, "lastError");
25575
25684
  __publicField(this, "lastCheckTime");
25576
25685
  __publicField(this, "totalSyncs", 0);
25577
25686
  // 轮询定时器列表(远程 shouldSync 轮询,或两端都不支持变更检测时的兜底轮询)
@@ -25632,7 +25741,7 @@ var ZenFSConfig = (() => {
25632
25741
  try {
25633
25742
  await this.options.preSyncHook();
25634
25743
  } catch (err2) {
25635
- console.warn(`[zen-fs-sync] preSyncHook error pairId=${this.pairId}`, err2);
25744
+ log3(`[zen-fs-sync] preSyncHook error pairId=${this.pairId}`, err2);
25636
25745
  } finally {
25637
25746
  this.hookInProgress = false;
25638
25747
  }
@@ -25647,18 +25756,21 @@ var ZenFSConfig = (() => {
25647
25756
  try {
25648
25757
  await this.options.postSyncHook();
25649
25758
  } catch (err2) {
25650
- console.warn(`[zen-fs-sync] postSyncHook error pairId=${this.pairId}`, err2);
25759
+ log3(`[zen-fs-sync] postSyncHook error pairId=${this.pairId}`, err2);
25651
25760
  } finally {
25652
25761
  this.hookInProgress = false;
25653
25762
  }
25654
25763
  }
25655
25764
  result.durationMs = Date.now() - startTime;
25656
25765
  this.lastResult = result;
25766
+ if (result.errors && result.errors.length > 0) {
25767
+ this.lastError = { message: result.errors[0].message, timestamp: result.timestamp };
25768
+ }
25657
25769
  this.totalSyncs++;
25658
25770
  this.state = this.pollTimers.length > 0 ? "watching" : "idle";
25659
25771
  const hasActivity = result.filesCreated > 0 || result.filesUpdated > 0 || result.filesDeleted > 0 || result.conflicts.length > 0;
25660
25772
  if (hasActivity) {
25661
- console.log(`[zen-fs-sync] sync pairId=${this.pairId} +${result.filesCreated}/~${result.filesUpdated}/-${result.filesDeleted} skip:${result.filesSkipped} conflicts:${result.conflicts.length} ${result.durationMs}ms`);
25773
+ log3(`[zen-fs-sync] sync pairId=${this.pairId} +${result.filesCreated}/~${result.filesUpdated}/-${result.filesDeleted} skip:${result.filesSkipped} conflicts:${result.conflicts.length} ${result.durationMs}ms`);
25662
25774
  }
25663
25775
  this.emit({
25664
25776
  type: "sync:end",
@@ -25669,7 +25781,8 @@ var ZenFSConfig = (() => {
25669
25781
  return result;
25670
25782
  } catch (error) {
25671
25783
  this.state = this.pollTimers.length > 0 ? "watching" : "idle";
25672
- console.error(`[zen-fs-sync] sync ERROR pairId=${this.pairId}`, error);
25784
+ this.lastError = { message: error instanceof Error ? error.message : String(error), timestamp: Date.now() };
25785
+ log3(`[zen-fs-sync] sync ERROR pairId=${this.pairId}`, error);
25673
25786
  this.emit({
25674
25787
  type: "sync:error",
25675
25788
  pairId: this.pairId,
@@ -25760,6 +25873,7 @@ var ZenFSConfig = (() => {
25760
25873
  state: this.state,
25761
25874
  lastResult: this.lastResult,
25762
25875
  lastCheckTime: this.lastCheckTime,
25876
+ lastError: this.lastError,
25763
25877
  // 是否正在 watch:有轮询定时器或处于 Watching 状态
25764
25878
  watching: this.pollTimers.length > 0 || this.state === "watching",
25765
25879
  totalSyncs: this.totalSyncs
@@ -25814,7 +25928,7 @@ var ZenFSConfig = (() => {
25814
25928
  this.options.filter
25815
25929
  );
25816
25930
  if (changes.length > 0) {
25817
- console.log(`[zen-fs-sync] syncOneWay START direction=${directionLabel} changes=${changes.length}`);
25931
+ log3(`[zen-fs-sync] syncOneWay START direction=${directionLabel} changes=${changes.length}`);
25818
25932
  }
25819
25933
  const newSnap = await this.getSnapshot(src);
25820
25934
  if (newSnap !== null) {
@@ -25825,6 +25939,7 @@ var ZenFSConfig = (() => {
25825
25939
  let filesDeleted = 0;
25826
25940
  let filesSkipped = 0;
25827
25941
  const conflicts = [];
25942
+ const errors = [];
25828
25943
  for (const change of changes) {
25829
25944
  const srcPath = resolvePath(this.root, change.path);
25830
25945
  const tgtPath = resolvePath(this.root, change.path);
@@ -25868,37 +25983,40 @@ var ZenFSConfig = (() => {
25868
25983
  try {
25869
25984
  const tgtContent = await tgt.readFile(tgtPath, "utf-8");
25870
25985
  if (srcContent === tgtContent) {
25871
- console.log(`[zen-fs-sync] WRITE SKIP (content identical) ${change.path}`);
25986
+ log3(`[zen-fs-sync] WRITE SKIP (content identical) ${change.path}`);
25872
25987
  filesSkipped++;
25873
25988
  break;
25874
25989
  }
25875
25990
  } catch {
25876
25991
  }
25877
- console.log(`[zen-fs-sync] WRITE ${change.type} [${directionLabel}] ${srcPath} \u2192 ${tgtPath} (${srcContent.length} chars)`);
25992
+ log3(`[zen-fs-sync] WRITE ${change.type} [${directionLabel}] ${srcPath} \u2192 ${tgtPath} (${srcContent.length} chars)`);
25878
25993
  await ensureDir(tgt, tgtPath.substring(0, tgtPath.lastIndexOf("/")));
25879
25994
  await tgt.writeFile(tgtPath, srcContent);
25880
25995
  if (isCreated) filesCreated++;
25881
25996
  else filesUpdated++;
25882
25997
  } catch (err2) {
25883
- console.error(`[zen-fs-sync] WRITE FAIL ${change.type} [${directionLabel}] ${srcPath} \u2192 ${tgtPath}:`, err2);
25998
+ log3(`[zen-fs-sync] WRITE FAIL ${change.type} [${directionLabel}] ${srcPath} \u2192 ${tgtPath}:`, err2);
25999
+ errors.push({ path: change.path, message: err2 instanceof Error ? err2.message : String(err2) });
25884
26000
  filesSkipped++;
25885
26001
  }
25886
26002
  break;
25887
26003
  }
25888
26004
  case "deleted": {
25889
26005
  try {
25890
- console.log(`[zen-fs-sync] DELETE [${directionLabel}] ${tgtPath}`);
25891
- console.log(`[SYNC-TRACE] oneway DELETE \u2192 tgt.unlink(${tgtPath}) \u2014 NO exists() check before unlink`);
26006
+ log3(`[zen-fs-sync] DELETE [${directionLabel}] ${tgtPath}`);
26007
+ log3(`[SYNC-TRACE] oneway DELETE \u2192 tgt.unlink(${tgtPath}) \u2014 NO exists() check before unlink`);
25892
26008
  await tgt.unlink(tgtPath);
25893
26009
  filesDeleted++;
25894
26010
  } catch (err2) {
25895
- console.warn(`[zen-fs-sync] DELETE SKIP [${directionLabel}] ${tgtPath}:`, err2);
26011
+ log3(`[zen-fs-sync] DELETE SKIP [${directionLabel}] ${tgtPath}:`, err2);
26012
+ errors.push({ path: change.path, message: err2 instanceof Error ? err2.message : String(err2) });
25896
26013
  filesSkipped++;
25897
26014
  }
25898
26015
  break;
25899
26016
  }
25900
26017
  }
25901
26018
  }
26019
+ await this.preserveDirectories(src, tgt);
25902
26020
  return {
25903
26021
  pairId: this.pairId,
25904
26022
  direction: this.options.direction,
@@ -25908,6 +26026,7 @@ var ZenFSConfig = (() => {
25908
26026
  filesDeleted,
25909
26027
  filesSkipped,
25910
26028
  conflicts,
26029
+ errors,
25911
26030
  changes,
25912
26031
  durationMs: 0
25913
26032
  // 由 sync() 方法填充
@@ -25920,7 +26039,7 @@ var ZenFSConfig = (() => {
25920
26039
  this.getSnapshot(this.target)
25921
26040
  ]);
25922
26041
  if (srcSnap === null || tgtSnap === null) {
25923
- console.log(`[zen-fs-sync] syncBidirectional SKIP (one side unreachable)`);
26042
+ log3(`[zen-fs-sync] syncBidirectional SKIP (one side unreachable)`);
25924
26043
  return {
25925
26044
  pairId: this.pairId,
25926
26045
  direction: "bi-directional",
@@ -25957,13 +26076,14 @@ var ZenFSConfig = (() => {
25957
26076
  this.prevTgtSnap = tgtSnap;
25958
26077
  const srcPaths = Array.from(srcSnap.keys()).sort();
25959
26078
  const tgtPaths = Array.from(tgtSnap.keys()).sort();
25960
- console.log(`[zen-fs-sync] syncBidirectional comparing source=${srcPaths.length} target=${tgtPaths.length}`);
26079
+ log3(`[zen-fs-sync] syncBidirectional comparing source=${srcPaths.length} target=${tgtPaths.length}`);
25961
26080
  let filesCreated = 0;
25962
26081
  let filesUpdated = 0;
25963
26082
  let filesDeleted = 0;
25964
26083
  let filesSkipped = 0;
25965
26084
  const conflicts = [];
25966
26085
  const changes = [];
26086
+ const errors = [];
25967
26087
  const allPaths = /* @__PURE__ */ new Set([...srcSnap.keys(), ...tgtSnap.keys()]);
25968
26088
  for (const path of allPaths) {
25969
26089
  const srcEntry = srcSnap.get(path);
@@ -25972,13 +26092,14 @@ var ZenFSConfig = (() => {
25972
26092
  if (oldPrevSrcSnap?.has(path)) {
25973
26093
  try {
25974
26094
  const fullPath = resolvePath(this.root, path);
25975
- console.log(`[zen-fs-sync] DELETE (src deleted) target ${path}`);
25976
- console.log(`[SYNC-TRACE] bidirectional DELETE (src deleted) \u2192 target.unlink(${fullPath}) \u2014 NO exists() check before unlink`);
26095
+ log3(`[zen-fs-sync] DELETE (src deleted) target ${path}`);
26096
+ log3(`[SYNC-TRACE] bidirectional DELETE (src deleted) \u2192 target.unlink(${fullPath}) \u2014 NO exists() check before unlink`);
25977
26097
  await this.target.unlink(fullPath);
25978
26098
  filesDeleted++;
25979
26099
  changes.push({ path, type: "deleted", targetSnapshot: tgtEntry });
25980
26100
  } catch (err2) {
25981
- console.warn(`[zen-fs-sync] DELETE SKIP (src deleted) target ${path}:`, err2);
26101
+ log3(`[zen-fs-sync] DELETE SKIP (src deleted) target ${path}:`, err2);
26102
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
25982
26103
  filesSkipped++;
25983
26104
  }
25984
26105
  } else {
@@ -25987,12 +26108,13 @@ var ZenFSConfig = (() => {
25987
26108
  if (wrote) {
25988
26109
  filesCreated++;
25989
26110
  changes.push({ path, type: "created", sourceSnapshot: tgtEntry });
25990
- console.log(`[zen-fs-sync] COPY target\u2192source ${path}`);
26111
+ log3(`[zen-fs-sync] COPY target\u2192source ${path}`);
25991
26112
  } else {
25992
26113
  filesSkipped++;
25993
26114
  }
25994
26115
  } catch (err2) {
25995
- console.error(`[zen-fs-sync] COPY FAIL target\u2192source ${path}:`, err2);
26116
+ log3(`[zen-fs-sync] COPY FAIL target\u2192source ${path}:`, err2);
26117
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
25996
26118
  filesSkipped++;
25997
26119
  }
25998
26120
  }
@@ -26000,13 +26122,14 @@ var ZenFSConfig = (() => {
26000
26122
  if (oldPrevTgtSnap?.has(path)) {
26001
26123
  try {
26002
26124
  const fullPath = resolvePath(this.root, path);
26003
- console.log(`[zen-fs-sync] DELETE (tgt deleted) source ${path}`);
26004
- console.log(`[SYNC-TRACE] bidirectional DELETE (tgt deleted) \u2192 source.unlink(${fullPath}) \u2014 NO exists() check before unlink`);
26125
+ log3(`[zen-fs-sync] DELETE (tgt deleted) source ${path}`);
26126
+ log3(`[SYNC-TRACE] bidirectional DELETE (tgt deleted) \u2192 source.unlink(${fullPath}) \u2014 NO exists() check before unlink`);
26005
26127
  await this.source.unlink(fullPath);
26006
26128
  filesDeleted++;
26007
26129
  changes.push({ path, type: "deleted", sourceSnapshot: srcEntry });
26008
26130
  } catch (err2) {
26009
- console.warn(`[zen-fs-sync] DELETE SKIP (tgt deleted) source ${path}:`, err2);
26131
+ log3(`[zen-fs-sync] DELETE SKIP (tgt deleted) source ${path}:`, err2);
26132
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
26010
26133
  filesSkipped++;
26011
26134
  }
26012
26135
  } else {
@@ -26015,12 +26138,13 @@ var ZenFSConfig = (() => {
26015
26138
  if (wrote) {
26016
26139
  filesCreated++;
26017
26140
  changes.push({ path, type: "created", sourceSnapshot: srcEntry });
26018
- console.log(`[zen-fs-sync] COPY source\u2192target ${path}`);
26141
+ log3(`[zen-fs-sync] COPY source\u2192target ${path}`);
26019
26142
  } else {
26020
26143
  filesSkipped++;
26021
26144
  }
26022
26145
  } catch (err2) {
26023
- console.error(`[zen-fs-sync] COPY FAIL source\u2192target ${path}:`, err2);
26146
+ log3(`[zen-fs-sync] COPY FAIL source\u2192target ${path}:`, err2);
26147
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
26024
26148
  filesSkipped++;
26025
26149
  }
26026
26150
  }
@@ -26036,15 +26160,17 @@ var ZenFSConfig = (() => {
26036
26160
  srcContent = await this.source.readFile(fullPath, "utf-8");
26037
26161
  tgtContent = await this.target.readFile(fullPath, "utf-8");
26038
26162
  } catch (err2) {
26039
- console.error(`[zen-fs-sync] CONTENT READ FAIL ${path}:`, err2);
26163
+ log3(`[zen-fs-sync] CONTENT READ FAIL ${path}:`, err2);
26164
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
26040
26165
  filesSkipped++;
26041
26166
  continue;
26042
26167
  }
26043
26168
  if (srcContent === tgtContent) {
26044
26169
  const oldestMtime = Math.min(srcEntry.mtimeMs, tgtEntry.mtimeMs);
26170
+ const keptOn = srcEntry.mtimeMs <= tgtEntry.mtimeMs ? "src" : "tgt";
26045
26171
  await this.normalizeMtimeBoth(path, srcContent, oldestMtime);
26046
26172
  filesSkipped++;
26047
- console.log(`[zen-fs-sync] MTIME NORMALIZE ${path} \u2192 mtime=${oldestMtime} (content identical, was src=${srcEntry.mtimeMs} tgt=${tgtEntry.mtimeMs})`);
26173
+ log3(`[zen-fs-sync] MTIME NORMALIZE ${path} \u2192 mtime=${oldestMtime} (content identical; src=${srcEntry.mtimeMs} tgt=${tgtEntry.mtimeMs}; kept=oldest(${keptOn}); real mtime stored in sidecar .${path}.mtime)`);
26048
26174
  } else {
26049
26175
  const newerIsSource = srcEntry.mtimeMs > tgtEntry.mtimeMs;
26050
26176
  const fromFs = newerIsSource ? this.source : this.target;
@@ -26055,12 +26181,13 @@ var ZenFSConfig = (() => {
26055
26181
  if (wrote) {
26056
26182
  filesUpdated++;
26057
26183
  changes.push({ path, type: "modified", sourceSnapshot: newerIsSource ? srcEntry : tgtEntry, targetSnapshot: newerIsSource ? tgtEntry : srcEntry });
26058
- console.log(`[zen-fs-sync] UPDATE ${fromLabel} ${path} (mtime=${newerIsSource ? srcEntry.mtimeMs : tgtEntry.mtimeMs} > ${newerIsSource ? tgtEntry.mtimeMs : srcEntry.mtimeMs})`);
26184
+ log3(`[zen-fs-sync] UPDATE ${fromLabel} ${path} (mtime=${newerIsSource ? srcEntry.mtimeMs : tgtEntry.mtimeMs} > ${newerIsSource ? tgtEntry.mtimeMs : srcEntry.mtimeMs})`);
26059
26185
  } else {
26060
26186
  filesSkipped++;
26061
26187
  }
26062
26188
  } catch (err2) {
26063
- console.error(`[zen-fs-sync] UPDATE FAIL ${fromLabel} ${path}:`, err2);
26189
+ log3(`[zen-fs-sync] UPDATE FAIL ${fromLabel} ${path}:`, err2);
26190
+ errors.push({ path, message: err2 instanceof Error ? err2.message : String(err2) });
26064
26191
  filesSkipped++;
26065
26192
  }
26066
26193
  }
@@ -26086,12 +26213,14 @@ var ZenFSConfig = (() => {
26086
26213
  await this.writeFileBoth(path, resolved.content);
26087
26214
  filesUpdated++;
26088
26215
  changes.push({ path, type: "modified", sourceSnapshot: { ...srcEntry, mtimeMs: Date.now() }, targetSnapshot: srcEntry });
26089
- console.log(`[zen-fs-sync] CONFLICT ${path} resolved=${resolved.strategy}`);
26216
+ log3(`[zen-fs-sync] CONFLICT ${path} resolved=${resolved.strategy}`);
26090
26217
  }
26091
26218
  }
26092
26219
  }
26220
+ await this.preserveDirectories(this.source, this.target);
26221
+ await this.preserveDirectories(this.target, this.source);
26093
26222
  const durationMs = Date.now() - startTime;
26094
- console.log(`[zen-fs-sync] syncBidirectional END pairId=${this.pairId} +${filesCreated}/~${filesUpdated}/-${filesDeleted} ${durationMs}ms`);
26223
+ log3(`[zen-fs-sync] syncBidirectional END pairId=${this.pairId} +${filesCreated}/~${filesUpdated}/-${filesDeleted} ${durationMs}ms`);
26095
26224
  return {
26096
26225
  pairId: this.pairId,
26097
26226
  direction: "bi-directional",
@@ -26101,6 +26230,7 @@ var ZenFSConfig = (() => {
26101
26230
  filesDeleted,
26102
26231
  filesSkipped,
26103
26232
  conflicts,
26233
+ errors,
26104
26234
  changes,
26105
26235
  durationMs
26106
26236
  };
@@ -26118,6 +26248,28 @@ var ZenFSConfig = (() => {
26118
26248
  }
26119
26249
  return buildSnapshot(fs, this.root, this.options.filter);
26120
26250
  }
26251
+ /**
26252
+ * Preserve empty directories across backends (zen-fs-sync/docs/SyncableFS.md §2).
26253
+ *
26254
+ * A backend that cannot store empty directories (Git, RemoteStorage) keeps a
26255
+ * directory alive with an internal `.keep` placeholder that is hidden from
26256
+ * callers. Because the sync snapshot is file-based, empty directories are not
26257
+ * represented, so the sync engine must explicitly `mkdir` each source
26258
+ * directory on the target. The target's own `mkdir` then produces its own
26259
+ * (hidden) placeholder. Best-effort: failures are logged, never thrown.
26260
+ */
26261
+ async preserveDirectories(source, target) {
26262
+ try {
26263
+ const dirs = await collectDirectories(source, this.root, this.options.filter);
26264
+ for (const relDir of dirs) {
26265
+ if (relDir === "/") continue;
26266
+ const fullDir = resolvePath(this.root, relDir);
26267
+ await ensureDir(target, fullDir);
26268
+ }
26269
+ } catch (err2) {
26270
+ log3(`[zen-fs-sync] preserveDirectories FAIL: ${err2}`);
26271
+ }
26272
+ }
26121
26273
  /**
26122
26274
  * Compare two snapshots for equality (same paths, same size, same mtime).
26123
26275
  * Returns true if they are identical, false otherwise.
@@ -26682,70 +26834,10 @@ var ZenFSConfig = (() => {
26682
26834
  init_backend_registry();
26683
26835
  init_dist4();
26684
26836
 
26685
- // node_modules/@richard432/localstorage-logger/dist/index.mjs
26686
- var PREFIX = "debug:";
26687
- function hasLocalStorage() {
26688
- try {
26689
- return typeof localStorage !== "undefined" && localStorage !== null;
26690
- } catch {
26691
- return false;
26692
- }
26693
- }
26694
- function getProcessEnv() {
26695
- try {
26696
- if (typeof process !== "undefined" && process.env !== void 0) {
26697
- return process.env;
26698
- }
26699
- } catch {
26700
- }
26701
- return null;
26702
- }
26703
- function moduleToEnvKey(module) {
26704
- return "DEBUG_" + module.replace(/[:-]/g, "_").toUpperCase();
26705
- }
26706
- function isEnabled2(module) {
26707
- const key = `${PREFIX}${module}`;
26708
- if (hasLocalStorage()) {
26709
- const val = localStorage.getItem(key);
26710
- if (val === null) {
26711
- localStorage.setItem(key, "1");
26712
- return true;
26713
- }
26714
- return val === "1";
26715
- }
26716
- const env = getProcessEnv();
26717
- if (env) {
26718
- const envKey = moduleToEnvKey(module);
26719
- const val = env[envKey];
26720
- if (val === void 0) return true;
26721
- return val === "1";
26722
- }
26723
- return true;
26724
- }
26725
- function createLogger2(module) {
26726
- return {
26727
- log(...args) {
26728
- if (isEnabled2(module)) {
26729
- console.log(`[${module}]`, ...args);
26730
- }
26731
- },
26732
- warn(...args) {
26733
- if (isEnabled2(module)) {
26734
- console.warn(`[${module}]`, ...args);
26735
- }
26736
- },
26737
- error(...args) {
26738
- if (isEnabled2(module)) {
26739
- console.error(`[${module}]`, ...args);
26740
- }
26741
- }
26742
- };
26743
- }
26744
-
26745
26837
  // src/logger.ts
26746
- var MODULE_PREFIX = "zen-fs-config";
26838
+ var MODULE_PREFIX2 = "zen-fs-config";
26747
26839
  function createLogger3(prefix) {
26748
- const logger = createLogger2(`${MODULE_PREFIX}:${prefix}`);
26840
+ const logger = createLogger(`${MODULE_PREFIX2}:${prefix}`);
26749
26841
  return (...args) => logger.log(...args);
26750
26842
  }
26751
26843
 
@@ -27351,7 +27443,7 @@ var ZenFSConfig = (() => {
27351
27443
  }
27352
27444
 
27353
27445
  // src/config-repo.ts
27354
- var log6 = createLogger2("zen-fs-config:config-repo");
27446
+ var log6 = createLogger("zen-fs-config:config-repo");
27355
27447
  var META_DIR = "/.meta";
27356
27448
  var GROUP_TYPE_FILE = `${META_DIR}/group-type`;
27357
27449
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
@@ -27360,6 +27452,8 @@ var ZenFSConfig = (() => {
27360
27452
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
27361
27453
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
27362
27454
  var NODES_DIR = "/nodes";
27455
+ var SHARED_DIR = "/shared";
27456
+ var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
27363
27457
  var LOCAL_IDB_BACKEND_ID = "local-idb";
27364
27458
  function tombstoneFileName(filePath) {
27365
27459
  return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
@@ -28878,7 +28972,8 @@ var ZenFSConfig = (() => {
28878
28972
  {
28879
28973
  direction: SyncDirection.BiDirectional,
28880
28974
  conflictStrategy: "source-wins",
28881
- pollIntervalMs: this._pollIntervalMs
28975
+ pollIntervalMs: this._pollIntervalMs,
28976
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
28882
28977
  },
28883
28978
  "/"
28884
28979
  );
@@ -28924,7 +29019,8 @@ var ZenFSConfig = (() => {
28924
29019
  {
28925
29020
  direction: SyncDirection.BiDirectional,
28926
29021
  conflictStrategy: "source-wins",
28927
- pollIntervalMs: this._pollIntervalMs
29022
+ pollIntervalMs: this._pollIntervalMs,
29023
+ filter: { excludePrefixes: CONFIG_ONLY_PREFIXES }
28928
29024
  },
28929
29025
  "/"
28930
29026
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.41",
3
+ "version": "0.5.43",
4
4
  "description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -85,6 +85,6 @@
85
85
  "typescript": "^5.9.3",
86
86
  "vitest": "^1.6.1",
87
87
  "zen-fs-cache": "^1.0.6",
88
- "zen-fs-sync": "^0.4.9"
88
+ "zen-fs-sync": "^0.4.17"
89
89
  }
90
90
  }