zen-fs-config 0.3.24 → 0.3.27

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.d.mts CHANGED
@@ -29,6 +29,17 @@ interface VersionMeta {
29
29
  /** Timestamp when the version was created. */
30
30
  timestamp: number;
31
31
  }
32
+ /** Content of a tombstone file in `.meta/.deleted/`. */
33
+ interface TombstoneMeta {
34
+ /** The deleted file path. */
35
+ path: string;
36
+ /** Timestamp of deletion. */
37
+ deletedAt: number;
38
+ /** Backend ID that initiated the deletion. */
39
+ deletedBy: string;
40
+ /** Backend IDs that have confirmed the deletion (synced). */
41
+ confirmedBy: string[];
42
+ }
32
43
  /** Content of a conflict archive file in `.meta/.conflicts/`. */
33
44
  interface ConflictArchive {
34
45
  /** The config file path that conflicted. */
@@ -146,6 +157,12 @@ interface IConfigRepo {
146
157
  getBackends(): Promise<BackendsMeta | null>;
147
158
  /** Write .meta/backends.json. */
148
159
  updateBackends(meta: BackendsMeta): Promise<void>;
160
+ /**
161
+ * Delete a file and record a tombstone for cross-backend sync.
162
+ * The tombstone ensures the deletion propagates to all backends
163
+ * instead of being treated as a "missing file" that gets re-created.
164
+ */
165
+ deleteFile(path: string): Promise<void>;
149
166
  /**
150
167
  * Sync .meta/ files (backends.json) to all replica backends.
151
168
  * Called automatically by createConfigRepo() after setupSync().
@@ -191,21 +208,54 @@ declare function configKeyToFilePath(configPath: string): string;
191
208
  declare function getExtension(path: string): string;
192
209
 
193
210
  /**
194
- * zen-fs-config — ConfigRepo Implementation
211
+ * zen-fs-config — Backend Registry
195
212
  *
196
- * Core implementation of IConfigRepo and the createConfigRepo factory.
213
+ * A pluggable registry that maps backend type names to factory functions.
214
+ *
215
+ * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
216
+ * Instead, it provides:
217
+ * 1. A simple registry API (registerBackend, createBackend, etc.)
218
+ * 2. One built-in backend (InMemory) — zero extra dependencies
219
+ * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
220
+ * implementation into the BackendInstance interface
221
+ *
222
+ * Applications (like zen-fs-config-admin) register whatever backends
223
+ * they need at startup. Adding a new backend never requires changing
224
+ * zen-fs-config itself.
197
225
  */
198
226
 
199
- interface MinimalAsyncFS {
227
+ type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
228
+ /**
229
+ * The minimal interface a backend instance must satisfy.
230
+ * Matches zen-fs-cache's CacheableFileSystem requirements.
231
+ */
232
+ interface BackendInstance {
200
233
  readFile(path: string, ...args: any[]): Promise<any>;
201
- writeFile(path: string, data: any, options?: any): Promise<void>;
234
+ writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
202
235
  readdir(path: string): Promise<string[]>;
203
- stat(path: string): Promise<any>;
236
+ stat(path: string, ...args: any[]): Promise<any>;
204
237
  exists(path: string): Promise<boolean>;
205
238
  mkdir(path: string, options?: any): Promise<any>;
206
239
  unlink(path: string): Promise<void>;
207
- rmdir?(path: string): Promise<void>;
240
+ rmdir(path: string): Promise<void>;
208
241
  rename?(oldPath: string, newPath: string): Promise<void>;
242
+ readFileMeta?(path: string, opts?: any): Promise<any>;
243
+ getRevision?(path: string): Promise<string | number | undefined>;
244
+ }
245
+ declare function registerBackend(type: string, factory: BackendFactory): void;
246
+ declare function unregisterBackend(type: string): boolean;
247
+ declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
248
+ declare function hasBackend(type: string): boolean;
249
+ declare function listBackends(): string[];
250
+ declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
251
+
252
+ /**
253
+ * zen-fs-config — ConfigRepo Implementation
254
+ *
255
+ * Core implementation of IConfigRepo and the createConfigRepo factory.
256
+ */
257
+
258
+ interface MinimalAsyncFS extends BackendInstance {
209
259
  }
210
260
  declare class ConfigRepo implements IConfigRepo {
211
261
  readonly appId: string;
@@ -222,7 +272,8 @@ declare class ConfigRepo implements IConfigRepo {
222
272
  private onConflictCallback?;
223
273
  private disposed;
224
274
  private configCache;
225
- constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
275
+ private readonly primaryBackendId;
276
+ constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
226
277
  /** Full path to this node's directory on the primary backend. */
227
278
  get nodePath(): string;
228
279
  load(rawConfig?: string): Promise<void>;
@@ -235,6 +286,28 @@ declare class ConfigRepo implements IConfigRepo {
235
286
  }): Promise<SyncResult>;
236
287
  peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
237
288
  flush(): Promise<SyncResult[]>;
289
+ /**
290
+ * Delete a file and write a tombstone so the deletion propagates
291
+ * to all backends instead of being treated as "missing file → re-create".
292
+ */
293
+ deleteFile(path: string): Promise<void>;
294
+ /**
295
+ * Read all tombstones from the primary backend.
296
+ */
297
+ private readTombstones;
298
+ /**
299
+ * Before sync: for each tombstone, delete the actual file on all replicas.
300
+ * This prevents bi-directional sync from copying the file back.
301
+ */
302
+ private processTombstones;
303
+ /**
304
+ * After sync: mark each tombstone as confirmed by all replica backends.
305
+ */
306
+ private updateTombstoneConfirmations;
307
+ /**
308
+ * GC: remove tombstones where all backends in backends.json have confirmed.
309
+ */
310
+ private gcTombstones;
238
311
  /**
239
312
  * Sync .meta/ files (backends.json) to all replica backends.
240
313
  *
@@ -264,48 +337,6 @@ declare class ConfigRepo implements IConfigRepo {
264
337
  }
265
338
  declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Promise<IConfigRepo>;
266
339
 
267
- /**
268
- * zen-fs-config — Backend Registry
269
- *
270
- * A pluggable registry that maps backend type names to factory functions.
271
- *
272
- * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
273
- * Instead, it provides:
274
- * 1. A simple registry API (registerBackend, createBackend, etc.)
275
- * 2. One built-in backend (InMemory) — zero extra dependencies
276
- * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
277
- * implementation into the BackendInstance interface
278
- *
279
- * Applications (like zen-fs-config-admin) register whatever backends
280
- * they need at startup. Adding a new backend never requires changing
281
- * zen-fs-config itself.
282
- */
283
-
284
- type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
285
- /**
286
- * The minimal interface a backend instance must satisfy.
287
- * Matches zen-fs-cache's CacheableFileSystem requirements.
288
- */
289
- interface BackendInstance {
290
- readFile(path: string, ...args: any[]): Promise<any>;
291
- writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
292
- readdir(path: string): Promise<string[]>;
293
- stat(path: string, ...args: any[]): Promise<any>;
294
- exists(path: string): Promise<boolean>;
295
- mkdir(path: string, options?: any): Promise<any>;
296
- unlink(path: string): Promise<void>;
297
- rmdir(path: string): Promise<void>;
298
- rename?(oldPath: string, newPath: string): Promise<void>;
299
- readFileMeta?(path: string, opts?: any): Promise<any>;
300
- getRevision?(path: string): Promise<string | number | undefined>;
301
- }
302
- declare function registerBackend(type: string, factory: BackendFactory): void;
303
- declare function unregisterBackend(type: string): boolean;
304
- declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
305
- declare function hasBackend(type: string): boolean;
306
- declare function listBackends(): string[];
307
- declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
308
-
309
340
  /**
310
341
  * zen-fs-config — Sidecar Version File Management
311
342
  *
@@ -348,4 +379,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
348
379
  */
349
380
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
350
381
 
351
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
382
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type TombstoneMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,17 @@ interface VersionMeta {
29
29
  /** Timestamp when the version was created. */
30
30
  timestamp: number;
31
31
  }
32
+ /** Content of a tombstone file in `.meta/.deleted/`. */
33
+ interface TombstoneMeta {
34
+ /** The deleted file path. */
35
+ path: string;
36
+ /** Timestamp of deletion. */
37
+ deletedAt: number;
38
+ /** Backend ID that initiated the deletion. */
39
+ deletedBy: string;
40
+ /** Backend IDs that have confirmed the deletion (synced). */
41
+ confirmedBy: string[];
42
+ }
32
43
  /** Content of a conflict archive file in `.meta/.conflicts/`. */
33
44
  interface ConflictArchive {
34
45
  /** The config file path that conflicted. */
@@ -146,6 +157,12 @@ interface IConfigRepo {
146
157
  getBackends(): Promise<BackendsMeta | null>;
147
158
  /** Write .meta/backends.json. */
148
159
  updateBackends(meta: BackendsMeta): Promise<void>;
160
+ /**
161
+ * Delete a file and record a tombstone for cross-backend sync.
162
+ * The tombstone ensures the deletion propagates to all backends
163
+ * instead of being treated as a "missing file" that gets re-created.
164
+ */
165
+ deleteFile(path: string): Promise<void>;
149
166
  /**
150
167
  * Sync .meta/ files (backends.json) to all replica backends.
151
168
  * Called automatically by createConfigRepo() after setupSync().
@@ -191,21 +208,54 @@ declare function configKeyToFilePath(configPath: string): string;
191
208
  declare function getExtension(path: string): string;
192
209
 
193
210
  /**
194
- * zen-fs-config — ConfigRepo Implementation
211
+ * zen-fs-config — Backend Registry
195
212
  *
196
- * Core implementation of IConfigRepo and the createConfigRepo factory.
213
+ * A pluggable registry that maps backend type names to factory functions.
214
+ *
215
+ * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
216
+ * Instead, it provides:
217
+ * 1. A simple registry API (registerBackend, createBackend, etc.)
218
+ * 2. One built-in backend (InMemory) — zero extra dependencies
219
+ * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
220
+ * implementation into the BackendInstance interface
221
+ *
222
+ * Applications (like zen-fs-config-admin) register whatever backends
223
+ * they need at startup. Adding a new backend never requires changing
224
+ * zen-fs-config itself.
197
225
  */
198
226
 
199
- interface MinimalAsyncFS {
227
+ type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
228
+ /**
229
+ * The minimal interface a backend instance must satisfy.
230
+ * Matches zen-fs-cache's CacheableFileSystem requirements.
231
+ */
232
+ interface BackendInstance {
200
233
  readFile(path: string, ...args: any[]): Promise<any>;
201
- writeFile(path: string, data: any, options?: any): Promise<void>;
234
+ writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
202
235
  readdir(path: string): Promise<string[]>;
203
- stat(path: string): Promise<any>;
236
+ stat(path: string, ...args: any[]): Promise<any>;
204
237
  exists(path: string): Promise<boolean>;
205
238
  mkdir(path: string, options?: any): Promise<any>;
206
239
  unlink(path: string): Promise<void>;
207
- rmdir?(path: string): Promise<void>;
240
+ rmdir(path: string): Promise<void>;
208
241
  rename?(oldPath: string, newPath: string): Promise<void>;
242
+ readFileMeta?(path: string, opts?: any): Promise<any>;
243
+ getRevision?(path: string): Promise<string | number | undefined>;
244
+ }
245
+ declare function registerBackend(type: string, factory: BackendFactory): void;
246
+ declare function unregisterBackend(type: string): boolean;
247
+ declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
248
+ declare function hasBackend(type: string): boolean;
249
+ declare function listBackends(): string[];
250
+ declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
251
+
252
+ /**
253
+ * zen-fs-config — ConfigRepo Implementation
254
+ *
255
+ * Core implementation of IConfigRepo and the createConfigRepo factory.
256
+ */
257
+
258
+ interface MinimalAsyncFS extends BackendInstance {
209
259
  }
210
260
  declare class ConfigRepo implements IConfigRepo {
211
261
  readonly appId: string;
@@ -222,7 +272,8 @@ declare class ConfigRepo implements IConfigRepo {
222
272
  private onConflictCallback?;
223
273
  private disposed;
224
274
  private configCache;
225
- constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
275
+ private readonly primaryBackendId;
276
+ constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
226
277
  /** Full path to this node's directory on the primary backend. */
227
278
  get nodePath(): string;
228
279
  load(rawConfig?: string): Promise<void>;
@@ -235,6 +286,28 @@ declare class ConfigRepo implements IConfigRepo {
235
286
  }): Promise<SyncResult>;
236
287
  peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
237
288
  flush(): Promise<SyncResult[]>;
289
+ /**
290
+ * Delete a file and write a tombstone so the deletion propagates
291
+ * to all backends instead of being treated as "missing file → re-create".
292
+ */
293
+ deleteFile(path: string): Promise<void>;
294
+ /**
295
+ * Read all tombstones from the primary backend.
296
+ */
297
+ private readTombstones;
298
+ /**
299
+ * Before sync: for each tombstone, delete the actual file on all replicas.
300
+ * This prevents bi-directional sync from copying the file back.
301
+ */
302
+ private processTombstones;
303
+ /**
304
+ * After sync: mark each tombstone as confirmed by all replica backends.
305
+ */
306
+ private updateTombstoneConfirmations;
307
+ /**
308
+ * GC: remove tombstones where all backends in backends.json have confirmed.
309
+ */
310
+ private gcTombstones;
238
311
  /**
239
312
  * Sync .meta/ files (backends.json) to all replica backends.
240
313
  *
@@ -264,48 +337,6 @@ declare class ConfigRepo implements IConfigRepo {
264
337
  }
265
338
  declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Promise<IConfigRepo>;
266
339
 
267
- /**
268
- * zen-fs-config — Backend Registry
269
- *
270
- * A pluggable registry that maps backend type names to factory functions.
271
- *
272
- * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
273
- * Instead, it provides:
274
- * 1. A simple registry API (registerBackend, createBackend, etc.)
275
- * 2. One built-in backend (InMemory) — zero extra dependencies
276
- * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
277
- * implementation into the BackendInstance interface
278
- *
279
- * Applications (like zen-fs-config-admin) register whatever backends
280
- * they need at startup. Adding a new backend never requires changing
281
- * zen-fs-config itself.
282
- */
283
-
284
- type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
285
- /**
286
- * The minimal interface a backend instance must satisfy.
287
- * Matches zen-fs-cache's CacheableFileSystem requirements.
288
- */
289
- interface BackendInstance {
290
- readFile(path: string, ...args: any[]): Promise<any>;
291
- writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
292
- readdir(path: string): Promise<string[]>;
293
- stat(path: string, ...args: any[]): Promise<any>;
294
- exists(path: string): Promise<boolean>;
295
- mkdir(path: string, options?: any): Promise<any>;
296
- unlink(path: string): Promise<void>;
297
- rmdir(path: string): Promise<void>;
298
- rename?(oldPath: string, newPath: string): Promise<void>;
299
- readFileMeta?(path: string, opts?: any): Promise<any>;
300
- getRevision?(path: string): Promise<string | number | undefined>;
301
- }
302
- declare function registerBackend(type: string, factory: BackendFactory): void;
303
- declare function unregisterBackend(type: string): boolean;
304
- declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
305
- declare function hasBackend(type: string): boolean;
306
- declare function listBackends(): string[];
307
- declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
308
-
309
340
  /**
310
341
  * zen-fs-config — Sidecar Version File Management
311
342
  *
@@ -348,4 +379,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
348
379
  */
349
380
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
350
381
 
351
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
382
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type TombstoneMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.js CHANGED
@@ -225,8 +225,8 @@ async function ensureParentDir(absolutePath) {
225
225
  }
226
226
 
227
227
  // src/adapters.ts
228
- function backendToSyncableFS(backend) {
229
- return {
228
+ function backendToSyncableFS(backend, name) {
229
+ const syncable = {
230
230
  async readdir(path) {
231
231
  return backend.readdir(path);
232
232
  },
@@ -252,8 +252,7 @@ function backendToSyncableFS(backend) {
252
252
  async stat(path) {
253
253
  const s = await backend.stat(path);
254
254
  return {
255
- isFile: typeof s.isFile === "function" ? () => s.isFile() : () => !!(s.mode && !(s.mode & 16384)),
256
- isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
255
+ mode: typeof s.mode === "number" ? s.mode : void 0,
257
256
  size: s.size ?? 0,
258
257
  mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
259
258
  };
@@ -265,47 +264,14 @@ function backendToSyncableFS(backend) {
265
264
  return backend.exists(path);
266
265
  }
267
266
  };
268
- }
269
- function cachedFSToSyncableFS(cached) {
270
- return {
271
- async readdir(path) {
272
- return cached.readdir(path);
273
- },
274
- async readFile(path, encoding) {
275
- const data = await cached.readFile(path);
276
- if (encoding) {
277
- if (typeof data === "string") return data;
278
- return new TextDecoder().decode(
279
- data instanceof ArrayBuffer ? new Uint8Array(data) : data
280
- );
281
- }
282
- if (typeof data === "string") return Buffer.from(data);
283
- if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
284
- return Buffer.from(data);
285
- },
286
- async writeFile(path, data) {
287
- return cached.writeFile(path, data);
288
- },
289
- async unlink(path) {
290
- return cached.unlink(path);
291
- },
292
- async stat(path) {
293
- const s = await cached.stat(path);
294
- const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
295
- return {
296
- isFile: () => !isDir,
297
- isDirectory: () => isDir,
298
- size: s.size,
299
- mtimeMs: s.mtimeMs ?? s.mtime
300
- };
301
- },
302
- async mkdir(path, options) {
303
- return cached.mkdir(path, options);
304
- },
305
- async exists(path) {
306
- return cached.exists(path);
307
- }
308
- };
267
+ if (name) {
268
+ syncable.backendName = name;
269
+ } else if (backend.backendName) {
270
+ syncable.backendName = backend.backendName;
271
+ } else {
272
+ syncable.backendName = backend.constructor.name || "Backend";
273
+ }
274
+ return syncable;
309
275
  }
310
276
 
311
277
  // src/backend-registry.ts
@@ -365,12 +331,10 @@ async function wrapZenFSFileSystem(config) {
365
331
  },
366
332
  async stat(path, ..._args) {
367
333
  const st = await isolatedFS.stat(path);
368
- const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
369
334
  return {
370
- isFile: () => !isDir,
371
- isDirectory: () => isDir,
335
+ mode: typeof st.mode === "number" ? st.mode : void 0,
372
336
  size: st.size,
373
- mtime: st.mtimeMs ?? st.mtime
337
+ mtimeMs: st.mtimeMs ?? st.mtime ?? 0
374
338
  };
375
339
  },
376
340
  async exists(path) {
@@ -485,8 +449,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
485
449
  var META_DIR = "/.meta";
486
450
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
487
451
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
452
+ var DELETIONS_DIR = `${META_DIR}/.deleted`;
488
453
  var NODES_DIR = "/nodes";
489
454
  var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
455
+ function tombstoneFileName(filePath) {
456
+ return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
457
+ }
490
458
  var ConfigRepo = class {
491
459
  appId;
492
460
  nodeId;
@@ -502,15 +470,17 @@ var ConfigRepo = class {
502
470
  onConflictCallback;
503
471
  disposed = false;
504
472
  configCache = /* @__PURE__ */ new Map();
505
- constructor(appId, nodeId, cachedFS, serializer, onConflict) {
473
+ primaryBackendId;
474
+ constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
506
475
  this.appId = appId;
507
476
  this.nodeId = nodeId;
477
+ this.primaryBackendId = primaryBackendId;
508
478
  this.cachedFS = cachedFS;
509
479
  this.serializer = serializer;
510
480
  this.syncEngine = new import_zen_fs_sync.ZenFSSync();
511
481
  this.replicaBackends = /* @__PURE__ */ new Map();
512
482
  this.onConflictCallback = onConflict;
513
- this.fullFS = cachedFSToSyncableFS(cachedFS);
483
+ this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
514
484
  this.fs = createChrootFS(cachedFS, `/${appId}`);
515
485
  this.rootFS = createChrootFS(cachedFS, "/");
516
486
  }
@@ -654,9 +624,141 @@ var ConfigRepo = class {
654
624
  // -----------------------------------------------------------------------
655
625
  async flush() {
656
626
  this.assertNotDisposed();
627
+ await this.processTombstones();
657
628
  const resultsMap = await this.syncEngine.syncAll();
629
+ await this.updateTombstoneConfirmations();
630
+ await this.gcTombstones();
658
631
  return Array.from(resultsMap.values());
659
632
  }
633
+ // -----------------------------------------------------------------------
634
+ // Tombstone (Deletion Tracking)
635
+ // -----------------------------------------------------------------------
636
+ /**
637
+ * Delete a file and write a tombstone so the deletion propagates
638
+ * to all backends instead of being treated as "missing file → re-create".
639
+ */
640
+ async deleteFile(path) {
641
+ this.assertNotDisposed();
642
+ const normalizedPath = path.startsWith("/") ? path : "/" + path;
643
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
644
+ const tombstone = {
645
+ path: normalizedPath,
646
+ deletedAt: Date.now(),
647
+ deletedBy: this.primaryBackendId,
648
+ confirmedBy: [this.primaryBackendId]
649
+ };
650
+ await this.ensureDir(tombstonePath);
651
+ await this.cachedFS.writeFile(
652
+ tombstonePath,
653
+ new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
654
+ );
655
+ try {
656
+ await this.cachedFS.unlink(normalizedPath);
657
+ } catch {
658
+ }
659
+ const versionPath = versionPathFor(normalizedPath);
660
+ try {
661
+ await this.cachedFS.unlink(versionPath);
662
+ } catch {
663
+ }
664
+ console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
665
+ }
666
+ /**
667
+ * Read all tombstones from the primary backend.
668
+ */
669
+ async readTombstones() {
670
+ try {
671
+ const entries = await this.cachedFS.readdir(DELETIONS_DIR);
672
+ const tombstones = [];
673
+ for (const entry of entries) {
674
+ if (!entry.endsWith(".json")) continue;
675
+ try {
676
+ const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
677
+ const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
678
+ tombstones.push(data);
679
+ } catch {
680
+ }
681
+ }
682
+ return tombstones;
683
+ } catch {
684
+ return [];
685
+ }
686
+ }
687
+ /**
688
+ * Before sync: for each tombstone, delete the actual file on all replicas.
689
+ * This prevents bi-directional sync from copying the file back.
690
+ */
691
+ async processTombstones() {
692
+ const tombstones = await this.readTombstones();
693
+ if (tombstones.length === 0) return;
694
+ console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
695
+ for (const tombstone of tombstones) {
696
+ try {
697
+ await this.cachedFS.unlink(tombstone.path);
698
+ } catch {
699
+ }
700
+ try {
701
+ await this.cachedFS.unlink(versionPathFor(tombstone.path));
702
+ } catch {
703
+ }
704
+ for (const [replicaId, replica] of this.replicaBackends) {
705
+ try {
706
+ await replica.instance.unlink(tombstone.path);
707
+ } catch {
708
+ }
709
+ try {
710
+ await replica.instance.unlink(versionPathFor(tombstone.path));
711
+ } catch {
712
+ }
713
+ console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
714
+ }
715
+ }
716
+ }
717
+ /**
718
+ * After sync: mark each tombstone as confirmed by all replica backends.
719
+ */
720
+ async updateTombstoneConfirmations() {
721
+ const tombstones = await this.readTombstones();
722
+ if (tombstones.length === 0) return;
723
+ const backendsMeta = await this.getBackends();
724
+ const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
725
+ for (const tombstone of tombstones) {
726
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
727
+ for (const replicaId of this.replicaBackends.keys()) {
728
+ if (!tombstone.confirmedBy.includes(replicaId)) {
729
+ tombstone.confirmedBy.push(replicaId);
730
+ }
731
+ }
732
+ try {
733
+ await this.cachedFS.writeFile(
734
+ tombstonePath,
735
+ new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
736
+ );
737
+ } catch {
738
+ }
739
+ }
740
+ console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
741
+ }
742
+ /**
743
+ * GC: remove tombstones where all backends in backends.json have confirmed.
744
+ */
745
+ async gcTombstones() {
746
+ const tombstones = await this.readTombstones();
747
+ if (tombstones.length === 0) return;
748
+ const backendsMeta = await this.getBackends();
749
+ const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
750
+ for (const tombstone of tombstones) {
751
+ const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
752
+ if (allConfirmed) {
753
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
754
+ try {
755
+ await this.cachedFS.unlink(tombstonePath);
756
+ console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
757
+ } catch {
758
+ }
759
+ }
760
+ }
761
+ }
660
762
  /**
661
763
  * Sync .meta/ files (backends.json) to all replica backends.
662
764
  *
@@ -769,7 +871,7 @@ var ConfigRepo = class {
769
871
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
770
872
  try {
771
873
  const instance = await createBackend(desc);
772
- const syncable = backendToSyncableFS(instance);
874
+ const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
773
875
  this.replicaBackends.set(desc.id, { instance, syncable });
774
876
  console.log(`[ConfigRepo] Replica ${desc.id} created successfully`);
775
877
  } catch (err) {
@@ -986,21 +1088,7 @@ async function createConfigRepo(appId, options) {
986
1088
  type: options.backendInfo.type,
987
1089
  options: options.backendInfo.options
988
1090
  });
989
- const zenCache = await import("zen-fs-cache");
990
- let cacheStore;
991
- const storeType = options.cache?.storeType ?? "MemoryCacheStore";
992
- if (storeType === "IdbCacheStore") {
993
- cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
994
- } else {
995
- cacheStore = new zenCache.MemoryCacheStore();
996
- }
997
- const cachedFS = new zenCache.CachedFileSystem(
998
- primaryInstance,
999
- cacheStore,
1000
- {
1001
- ttlMs: options.cache?.ttlMs ?? 0
1002
- }
1003
- );
1091
+ const cachedFS = primaryInstance;
1004
1092
  try {
1005
1093
  const metaExists = await primaryInstance.exists(META_DIR);
1006
1094
  console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
@@ -1015,6 +1103,7 @@ async function createConfigRepo(appId, options) {
1015
1103
  const tempRepo = new ConfigRepo(
1016
1104
  appId,
1017
1105
  "",
1106
+ options.primaryBackendId,
1018
1107
  cachedFS,
1019
1108
  createSerializerChain(),
1020
1109
  void 0
@@ -1071,6 +1160,7 @@ async function createConfigRepo(appId, options) {
1071
1160
  const repo = new ConfigRepo(
1072
1161
  appId,
1073
1162
  nodeId,
1163
+ options.primaryBackendId,
1074
1164
  cachedFS,
1075
1165
  serializer,
1076
1166
  options.onConflict
package/dist/index.mjs CHANGED
@@ -176,8 +176,8 @@ async function ensureParentDir(absolutePath) {
176
176
  }
177
177
 
178
178
  // src/adapters.ts
179
- function backendToSyncableFS(backend) {
180
- return {
179
+ function backendToSyncableFS(backend, name) {
180
+ const syncable = {
181
181
  async readdir(path) {
182
182
  return backend.readdir(path);
183
183
  },
@@ -203,8 +203,7 @@ function backendToSyncableFS(backend) {
203
203
  async stat(path) {
204
204
  const s = await backend.stat(path);
205
205
  return {
206
- isFile: typeof s.isFile === "function" ? () => s.isFile() : () => !!(s.mode && !(s.mode & 16384)),
207
- isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
206
+ mode: typeof s.mode === "number" ? s.mode : void 0,
208
207
  size: s.size ?? 0,
209
208
  mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
210
209
  };
@@ -216,47 +215,14 @@ function backendToSyncableFS(backend) {
216
215
  return backend.exists(path);
217
216
  }
218
217
  };
219
- }
220
- function cachedFSToSyncableFS(cached) {
221
- return {
222
- async readdir(path) {
223
- return cached.readdir(path);
224
- },
225
- async readFile(path, encoding) {
226
- const data = await cached.readFile(path);
227
- if (encoding) {
228
- if (typeof data === "string") return data;
229
- return new TextDecoder().decode(
230
- data instanceof ArrayBuffer ? new Uint8Array(data) : data
231
- );
232
- }
233
- if (typeof data === "string") return Buffer.from(data);
234
- if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
235
- return Buffer.from(data);
236
- },
237
- async writeFile(path, data) {
238
- return cached.writeFile(path, data);
239
- },
240
- async unlink(path) {
241
- return cached.unlink(path);
242
- },
243
- async stat(path) {
244
- const s = await cached.stat(path);
245
- const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
246
- return {
247
- isFile: () => !isDir,
248
- isDirectory: () => isDir,
249
- size: s.size,
250
- mtimeMs: s.mtimeMs ?? s.mtime
251
- };
252
- },
253
- async mkdir(path, options) {
254
- return cached.mkdir(path, options);
255
- },
256
- async exists(path) {
257
- return cached.exists(path);
258
- }
259
- };
218
+ if (name) {
219
+ syncable.backendName = name;
220
+ } else if (backend.backendName) {
221
+ syncable.backendName = backend.backendName;
222
+ } else {
223
+ syncable.backendName = backend.constructor.name || "Backend";
224
+ }
225
+ return syncable;
260
226
  }
261
227
 
262
228
  // src/backend-registry.ts
@@ -316,12 +282,10 @@ async function wrapZenFSFileSystem(config) {
316
282
  },
317
283
  async stat(path, ..._args) {
318
284
  const st = await isolatedFS.stat(path);
319
- const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
320
285
  return {
321
- isFile: () => !isDir,
322
- isDirectory: () => isDir,
286
+ mode: typeof st.mode === "number" ? st.mode : void 0,
323
287
  size: st.size,
324
- mtime: st.mtimeMs ?? st.mtime
288
+ mtimeMs: st.mtimeMs ?? st.mtime ?? 0
325
289
  };
326
290
  },
327
291
  async exists(path) {
@@ -436,8 +400,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
436
400
  var META_DIR = "/.meta";
437
401
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
438
402
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
403
+ var DELETIONS_DIR = `${META_DIR}/.deleted`;
439
404
  var NODES_DIR = "/nodes";
440
405
  var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
406
+ function tombstoneFileName(filePath) {
407
+ return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
408
+ }
441
409
  var ConfigRepo = class {
442
410
  appId;
443
411
  nodeId;
@@ -453,15 +421,17 @@ var ConfigRepo = class {
453
421
  onConflictCallback;
454
422
  disposed = false;
455
423
  configCache = /* @__PURE__ */ new Map();
456
- constructor(appId, nodeId, cachedFS, serializer, onConflict) {
424
+ primaryBackendId;
425
+ constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
457
426
  this.appId = appId;
458
427
  this.nodeId = nodeId;
428
+ this.primaryBackendId = primaryBackendId;
459
429
  this.cachedFS = cachedFS;
460
430
  this.serializer = serializer;
461
431
  this.syncEngine = new ZenFSSync();
462
432
  this.replicaBackends = /* @__PURE__ */ new Map();
463
433
  this.onConflictCallback = onConflict;
464
- this.fullFS = cachedFSToSyncableFS(cachedFS);
434
+ this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
465
435
  this.fs = createChrootFS(cachedFS, `/${appId}`);
466
436
  this.rootFS = createChrootFS(cachedFS, "/");
467
437
  }
@@ -605,9 +575,141 @@ var ConfigRepo = class {
605
575
  // -----------------------------------------------------------------------
606
576
  async flush() {
607
577
  this.assertNotDisposed();
578
+ await this.processTombstones();
608
579
  const resultsMap = await this.syncEngine.syncAll();
580
+ await this.updateTombstoneConfirmations();
581
+ await this.gcTombstones();
609
582
  return Array.from(resultsMap.values());
610
583
  }
584
+ // -----------------------------------------------------------------------
585
+ // Tombstone (Deletion Tracking)
586
+ // -----------------------------------------------------------------------
587
+ /**
588
+ * Delete a file and write a tombstone so the deletion propagates
589
+ * to all backends instead of being treated as "missing file → re-create".
590
+ */
591
+ async deleteFile(path) {
592
+ this.assertNotDisposed();
593
+ const normalizedPath = path.startsWith("/") ? path : "/" + path;
594
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
595
+ const tombstone = {
596
+ path: normalizedPath,
597
+ deletedAt: Date.now(),
598
+ deletedBy: this.primaryBackendId,
599
+ confirmedBy: [this.primaryBackendId]
600
+ };
601
+ await this.ensureDir(tombstonePath);
602
+ await this.cachedFS.writeFile(
603
+ tombstonePath,
604
+ new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
605
+ );
606
+ try {
607
+ await this.cachedFS.unlink(normalizedPath);
608
+ } catch {
609
+ }
610
+ const versionPath = versionPathFor(normalizedPath);
611
+ try {
612
+ await this.cachedFS.unlink(versionPath);
613
+ } catch {
614
+ }
615
+ console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
616
+ }
617
+ /**
618
+ * Read all tombstones from the primary backend.
619
+ */
620
+ async readTombstones() {
621
+ try {
622
+ const entries = await this.cachedFS.readdir(DELETIONS_DIR);
623
+ const tombstones = [];
624
+ for (const entry of entries) {
625
+ if (!entry.endsWith(".json")) continue;
626
+ try {
627
+ const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
628
+ const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
629
+ tombstones.push(data);
630
+ } catch {
631
+ }
632
+ }
633
+ return tombstones;
634
+ } catch {
635
+ return [];
636
+ }
637
+ }
638
+ /**
639
+ * Before sync: for each tombstone, delete the actual file on all replicas.
640
+ * This prevents bi-directional sync from copying the file back.
641
+ */
642
+ async processTombstones() {
643
+ const tombstones = await this.readTombstones();
644
+ if (tombstones.length === 0) return;
645
+ console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
646
+ for (const tombstone of tombstones) {
647
+ try {
648
+ await this.cachedFS.unlink(tombstone.path);
649
+ } catch {
650
+ }
651
+ try {
652
+ await this.cachedFS.unlink(versionPathFor(tombstone.path));
653
+ } catch {
654
+ }
655
+ for (const [replicaId, replica] of this.replicaBackends) {
656
+ try {
657
+ await replica.instance.unlink(tombstone.path);
658
+ } catch {
659
+ }
660
+ try {
661
+ await replica.instance.unlink(versionPathFor(tombstone.path));
662
+ } catch {
663
+ }
664
+ console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
665
+ }
666
+ }
667
+ }
668
+ /**
669
+ * After sync: mark each tombstone as confirmed by all replica backends.
670
+ */
671
+ async updateTombstoneConfirmations() {
672
+ const tombstones = await this.readTombstones();
673
+ if (tombstones.length === 0) return;
674
+ const backendsMeta = await this.getBackends();
675
+ const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
676
+ for (const tombstone of tombstones) {
677
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
678
+ for (const replicaId of this.replicaBackends.keys()) {
679
+ if (!tombstone.confirmedBy.includes(replicaId)) {
680
+ tombstone.confirmedBy.push(replicaId);
681
+ }
682
+ }
683
+ try {
684
+ await this.cachedFS.writeFile(
685
+ tombstonePath,
686
+ new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
687
+ );
688
+ } catch {
689
+ }
690
+ }
691
+ console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
692
+ }
693
+ /**
694
+ * GC: remove tombstones where all backends in backends.json have confirmed.
695
+ */
696
+ async gcTombstones() {
697
+ const tombstones = await this.readTombstones();
698
+ if (tombstones.length === 0) return;
699
+ const backendsMeta = await this.getBackends();
700
+ const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
701
+ for (const tombstone of tombstones) {
702
+ const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
703
+ if (allConfirmed) {
704
+ const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
705
+ try {
706
+ await this.cachedFS.unlink(tombstonePath);
707
+ console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
708
+ } catch {
709
+ }
710
+ }
711
+ }
712
+ }
611
713
  /**
612
714
  * Sync .meta/ files (backends.json) to all replica backends.
613
715
  *
@@ -720,7 +822,7 @@ var ConfigRepo = class {
720
822
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
721
823
  try {
722
824
  const instance = await createBackend(desc);
723
- const syncable = backendToSyncableFS(instance);
825
+ const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
724
826
  this.replicaBackends.set(desc.id, { instance, syncable });
725
827
  console.log(`[ConfigRepo] Replica ${desc.id} created successfully`);
726
828
  } catch (err) {
@@ -937,21 +1039,7 @@ async function createConfigRepo(appId, options) {
937
1039
  type: options.backendInfo.type,
938
1040
  options: options.backendInfo.options
939
1041
  });
940
- const zenCache = await import("zen-fs-cache");
941
- let cacheStore;
942
- const storeType = options.cache?.storeType ?? "MemoryCacheStore";
943
- if (storeType === "IdbCacheStore") {
944
- cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
945
- } else {
946
- cacheStore = new zenCache.MemoryCacheStore();
947
- }
948
- const cachedFS = new zenCache.CachedFileSystem(
949
- primaryInstance,
950
- cacheStore,
951
- {
952
- ttlMs: options.cache?.ttlMs ?? 0
953
- }
954
- );
1042
+ const cachedFS = primaryInstance;
955
1043
  try {
956
1044
  const metaExists = await primaryInstance.exists(META_DIR);
957
1045
  console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
@@ -966,6 +1054,7 @@ async function createConfigRepo(appId, options) {
966
1054
  const tempRepo = new ConfigRepo(
967
1055
  appId,
968
1056
  "",
1057
+ options.primaryBackendId,
969
1058
  cachedFS,
970
1059
  createSerializerChain(),
971
1060
  void 0
@@ -1022,6 +1111,7 @@ async function createConfigRepo(appId, options) {
1022
1111
  const repo = new ConfigRepo(
1023
1112
  appId,
1024
1113
  nodeId,
1114
+ options.primaryBackendId,
1025
1115
  cachedFS,
1026
1116
  serializer,
1027
1117
  options.onConflict
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.3.24",
3
+ "version": "0.3.27",
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",
@@ -49,7 +49,6 @@
49
49
  "typescript": "^5.9.3",
50
50
  "vitest": "^1.6.1",
51
51
  "zen-fs-cache": "^1.0.1",
52
- "zen-fs-sync": "^0.1.0"
53
- },
54
- "dependencies": {}
52
+ "zen-fs-sync": "^0.2.2"
53
+ }
55
54
  }