zen-fs-config 0.5.30 → 0.5.32

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.
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-XLOBWTYJ.mjs";
12
+ } from "./chunk-UDEOYFWS.mjs";
13
13
  export {
14
14
  createBackend,
15
15
  getAccountFields,
@@ -184,15 +184,24 @@ var OPTIONAL_BACKEND_GLOBALS = {
184
184
  };
185
185
  async function importOptionalBackend(pkg) {
186
186
  const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
187
+ const fromGlobal = () => {
188
+ if (!candidates) return void 0;
189
+ for (const name of candidates) {
190
+ const g = globalThis[name];
191
+ if (g) return g;
192
+ }
193
+ return void 0;
194
+ };
187
195
  try {
188
- return await import(pkg);
189
- } catch {
190
- if (candidates) {
191
- for (const name of candidates) {
192
- const g = globalThis[name];
193
- if (g) return g;
194
- }
196
+ const mod = await import(pkg);
197
+ if (mod && Object.keys(mod).length === 0) {
198
+ const g = fromGlobal();
199
+ if (g) return g;
195
200
  }
201
+ return mod;
202
+ } catch {
203
+ const g = fromGlobal();
204
+ if (g) return g;
196
205
  const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
197
206
  throw new Error(
198
207
  `Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
@@ -200,7 +209,13 @@ async function importOptionalBackend(pkg) {
200
209
  }
201
210
  }
202
211
  registerBackend("GitHub", async (options) => {
203
- const { Github } = await importOptionalBackend("zen-fs-github");
212
+ const _gh = await importOptionalBackend("zen-fs-github");
213
+ const Github = _gh?.Github ?? _gh?.default;
214
+ if (typeof Github !== "function") {
215
+ throw new Error(
216
+ `Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
217
+ );
218
+ }
204
219
  const backend = await wrapZenFSFileSystem({
205
220
  backend: Github,
206
221
  token: options.token,
@@ -248,7 +263,13 @@ registerBackend("GitHub", async (options) => {
248
263
  accountFields: ["token", "owner", "baseUrl"]
249
264
  });
250
265
  registerBackend("Gitee", async (options) => {
251
- const { Gitee } = await importOptionalBackend("zen-fs-gitee");
266
+ const _gt = await importOptionalBackend("zen-fs-gitee");
267
+ const Gitee = _gt?.Gitee ?? _gt?.default;
268
+ if (typeof Gitee !== "function") {
269
+ throw new Error(
270
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
271
+ );
272
+ }
252
273
  return wrapZenFSFileSystem({
253
274
  backend: Gitee,
254
275
  token: options.token,
@@ -272,7 +293,13 @@ registerBackend("Gitee", async (options) => {
272
293
  accountFields: ["token", "owner", "baseUrl"]
273
294
  });
274
295
  registerBackend("RemoteStorage", async (options) => {
275
- const { RemoteStorageFileSystem } = await importOptionalBackend("zen-fs-remotestoragejs");
296
+ const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
297
+ const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
298
+ if (typeof RemoteStorageFileSystem !== "function") {
299
+ throw new Error(
300
+ `Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
301
+ );
302
+ }
276
303
  return new RemoteStorageFileSystem({
277
304
  href: options.href,
278
305
  token: options.token,
package/dist/index.d.mts CHANGED
@@ -4,6 +4,65 @@ import * as node_fs from 'node:fs';
4
4
  import * as node_path from 'node:path';
5
5
  import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
6
6
 
7
+ /**
8
+ * zen-fs-config — mtime sidecar cleanup
9
+ *
10
+ * Backends that cannot store a precise mtime natively (RemoteStorage, Gitee,
11
+ * …) persist it in a `.mtime` sidecar next to each file
12
+ * (`data.json` → `.data.json.mtime`). A sidecar is backend-internal metadata:
13
+ * it belongs to the backend that produced it and must never be handed out by
14
+ * `readdir()`/`createSnapshot()`.
15
+ *
16
+ * Older builds did leak them, and once a sidecar lands in the local primary
17
+ * (IndexedDB on browser, Folder on Node) it stays there forever — zen-fs-sync
18
+ * deliberately skips `.mtime` paths on both sides, so nothing ever removes
19
+ * them, and every walk logs:
20
+ *
21
+ * [zen-fs-sync] mtime sidecar leaked from backend "local-idb" …
22
+ *
23
+ * This module walks the local primary and deletes those leaked files. It never
24
+ * touches replica (remote) backends: their sidecars are live metadata, and
25
+ * deleting them would cost network calls and lose mtime precision.
26
+ */
27
+ /** Minimal async FS surface needed for the purge. */
28
+ interface PurgeableFS {
29
+ readdir(path: string): Promise<string[]>;
30
+ stat(path: string): Promise<{
31
+ mode?: number;
32
+ }>;
33
+ unlink(path: string): Promise<void>;
34
+ }
35
+ interface PurgeMtimeOptions {
36
+ /** Root to scan. Default: `/` (the whole local primary). */
37
+ root?: string;
38
+ /** Report what would be deleted without unlinking anything. */
39
+ dryRun?: boolean;
40
+ }
41
+ interface MtimePurgeResult {
42
+ /** Files visited during the walk (sidecars included). */
43
+ scanned: number;
44
+ /** Sidecars deleted — or, in dryRun, that would be deleted. */
45
+ removed: string[];
46
+ /** Sidecars that could not be deleted (unlink failed). */
47
+ failed: string[];
48
+ }
49
+ /**
50
+ * True when a file name is an mtime sidecar.
51
+ *
52
+ * Uses the same rule as zen-fs-sync's walker (`path.endsWith('.mtime')`), so
53
+ * the pathological nested form (`.foo.mtime.mtime`) is covered too.
54
+ */
55
+ declare function isMtimeSidecar(fileName: string): boolean;
56
+ /**
57
+ * Delete every `.mtime` sidecar stored in a local primary backend.
58
+ *
59
+ * Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
60
+ * because sidecars live next to their data file and are themselves dotfiles.
61
+ * Unreadable entries are skipped; failures on individual files are collected
62
+ * in `failed` instead of aborting the walk.
63
+ */
64
+ declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
65
+
7
66
  /** A single backend in the topology. */
8
67
  interface BackendDescriptor {
9
68
  /** Unique identifier within this config repo (e.g., "local-idb"). */
@@ -159,6 +218,17 @@ interface ConfigRepoOptions {
159
218
  * Default: 1800000 (30 minutes).
160
219
  */
161
220
  syncPollIntervalMs?: number;
221
+ /**
222
+ * Delete leaked `.mtime` sidecars from the local primary on startup.
223
+ *
224
+ * Backends that keep a precise mtime out-of-band (RemoteStorage, Gitee…)
225
+ * write a `.mtime` file next to each data file. Older builds copied those
226
+ * backend-internal files into the local primary, where sync ignores them —
227
+ * so they linger forever and warn on every walk.
228
+ *
229
+ * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
230
+ */
231
+ purgeMtimeSidecars?: boolean;
162
232
  }
163
233
  /** Type of sync group. */
164
234
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -340,6 +410,17 @@ interface IConfigRepo {
340
410
  * Called automatically by createConfigRepo() after setupSync().
341
411
  */
342
412
  syncMetaToReplicas(): Promise<void>;
413
+ /**
414
+ * Delete leaked `.mtime` sidecars from the local primary backend.
415
+ *
416
+ * Runs automatically on `createConfigRepo()` unless
417
+ * `options.purgeMtimeSidecars === false`. Call it manually to clean a
418
+ * long-lived store, or with `{ dryRun: true }` to only list them.
419
+ */
420
+ purgeMtimeSidecars(options?: {
421
+ root?: string;
422
+ dryRun?: boolean;
423
+ }): Promise<MtimePurgeResult>;
343
424
  /**
344
425
  * Create a data-sync group for this app.
345
426
  * Each backend can optionally reference a config-sync backend's account
@@ -593,6 +674,22 @@ declare class ConfigRepo implements IConfigRepo {
593
674
  private safeExists;
594
675
  /** Public wrapper for processTombstones — used by createConfigRepo. */
595
676
  processTombstonesPublic(): Promise<void>;
677
+ /**
678
+ * Delete leaked `.mtime` sidecar files from the local primary backend
679
+ * (IndexedDB on browser, Folder on Node).
680
+ *
681
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
682
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
683
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
684
+ * invisible to sync, and every walk warns about the leak.
685
+ *
686
+ * Only the local primary is scanned — replica sidecars are live metadata of
687
+ * the backend that owns them and must be left alone.
688
+ *
689
+ * @param options.root Scan only this subtree (default `/`).
690
+ * @param options.dryRun List the sidecars without deleting them.
691
+ */
692
+ purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
596
693
  /**
597
694
  * Perform a full sync + dedup cycle without the watch snapshot cache.
598
695
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -826,4 +923,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
826
923
  */
827
924
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
828
925
 
829
- export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
926
+ export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,65 @@ import * as node_fs from 'node:fs';
4
4
  import * as node_path from 'node:path';
5
5
  import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
6
6
 
7
+ /**
8
+ * zen-fs-config — mtime sidecar cleanup
9
+ *
10
+ * Backends that cannot store a precise mtime natively (RemoteStorage, Gitee,
11
+ * …) persist it in a `.mtime` sidecar next to each file
12
+ * (`data.json` → `.data.json.mtime`). A sidecar is backend-internal metadata:
13
+ * it belongs to the backend that produced it and must never be handed out by
14
+ * `readdir()`/`createSnapshot()`.
15
+ *
16
+ * Older builds did leak them, and once a sidecar lands in the local primary
17
+ * (IndexedDB on browser, Folder on Node) it stays there forever — zen-fs-sync
18
+ * deliberately skips `.mtime` paths on both sides, so nothing ever removes
19
+ * them, and every walk logs:
20
+ *
21
+ * [zen-fs-sync] mtime sidecar leaked from backend "local-idb" …
22
+ *
23
+ * This module walks the local primary and deletes those leaked files. It never
24
+ * touches replica (remote) backends: their sidecars are live metadata, and
25
+ * deleting them would cost network calls and lose mtime precision.
26
+ */
27
+ /** Minimal async FS surface needed for the purge. */
28
+ interface PurgeableFS {
29
+ readdir(path: string): Promise<string[]>;
30
+ stat(path: string): Promise<{
31
+ mode?: number;
32
+ }>;
33
+ unlink(path: string): Promise<void>;
34
+ }
35
+ interface PurgeMtimeOptions {
36
+ /** Root to scan. Default: `/` (the whole local primary). */
37
+ root?: string;
38
+ /** Report what would be deleted without unlinking anything. */
39
+ dryRun?: boolean;
40
+ }
41
+ interface MtimePurgeResult {
42
+ /** Files visited during the walk (sidecars included). */
43
+ scanned: number;
44
+ /** Sidecars deleted — or, in dryRun, that would be deleted. */
45
+ removed: string[];
46
+ /** Sidecars that could not be deleted (unlink failed). */
47
+ failed: string[];
48
+ }
49
+ /**
50
+ * True when a file name is an mtime sidecar.
51
+ *
52
+ * Uses the same rule as zen-fs-sync's walker (`path.endsWith('.mtime')`), so
53
+ * the pathological nested form (`.foo.mtime.mtime`) is covered too.
54
+ */
55
+ declare function isMtimeSidecar(fileName: string): boolean;
56
+ /**
57
+ * Delete every `.mtime` sidecar stored in a local primary backend.
58
+ *
59
+ * Walks the whole tree from `options.root` — including dotfiles and `/.meta/`,
60
+ * because sidecars live next to their data file and are themselves dotfiles.
61
+ * Unreadable entries are skipped; failures on individual files are collected
62
+ * in `failed` instead of aborting the walk.
63
+ */
64
+ declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
65
+
7
66
  /** A single backend in the topology. */
8
67
  interface BackendDescriptor {
9
68
  /** Unique identifier within this config repo (e.g., "local-idb"). */
@@ -159,6 +218,17 @@ interface ConfigRepoOptions {
159
218
  * Default: 1800000 (30 minutes).
160
219
  */
161
220
  syncPollIntervalMs?: number;
221
+ /**
222
+ * Delete leaked `.mtime` sidecars from the local primary on startup.
223
+ *
224
+ * Backends that keep a precise mtime out-of-band (RemoteStorage, Gitee…)
225
+ * write a `.mtime` file next to each data file. Older builds copied those
226
+ * backend-internal files into the local primary, where sync ignores them —
227
+ * so they linger forever and warn on every walk.
228
+ *
229
+ * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
230
+ */
231
+ purgeMtimeSidecars?: boolean;
162
232
  }
163
233
  /** Type of sync group. */
164
234
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -340,6 +410,17 @@ interface IConfigRepo {
340
410
  * Called automatically by createConfigRepo() after setupSync().
341
411
  */
342
412
  syncMetaToReplicas(): Promise<void>;
413
+ /**
414
+ * Delete leaked `.mtime` sidecars from the local primary backend.
415
+ *
416
+ * Runs automatically on `createConfigRepo()` unless
417
+ * `options.purgeMtimeSidecars === false`. Call it manually to clean a
418
+ * long-lived store, or with `{ dryRun: true }` to only list them.
419
+ */
420
+ purgeMtimeSidecars(options?: {
421
+ root?: string;
422
+ dryRun?: boolean;
423
+ }): Promise<MtimePurgeResult>;
343
424
  /**
344
425
  * Create a data-sync group for this app.
345
426
  * Each backend can optionally reference a config-sync backend's account
@@ -593,6 +674,22 @@ declare class ConfigRepo implements IConfigRepo {
593
674
  private safeExists;
594
675
  /** Public wrapper for processTombstones — used by createConfigRepo. */
595
676
  processTombstonesPublic(): Promise<void>;
677
+ /**
678
+ * Delete leaked `.mtime` sidecar files from the local primary backend
679
+ * (IndexedDB on browser, Folder on Node).
680
+ *
681
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
682
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
683
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
684
+ * invisible to sync, and every walk warns about the leak.
685
+ *
686
+ * Only the local primary is scanned — replica sidecars are live metadata of
687
+ * the backend that owns them and must be left alone.
688
+ *
689
+ * @param options.root Scan only this subtree (default `/`).
690
+ * @param options.dryRun List the sidecars without deleting them.
691
+ */
692
+ purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
596
693
  /**
597
694
  * Perform a full sync + dedup cycle without the watch snapshot cache.
598
695
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -826,4 +923,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
826
923
  */
827
924
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
828
925
 
829
- export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
926
+ export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, LOCAL_IDB_BACKEND_ID, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.js CHANGED
@@ -187,15 +187,24 @@ function _cacheSet(key, value) {
187
187
  }
188
188
  async function importOptionalBackend(pkg) {
189
189
  const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
190
+ const fromGlobal = () => {
191
+ if (!candidates) return void 0;
192
+ for (const name of candidates) {
193
+ const g = globalThis[name];
194
+ if (g) return g;
195
+ }
196
+ return void 0;
197
+ };
190
198
  try {
191
- return await import(pkg);
192
- } catch {
193
- if (candidates) {
194
- for (const name of candidates) {
195
- const g = globalThis[name];
196
- if (g) return g;
197
- }
199
+ const mod = await import(pkg);
200
+ if (mod && Object.keys(mod).length === 0) {
201
+ const g = fromGlobal();
202
+ if (g) return g;
198
203
  }
204
+ return mod;
205
+ } catch {
206
+ const g = fromGlobal();
207
+ if (g) return g;
199
208
  const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
200
209
  throw new Error(
201
210
  `Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
@@ -249,7 +258,13 @@ var init_backend_registry = __esm({
249
258
  "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
250
259
  };
251
260
  registerBackend("GitHub", async (options) => {
252
- const { Github } = await importOptionalBackend("zen-fs-github");
261
+ const _gh = await importOptionalBackend("zen-fs-github");
262
+ const Github = _gh?.Github ?? _gh?.default;
263
+ if (typeof Github !== "function") {
264
+ throw new Error(
265
+ `Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
266
+ );
267
+ }
253
268
  const backend = await wrapZenFSFileSystem({
254
269
  backend: Github,
255
270
  token: options.token,
@@ -297,7 +312,13 @@ var init_backend_registry = __esm({
297
312
  accountFields: ["token", "owner", "baseUrl"]
298
313
  });
299
314
  registerBackend("Gitee", async (options) => {
300
- const { Gitee } = await importOptionalBackend("zen-fs-gitee");
315
+ const _gt = await importOptionalBackend("zen-fs-gitee");
316
+ const Gitee = _gt?.Gitee ?? _gt?.default;
317
+ if (typeof Gitee !== "function") {
318
+ throw new Error(
319
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
320
+ );
321
+ }
301
322
  return wrapZenFSFileSystem({
302
323
  backend: Gitee,
303
324
  token: options.token,
@@ -321,7 +342,13 @@ var init_backend_registry = __esm({
321
342
  accountFields: ["token", "owner", "baseUrl"]
322
343
  });
323
344
  registerBackend("RemoteStorage", async (options) => {
324
- const { RemoteStorageFileSystem } = await importOptionalBackend("zen-fs-remotestoragejs");
345
+ const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
346
+ const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
347
+ if (typeof RemoteStorageFileSystem !== "function") {
348
+ throw new Error(
349
+ `Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
350
+ );
351
+ }
325
352
  return new RemoteStorageFileSystem({
326
353
  href: options.href,
327
354
  token: options.token,
@@ -391,10 +418,12 @@ __export(index_exports, {
391
418
  hasBackend: () => hasBackend,
392
419
  incrementVersion: () => incrementVersion,
393
420
  isBrowserEnv: () => isBrowserEnv,
421
+ isMtimeSidecar: () => isMtimeSidecar,
394
422
  listBackendMetadata: () => listBackendMetadata,
395
423
  listBackends: () => listBackends,
396
424
  localPrimaryType: () => localPrimaryType,
397
425
  mergeAccountFields: () => mergeAccountFields,
426
+ purgeMtimeSidecars: () => purgeMtimeSidecars,
398
427
  readVersion: () => readVersion,
399
428
  registerBackend: () => registerBackend,
400
429
  registerFolderBackend: () => registerFolderBackend,
@@ -1011,6 +1040,60 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
1011
1040
  }
1012
1041
  }
1013
1042
 
1043
+ // src/mtime-cleanup.ts
1044
+ function isMtimeSidecar(fileName) {
1045
+ return fileName.endsWith(".mtime");
1046
+ }
1047
+ var S_IFDIR = 16384;
1048
+ function isDirectory(stat) {
1049
+ return typeof stat.mode === "number" && (stat.mode & S_IFDIR) === S_IFDIR;
1050
+ }
1051
+ function joinPath(dir, entry) {
1052
+ if (dir === "/" || dir === "") return `/${entry}`;
1053
+ return `${dir.replace(/\/+$/, "")}/${entry}`;
1054
+ }
1055
+ async function purgeMtimeSidecars(fs, options = {}) {
1056
+ const root = options.root && options.root !== "" ? options.root : "/";
1057
+ const dryRun = options.dryRun ?? false;
1058
+ const result = { scanned: 0, removed: [], failed: [] };
1059
+ const stack = [root];
1060
+ while (stack.length > 0) {
1061
+ const dir = stack.pop();
1062
+ let entries;
1063
+ try {
1064
+ entries = await fs.readdir(dir);
1065
+ } catch {
1066
+ continue;
1067
+ }
1068
+ for (const entry of entries) {
1069
+ const fullPath = joinPath(dir, entry);
1070
+ let stat;
1071
+ try {
1072
+ stat = await fs.stat(fullPath);
1073
+ } catch {
1074
+ continue;
1075
+ }
1076
+ if (isDirectory(stat)) {
1077
+ stack.push(fullPath);
1078
+ continue;
1079
+ }
1080
+ result.scanned++;
1081
+ if (!isMtimeSidecar(entry)) continue;
1082
+ if (dryRun) {
1083
+ result.removed.push(fullPath);
1084
+ continue;
1085
+ }
1086
+ try {
1087
+ await fs.unlink(fullPath);
1088
+ result.removed.push(fullPath);
1089
+ } catch {
1090
+ result.failed.push(fullPath);
1091
+ }
1092
+ }
1093
+ }
1094
+ return result;
1095
+ }
1096
+
1014
1097
  // src/config-repo.ts
1015
1098
  var log2 = (0, import_localstorage_logger2.createLogger)("zen-fs-config:config-repo");
1016
1099
  var META_DIR = "/.meta";
@@ -1464,6 +1547,35 @@ var ConfigRepo = class {
1464
1547
  async processTombstonesPublic() {
1465
1548
  await this.processTombstones();
1466
1549
  }
1550
+ /**
1551
+ * Delete leaked `.mtime` sidecar files from the local primary backend
1552
+ * (IndexedDB on browser, Folder on Node).
1553
+ *
1554
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
1555
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
1556
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
1557
+ * invisible to sync, and every walk warns about the leak.
1558
+ *
1559
+ * Only the local primary is scanned — replica sidecars are live metadata of
1560
+ * the backend that owns them and must be left alone.
1561
+ *
1562
+ * @param options.root Scan only this subtree (default `/`).
1563
+ * @param options.dryRun List the sidecars without deleting them.
1564
+ */
1565
+ async purgeMtimeSidecars(options) {
1566
+ this.assertNotDisposed();
1567
+ const result = await purgeMtimeSidecars(this.cachedFS, options);
1568
+ if (result.removed.length > 0) {
1569
+ log2.log(
1570
+ `[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
1571
+ result.removed
1572
+ );
1573
+ }
1574
+ if (result.failed.length > 0) {
1575
+ log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
1576
+ }
1577
+ return result;
1578
+ }
1467
1579
  /**
1468
1580
  * Perform a full sync + dedup cycle without the watch snapshot cache.
1469
1581
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -2385,6 +2497,17 @@ var AppDataGroupImpl = class {
2385
2497
  } catch {
2386
2498
  throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
2387
2499
  }
2500
+ try {
2501
+ const purged = await purgeMtimeSidecars(this.localFS);
2502
+ if (purged.removed.length > 0) {
2503
+ log2.warn(
2504
+ `[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
2505
+ purged.removed
2506
+ );
2507
+ }
2508
+ } catch (err) {
2509
+ log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
2510
+ }
2388
2511
  const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
2389
2512
  this.fs = createChrootFS(this.localFS, "/");
2390
2513
  const { kept, removed } = dedupeAppDataBackends(this._backends);
@@ -2511,6 +2634,19 @@ async function createConfigRepo(appId, options = {}) {
2511
2634
  log2.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
2512
2635
  const primaryInstance = await createBackend(localPrimary);
2513
2636
  const cachedFS = primaryInstance;
2637
+ if (options.purgeMtimeSidecars !== false) {
2638
+ try {
2639
+ const purged = await purgeMtimeSidecars(cachedFS);
2640
+ if (purged.removed.length > 0) {
2641
+ log2.warn(
2642
+ `[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
2643
+ purged.removed
2644
+ );
2645
+ }
2646
+ } catch (err) {
2647
+ log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
2648
+ }
2649
+ }
2514
2650
  const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
2515
2651
  try {
2516
2652
  await primaryInstance.mkdir(META_DIR);
@@ -2745,10 +2881,12 @@ init_backend_registry();
2745
2881
  hasBackend,
2746
2882
  incrementVersion,
2747
2883
  isBrowserEnv,
2884
+ isMtimeSidecar,
2748
2885
  listBackendMetadata,
2749
2886
  listBackends,
2750
2887
  localPrimaryType,
2751
2888
  mergeAccountFields,
2889
+ purgeMtimeSidecars,
2752
2890
  readVersion,
2753
2891
  registerBackend,
2754
2892
  registerFolderBackend,
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-XLOBWTYJ.mjs";
12
+ } from "./chunk-UDEOYFWS.mjs";
13
13
 
14
14
  // src/config-repo.ts
15
15
  import {
@@ -351,7 +351,7 @@ function registerFolderBackend() {
351
351
  const pathMod = await import("path");
352
352
  const dir = options.path;
353
353
  const store = new FolderStore({ path: dir, label: options.label, fs, pathMod });
354
- return (await import("./backend-registry-7OTQSRXY.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
354
+ return (await import("./backend-registry-7QGFWLM7.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
355
355
  },
356
356
  {
357
357
  type: "Folder",
@@ -613,6 +613,60 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
613
613
  }
614
614
  }
615
615
 
616
+ // src/mtime-cleanup.ts
617
+ function isMtimeSidecar(fileName) {
618
+ return fileName.endsWith(".mtime");
619
+ }
620
+ var S_IFDIR = 16384;
621
+ function isDirectory(stat) {
622
+ return typeof stat.mode === "number" && (stat.mode & S_IFDIR) === S_IFDIR;
623
+ }
624
+ function joinPath(dir, entry) {
625
+ if (dir === "/" || dir === "") return `/${entry}`;
626
+ return `${dir.replace(/\/+$/, "")}/${entry}`;
627
+ }
628
+ async function purgeMtimeSidecars(fs, options = {}) {
629
+ const root = options.root && options.root !== "" ? options.root : "/";
630
+ const dryRun = options.dryRun ?? false;
631
+ const result = { scanned: 0, removed: [], failed: [] };
632
+ const stack = [root];
633
+ while (stack.length > 0) {
634
+ const dir = stack.pop();
635
+ let entries;
636
+ try {
637
+ entries = await fs.readdir(dir);
638
+ } catch {
639
+ continue;
640
+ }
641
+ for (const entry of entries) {
642
+ const fullPath = joinPath(dir, entry);
643
+ let stat;
644
+ try {
645
+ stat = await fs.stat(fullPath);
646
+ } catch {
647
+ continue;
648
+ }
649
+ if (isDirectory(stat)) {
650
+ stack.push(fullPath);
651
+ continue;
652
+ }
653
+ result.scanned++;
654
+ if (!isMtimeSidecar(entry)) continue;
655
+ if (dryRun) {
656
+ result.removed.push(fullPath);
657
+ continue;
658
+ }
659
+ try {
660
+ await fs.unlink(fullPath);
661
+ result.removed.push(fullPath);
662
+ } catch {
663
+ result.failed.push(fullPath);
664
+ }
665
+ }
666
+ }
667
+ return result;
668
+ }
669
+
616
670
  // src/config-repo.ts
617
671
  var log2 = createLogger2("zen-fs-config:config-repo");
618
672
  var META_DIR = "/.meta";
@@ -1066,6 +1120,35 @@ var ConfigRepo = class {
1066
1120
  async processTombstonesPublic() {
1067
1121
  await this.processTombstones();
1068
1122
  }
1123
+ /**
1124
+ * Delete leaked `.mtime` sidecar files from the local primary backend
1125
+ * (IndexedDB on browser, Folder on Node).
1126
+ *
1127
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
1128
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
1129
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
1130
+ * invisible to sync, and every walk warns about the leak.
1131
+ *
1132
+ * Only the local primary is scanned — replica sidecars are live metadata of
1133
+ * the backend that owns them and must be left alone.
1134
+ *
1135
+ * @param options.root Scan only this subtree (default `/`).
1136
+ * @param options.dryRun List the sidecars without deleting them.
1137
+ */
1138
+ async purgeMtimeSidecars(options) {
1139
+ this.assertNotDisposed();
1140
+ const result = await purgeMtimeSidecars(this.cachedFS, options);
1141
+ if (result.removed.length > 0) {
1142
+ log2.log(
1143
+ `[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
1144
+ result.removed
1145
+ );
1146
+ }
1147
+ if (result.failed.length > 0) {
1148
+ log2.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
1149
+ }
1150
+ return result;
1151
+ }
1069
1152
  /**
1070
1153
  * Perform a full sync + dedup cycle without the watch snapshot cache.
1071
1154
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -1987,6 +2070,17 @@ var AppDataGroupImpl = class {
1987
2070
  } catch {
1988
2071
  throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
1989
2072
  }
2073
+ try {
2074
+ const purged = await purgeMtimeSidecars(this.localFS);
2075
+ if (purged.removed.length > 0) {
2076
+ log2.warn(
2077
+ `[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
2078
+ purged.removed
2079
+ );
2080
+ }
2081
+ } catch (err) {
2082
+ log2.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err?.message ?? err);
2083
+ }
1990
2084
  const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
1991
2085
  this.fs = createChrootFS(this.localFS, "/");
1992
2086
  const { kept, removed } = dedupeAppDataBackends(this._backends);
@@ -2113,6 +2207,19 @@ async function createConfigRepo(appId, options = {}) {
2113
2207
  log2.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
2114
2208
  const primaryInstance = await createBackend(localPrimary);
2115
2209
  const cachedFS = primaryInstance;
2210
+ if (options.purgeMtimeSidecars !== false) {
2211
+ try {
2212
+ const purged = await purgeMtimeSidecars(cachedFS);
2213
+ if (purged.removed.length > 0) {
2214
+ log2.warn(
2215
+ `[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
2216
+ purged.removed
2217
+ );
2218
+ }
2219
+ } catch (err) {
2220
+ log2.warn(`[createConfigRepo] .mtime purge failed:`, err?.message ?? err);
2221
+ }
2222
+ }
2116
2223
  const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
2117
2224
  try {
2118
2225
  await primaryInstance.mkdir(META_DIR);
@@ -2342,10 +2449,12 @@ export {
2342
2449
  hasBackend,
2343
2450
  incrementVersion,
2344
2451
  isBrowserEnv,
2452
+ isMtimeSidecar,
2345
2453
  listBackendMetadata,
2346
2454
  listBackends,
2347
2455
  localPrimaryType,
2348
2456
  mergeAccountFields,
2457
+ purgeMtimeSidecars,
2349
2458
  readVersion,
2350
2459
  registerBackend,
2351
2460
  registerFolderBackend,
@@ -24500,15 +24500,24 @@ var ZenFSConfig = (() => {
24500
24500
  }
24501
24501
  async function importOptionalBackend(pkg) {
24502
24502
  const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
24503
+ const fromGlobal = () => {
24504
+ if (!candidates) return void 0;
24505
+ for (const name of candidates) {
24506
+ const g = globalThis[name];
24507
+ if (g) return g;
24508
+ }
24509
+ return void 0;
24510
+ };
24503
24511
  try {
24504
- return await import(pkg);
24505
- } catch {
24506
- if (candidates) {
24507
- for (const name of candidates) {
24508
- const g = globalThis[name];
24509
- if (g) return g;
24510
- }
24512
+ const mod = await import(pkg);
24513
+ if (mod && Object.keys(mod).length === 0) {
24514
+ const g = fromGlobal();
24515
+ if (g) return g;
24511
24516
  }
24517
+ return mod;
24518
+ } catch {
24519
+ const g = fromGlobal();
24520
+ if (g) return g;
24512
24521
  const globalHint = candidates?.length ? `, or load its UMD bundle via <script src=".../${pkg}.js"> (exposes global "${candidates[0]}")` : "";
24513
24522
  throw new Error(
24514
24523
  `Backend package "${pkg}" is not available. In a bundler/import-map environment run: npm install ${pkg}${globalHint}.`
@@ -24562,7 +24571,13 @@ var ZenFSConfig = (() => {
24562
24571
  "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
24563
24572
  };
24564
24573
  registerBackend("GitHub", async (options) => {
24565
- const { Github } = await importOptionalBackend("zen-fs-github");
24574
+ const _gh = await importOptionalBackend("zen-fs-github");
24575
+ const Github = _gh?.Github ?? _gh?.default;
24576
+ if (typeof Github !== "function") {
24577
+ throw new Error(
24578
+ `Backend package "zen-fs-github" loaded, but its Github export is not a constructor (typeof=${typeof Github}). Load its UMD bundle so it exposes the global "ZenFSGitHub".`
24579
+ );
24580
+ }
24566
24581
  const backend = await wrapZenFSFileSystem({
24567
24582
  backend: Github,
24568
24583
  token: options.token,
@@ -24610,7 +24625,13 @@ var ZenFSConfig = (() => {
24610
24625
  accountFields: ["token", "owner", "baseUrl"]
24611
24626
  });
24612
24627
  registerBackend("Gitee", async (options) => {
24613
- const { Gitee } = await importOptionalBackend("zen-fs-gitee");
24628
+ const _gt = await importOptionalBackend("zen-fs-gitee");
24629
+ const Gitee = _gt?.Gitee ?? _gt?.default;
24630
+ if (typeof Gitee !== "function") {
24631
+ throw new Error(
24632
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
24633
+ );
24634
+ }
24614
24635
  return wrapZenFSFileSystem({
24615
24636
  backend: Gitee,
24616
24637
  token: options.token,
@@ -24634,7 +24655,13 @@ var ZenFSConfig = (() => {
24634
24655
  accountFields: ["token", "owner", "baseUrl"]
24635
24656
  });
24636
24657
  registerBackend("RemoteStorage", async (options) => {
24637
- const { RemoteStorageFileSystem } = await importOptionalBackend("zen-fs-remotestoragejs");
24658
+ const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
24659
+ const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
24660
+ if (typeof RemoteStorageFileSystem !== "function") {
24661
+ throw new Error(
24662
+ `Backend package "zen-fs-remotestoragejs" loaded, but its RemoteStorageFileSystem export is not a constructor (typeof=${typeof RemoteStorageFileSystem}). Load its UMD bundle via <script src=".../zen-fs-remotestoragejs.global.js"> so it exposes the global "ZenFSRemoteStorage".`
24663
+ );
24664
+ }
24638
24665
  return new RemoteStorageFileSystem({
24639
24666
  href: options.href,
24640
24667
  token: options.token,
@@ -25084,10 +25111,12 @@ var ZenFSConfig = (() => {
25084
25111
  hasBackend: () => hasBackend,
25085
25112
  incrementVersion: () => incrementVersion,
25086
25113
  isBrowserEnv: () => isBrowserEnv,
25114
+ isMtimeSidecar: () => isMtimeSidecar,
25087
25115
  listBackendMetadata: () => listBackendMetadata,
25088
25116
  listBackends: () => listBackends,
25089
25117
  localPrimaryType: () => localPrimaryType,
25090
25118
  mergeAccountFields: () => mergeAccountFields,
25119
+ purgeMtimeSidecars: () => purgeMtimeSidecars,
25091
25120
  readVersion: () => readVersion,
25092
25121
  registerBackend: () => registerBackend,
25093
25122
  registerFolderBackend: () => registerFolderBackend,
@@ -26888,6 +26917,60 @@ var ZenFSConfig = (() => {
26888
26917
  }
26889
26918
  }
26890
26919
 
26920
+ // src/mtime-cleanup.ts
26921
+ function isMtimeSidecar(fileName) {
26922
+ return fileName.endsWith(".mtime");
26923
+ }
26924
+ var S_IFDIR2 = 16384;
26925
+ function isDirectory3(stat5) {
26926
+ return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR2) === S_IFDIR2;
26927
+ }
26928
+ function joinPath(dir, entry) {
26929
+ if (dir === "/" || dir === "") return `/${entry}`;
26930
+ return `${dir.replace(/\/+$/, "")}/${entry}`;
26931
+ }
26932
+ async function purgeMtimeSidecars(fs, options = {}) {
26933
+ const root = options.root && options.root !== "" ? options.root : "/";
26934
+ const dryRun = options.dryRun ?? false;
26935
+ const result = { scanned: 0, removed: [], failed: [] };
26936
+ const stack = [root];
26937
+ while (stack.length > 0) {
26938
+ const dir = stack.pop();
26939
+ let entries2;
26940
+ try {
26941
+ entries2 = await fs.readdir(dir);
26942
+ } catch {
26943
+ continue;
26944
+ }
26945
+ for (const entry of entries2) {
26946
+ const fullPath = joinPath(dir, entry);
26947
+ let stat5;
26948
+ try {
26949
+ stat5 = await fs.stat(fullPath);
26950
+ } catch {
26951
+ continue;
26952
+ }
26953
+ if (isDirectory3(stat5)) {
26954
+ stack.push(fullPath);
26955
+ continue;
26956
+ }
26957
+ result.scanned++;
26958
+ if (!isMtimeSidecar(entry)) continue;
26959
+ if (dryRun) {
26960
+ result.removed.push(fullPath);
26961
+ continue;
26962
+ }
26963
+ try {
26964
+ await fs.unlink(fullPath);
26965
+ result.removed.push(fullPath);
26966
+ } catch {
26967
+ result.failed.push(fullPath);
26968
+ }
26969
+ }
26970
+ }
26971
+ return result;
26972
+ }
26973
+
26891
26974
  // src/config-repo.ts
26892
26975
  var log6 = createLogger2("zen-fs-config:config-repo");
26893
26976
  var META_DIR = "/.meta";
@@ -27341,6 +27424,35 @@ var ZenFSConfig = (() => {
27341
27424
  async processTombstonesPublic() {
27342
27425
  await this.processTombstones();
27343
27426
  }
27427
+ /**
27428
+ * Delete leaked `.mtime` sidecar files from the local primary backend
27429
+ * (IndexedDB on browser, Folder on Node).
27430
+ *
27431
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
27432
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
27433
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
27434
+ * invisible to sync, and every walk warns about the leak.
27435
+ *
27436
+ * Only the local primary is scanned — replica sidecars are live metadata of
27437
+ * the backend that owns them and must be left alone.
27438
+ *
27439
+ * @param options.root Scan only this subtree (default `/`).
27440
+ * @param options.dryRun List the sidecars without deleting them.
27441
+ */
27442
+ async purgeMtimeSidecars(options) {
27443
+ this.assertNotDisposed();
27444
+ const result = await purgeMtimeSidecars(this.cachedFS, options);
27445
+ if (result.removed.length > 0) {
27446
+ log6.log(
27447
+ `[ConfigRepo] purged ${result.removed.length} .mtime sidecar(s) from local primary` + (options?.dryRun ? " (dry run)" : ""),
27448
+ result.removed
27449
+ );
27450
+ }
27451
+ if (result.failed.length > 0) {
27452
+ log6.warn(`[ConfigRepo] failed to purge ${result.failed.length} .mtime sidecar(s):`, result.failed);
27453
+ }
27454
+ return result;
27455
+ }
27344
27456
  /**
27345
27457
  * Perform a full sync + dedup cycle without the watch snapshot cache.
27346
27458
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -28262,6 +28374,17 @@ var ZenFSConfig = (() => {
28262
28374
  } catch {
28263
28375
  throw new Error(`Failed to create local primary for data group "${this.groupId}"`);
28264
28376
  }
28377
+ try {
28378
+ const purged = await purgeMtimeSidecars(this.localFS);
28379
+ if (purged.removed.length > 0) {
28380
+ log6.warn(
28381
+ `[AppDataGroup:${this.groupId}] purged ${purged.removed.length} leaked .mtime sidecar(s):`,
28382
+ purged.removed
28383
+ );
28384
+ }
28385
+ } catch (err2) {
28386
+ log6.warn(`[AppDataGroup:${this.groupId}] .mtime purge failed:`, err2?.message ?? err2);
28387
+ }
28265
28388
  const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
28266
28389
  this.fs = createChrootFS(this.localFS, "/");
28267
28390
  const { kept, removed } = dedupeAppDataBackends(this._backends);
@@ -28388,6 +28511,19 @@ var ZenFSConfig = (() => {
28388
28511
  log6.log(`[createConfigRepo] Creating local primary (type: ${localPrimary.type})...`);
28389
28512
  const primaryInstance = await createBackend(localPrimary);
28390
28513
  const cachedFS = primaryInstance;
28514
+ if (options.purgeMtimeSidecars !== false) {
28515
+ try {
28516
+ const purged = await purgeMtimeSidecars(cachedFS);
28517
+ if (purged.removed.length > 0) {
28518
+ log6.warn(
28519
+ `[createConfigRepo] purged ${purged.removed.length} leaked .mtime sidecar(s) from local primary:`,
28520
+ purged.removed
28521
+ );
28522
+ }
28523
+ } catch (err2) {
28524
+ log6.warn(`[createConfigRepo] .mtime purge failed:`, err2?.message ?? err2);
28525
+ }
28526
+ }
28391
28527
  const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
28392
28528
  try {
28393
28529
  await primaryInstance.mkdir(META_DIR);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.30",
3
+ "version": "0.5.32",
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",