zen-fs-config 0.5.31 → 0.5.33

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
@@ -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 as `<name>.mtime` files.
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,29 @@ 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;
232
+ /**
233
+ * Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
234
+ * startup (rename locally, delete the remote residual).
235
+ *
236
+ * Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
237
+ * They are now `<name>.version`. This one-time sweep renames existing legacy
238
+ * sidecars (preserving version history) and removes the old copy from every
239
+ * replica so sync does not pull it back.
240
+ *
241
+ * Default: `true`. Pass `false` to skip the startup sweep.
242
+ */
243
+ migrateVersionSidecars?: boolean;
162
244
  }
163
245
  /** Type of sync group. */
164
246
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -340,6 +422,17 @@ interface IConfigRepo {
340
422
  * Called automatically by createConfigRepo() after setupSync().
341
423
  */
342
424
  syncMetaToReplicas(): Promise<void>;
425
+ /**
426
+ * Delete leaked `.mtime` sidecars from the local primary backend.
427
+ *
428
+ * Runs automatically on `createConfigRepo()` unless
429
+ * `options.purgeMtimeSidecars === false`. Call it manually to clean a
430
+ * long-lived store, or with `{ dryRun: true }` to only list them.
431
+ */
432
+ purgeMtimeSidecars(options?: {
433
+ root?: string;
434
+ dryRun?: boolean;
435
+ }): Promise<MtimePurgeResult>;
343
436
  /**
344
437
  * Create a data-sync group for this app.
345
438
  * Each backend can optionally reference a config-sync backend's account
@@ -504,6 +597,71 @@ declare function getAccountFields(type: string): string[];
504
597
  declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
505
598
  declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
506
599
 
600
+ /**
601
+ * zen-fs-config — Legacy Version Sidecar Migration
602
+ *
603
+ * Version sidecars used to be stored as hidden dotfiles:
604
+ *
605
+ * config file: /app-a/db.json
606
+ * version file: /app-a/.db.json.version (OLD — starts with `.`)
607
+ *
608
+ * They are now stored as `<name>.version` (no leading dot):
609
+ *
610
+ * version file: /app-a/db.json.version (NEW)
611
+ *
612
+ * This module migrates existing legacy sidecars to the new naming at startup,
613
+ * preserving version history (the file content is byte-identical after rename).
614
+ *
615
+ * Ambiguity safety: a legacy path `.x.version` could in theory belong to either
616
+ * a dotfile config `.x` (whose version is unchanged by the rename) or a
617
+ * non-dot config `x` (old naming). We resolve it by probing the directory:
618
+ * - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
619
+ * - `x` exists → rename legacy `.x.version` → `x.version`
620
+ * - neither → orphan (config gone) → delete
621
+ *
622
+ * We also delete the remote residual legacy path from every replica so the
623
+ * sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
624
+ * files on both ends).
625
+ */
626
+ /** Minimal async FS surface needed to read/rename/delete on the local primary. */
627
+ interface MigrateLocalFS {
628
+ readdir(path: string): Promise<string[]>;
629
+ stat(path: string): Promise<{
630
+ mode?: number;
631
+ }>;
632
+ readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
633
+ writeFile(path: string, data: Uint8Array): Promise<void>;
634
+ unlink(path: string): Promise<void>;
635
+ }
636
+ /** Minimal async FS surface needed to delete remote residual legacy sidecars. */
637
+ interface MigrateReplicaFS {
638
+ unlink(path: string): Promise<void>;
639
+ }
640
+ interface VersionMigrationOptions {
641
+ /** Root to scan. Default: `/` (the whole local primary). */
642
+ root?: string;
643
+ /** Report what would change without touching anything. */
644
+ dryRun?: boolean;
645
+ /** Replica backends whose legacy sidecars should be deleted. */
646
+ replicas?: MigrateReplicaFS[];
647
+ }
648
+ interface VersionMigrationResult {
649
+ /** Local legacy sidecars renamed to the new naming. */
650
+ renamed: string[];
651
+ /** Local orphan legacy sidecars deleted (no owning config). */
652
+ deleted: string[];
653
+ /** Remote residual legacy sidecars deleted (best-effort). */
654
+ remoteDeleted: string[];
655
+ /** Paths that failed to migrate. */
656
+ failed: string[];
657
+ }
658
+ /**
659
+ * Walk `fs` from `options.root` and migrate every legacy `.x.version` /
660
+ * `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
661
+ * naming. Best-effort: failures are collected in `failed`, never thrown.
662
+ */
663
+ declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
664
+
507
665
  /**
508
666
  * zen-fs-config — ConfigRepo Implementation
509
667
  *
@@ -593,6 +751,31 @@ declare class ConfigRepo implements IConfigRepo {
593
751
  private safeExists;
594
752
  /** Public wrapper for processTombstones — used by createConfigRepo. */
595
753
  processTombstonesPublic(): Promise<void>;
754
+ /**
755
+ * Delete leaked `.mtime` sidecar files from the local primary backend
756
+ * (IndexedDB on browser, Folder on Node).
757
+ *
758
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
759
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
760
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
761
+ * invisible to sync, and every walk warns about the leak.
762
+ *
763
+ * Only the local primary is scanned — replica sidecars are live metadata of
764
+ * the backend that owns them and must be left alone.
765
+ *
766
+ * @param options.root Scan only this subtree (default `/`).
767
+ * @param options.dryRun List the sidecars without deleting them.
768
+ */
769
+ purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
770
+ /**
771
+ * Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
772
+ * `<name>.version` naming. Renames local sidecars (preserving content/history)
773
+ * and deletes the residual legacy copy from every replica backend so sync
774
+ * does not pull it back. Best-effort: failures are logged, never thrown.
775
+ *
776
+ * Runs once at startup (see createConfigRepo). Safe to call again.
777
+ */
778
+ migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
596
779
  /**
597
780
  * Perform a full sync + dedup cycle without the watch snapshot cache.
598
781
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -779,24 +962,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
779
962
  /**
780
963
  * zen-fs-config — Sidecar Version File Management
781
964
  *
782
- * Each config file has a companion .version file for version-based change
783
- * detection and conflict resolution.
965
+ * Each config file has a companion `.version` sidecar file for version-based
966
+ * change detection and conflict resolution. The sidecar is named `<name>.version`
967
+ * (NOT `.name.version`) so it is not a hidden dotfile.
784
968
  *
785
969
  * Config file: /app-a/db.json
786
- * Version file: /app-a/.db.json.version
970
+ * Version file: /app-a/db.json.version
787
971
  */
788
972
 
789
973
  /**
790
974
  * Compute the sidecar version file path from a config file path.
791
975
  *
792
- * /app-a/db.json → /app-a/.db.json.version
793
- * /shared/flags.json → /shared/.flags.json.version
794
- * /nodes/s1/env.json → /nodes/s1/.env.json.version
976
+ * /app-a/db.json → /app-a/db.json.version
977
+ * /shared/flags.json → /shared/flags.json.version
978
+ * /nodes/s1/env.json → /nodes/s1/env.json.version
795
979
  *
796
980
  * Returns null for files that are already version sidecars (.version files),
797
- * to prevent creating version-of-version files (e.g. ..db.json.version.version).
981
+ * to prevent creating version-of-version files (e.g. db.json.version.version).
798
982
  */
799
983
  declare function versionPathFor(configFilePath: string): string | null;
984
+ /**
985
+ * Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
986
+ * file. Returns the old `.${name}.version` form for non-dot configs, or `null`
987
+ * for dotfile configs (whose version path is unchanged) and for version files
988
+ * themselves.
989
+ *
990
+ * Used as a read fallback so existing history in `.db.json.version` is still
991
+ * found after the naming scheme changed to `db.json.version`.
992
+ */
993
+ declare function legacyVersionPathFor(configFilePath: string): string | null;
800
994
  /**
801
995
  * Compute SHA-256 hash of a Uint8Array.
802
996
  * Returns "sha256:" prefix + hex digest.
@@ -826,4 +1020,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
826
1020
  */
827
1021
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
828
1022
 
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 };
1023
+ 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 MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, 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 as `<name>.mtime` files.
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,29 @@ 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;
232
+ /**
233
+ * Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
234
+ * startup (rename locally, delete the remote residual).
235
+ *
236
+ * Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
237
+ * They are now `<name>.version`. This one-time sweep renames existing legacy
238
+ * sidecars (preserving version history) and removes the old copy from every
239
+ * replica so sync does not pull it back.
240
+ *
241
+ * Default: `true`. Pass `false` to skip the startup sweep.
242
+ */
243
+ migrateVersionSidecars?: boolean;
162
244
  }
163
245
  /** Type of sync group. */
164
246
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -340,6 +422,17 @@ interface IConfigRepo {
340
422
  * Called automatically by createConfigRepo() after setupSync().
341
423
  */
342
424
  syncMetaToReplicas(): Promise<void>;
425
+ /**
426
+ * Delete leaked `.mtime` sidecars from the local primary backend.
427
+ *
428
+ * Runs automatically on `createConfigRepo()` unless
429
+ * `options.purgeMtimeSidecars === false`. Call it manually to clean a
430
+ * long-lived store, or with `{ dryRun: true }` to only list them.
431
+ */
432
+ purgeMtimeSidecars(options?: {
433
+ root?: string;
434
+ dryRun?: boolean;
435
+ }): Promise<MtimePurgeResult>;
343
436
  /**
344
437
  * Create a data-sync group for this app.
345
438
  * Each backend can optionally reference a config-sync backend's account
@@ -504,6 +597,71 @@ declare function getAccountFields(type: string): string[];
504
597
  declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
505
598
  declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
506
599
 
600
+ /**
601
+ * zen-fs-config — Legacy Version Sidecar Migration
602
+ *
603
+ * Version sidecars used to be stored as hidden dotfiles:
604
+ *
605
+ * config file: /app-a/db.json
606
+ * version file: /app-a/.db.json.version (OLD — starts with `.`)
607
+ *
608
+ * They are now stored as `<name>.version` (no leading dot):
609
+ *
610
+ * version file: /app-a/db.json.version (NEW)
611
+ *
612
+ * This module migrates existing legacy sidecars to the new naming at startup,
613
+ * preserving version history (the file content is byte-identical after rename).
614
+ *
615
+ * Ambiguity safety: a legacy path `.x.version` could in theory belong to either
616
+ * a dotfile config `.x` (whose version is unchanged by the rename) or a
617
+ * non-dot config `x` (old naming). We resolve it by probing the directory:
618
+ * - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
619
+ * - `x` exists → rename legacy `.x.version` → `x.version`
620
+ * - neither → orphan (config gone) → delete
621
+ *
622
+ * We also delete the remote residual legacy path from every replica so the
623
+ * sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
624
+ * files on both ends).
625
+ */
626
+ /** Minimal async FS surface needed to read/rename/delete on the local primary. */
627
+ interface MigrateLocalFS {
628
+ readdir(path: string): Promise<string[]>;
629
+ stat(path: string): Promise<{
630
+ mode?: number;
631
+ }>;
632
+ readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
633
+ writeFile(path: string, data: Uint8Array): Promise<void>;
634
+ unlink(path: string): Promise<void>;
635
+ }
636
+ /** Minimal async FS surface needed to delete remote residual legacy sidecars. */
637
+ interface MigrateReplicaFS {
638
+ unlink(path: string): Promise<void>;
639
+ }
640
+ interface VersionMigrationOptions {
641
+ /** Root to scan. Default: `/` (the whole local primary). */
642
+ root?: string;
643
+ /** Report what would change without touching anything. */
644
+ dryRun?: boolean;
645
+ /** Replica backends whose legacy sidecars should be deleted. */
646
+ replicas?: MigrateReplicaFS[];
647
+ }
648
+ interface VersionMigrationResult {
649
+ /** Local legacy sidecars renamed to the new naming. */
650
+ renamed: string[];
651
+ /** Local orphan legacy sidecars deleted (no owning config). */
652
+ deleted: string[];
653
+ /** Remote residual legacy sidecars deleted (best-effort). */
654
+ remoteDeleted: string[];
655
+ /** Paths that failed to migrate. */
656
+ failed: string[];
657
+ }
658
+ /**
659
+ * Walk `fs` from `options.root` and migrate every legacy `.x.version` /
660
+ * `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
661
+ * naming. Best-effort: failures are collected in `failed`, never thrown.
662
+ */
663
+ declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
664
+
507
665
  /**
508
666
  * zen-fs-config — ConfigRepo Implementation
509
667
  *
@@ -593,6 +751,31 @@ declare class ConfigRepo implements IConfigRepo {
593
751
  private safeExists;
594
752
  /** Public wrapper for processTombstones — used by createConfigRepo. */
595
753
  processTombstonesPublic(): Promise<void>;
754
+ /**
755
+ * Delete leaked `.mtime` sidecar files from the local primary backend
756
+ * (IndexedDB on browser, Folder on Node).
757
+ *
758
+ * Sidecars are produced by backends that keep a precise mtime out-of-band
759
+ * (RemoteStorage, Gitee…). Once one is copied into the local primary it is
760
+ * never removed: zen-fs-sync skips `.mtime` paths on both sides, so it is
761
+ * invisible to sync, and every walk warns about the leak.
762
+ *
763
+ * Only the local primary is scanned — replica sidecars are live metadata of
764
+ * the backend that owns them and must be left alone.
765
+ *
766
+ * @param options.root Scan only this subtree (default `/`).
767
+ * @param options.dryRun List the sidecars without deleting them.
768
+ */
769
+ purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
770
+ /**
771
+ * Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
772
+ * `<name>.version` naming. Renames local sidecars (preserving content/history)
773
+ * and deletes the residual legacy copy from every replica backend so sync
774
+ * does not pull it back. Best-effort: failures are logged, never thrown.
775
+ *
776
+ * Runs once at startup (see createConfigRepo). Safe to call again.
777
+ */
778
+ migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
596
779
  /**
597
780
  * Perform a full sync + dedup cycle without the watch snapshot cache.
598
781
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -779,24 +962,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
779
962
  /**
780
963
  * zen-fs-config — Sidecar Version File Management
781
964
  *
782
- * Each config file has a companion .version file for version-based change
783
- * detection and conflict resolution.
965
+ * Each config file has a companion `.version` sidecar file for version-based
966
+ * change detection and conflict resolution. The sidecar is named `<name>.version`
967
+ * (NOT `.name.version`) so it is not a hidden dotfile.
784
968
  *
785
969
  * Config file: /app-a/db.json
786
- * Version file: /app-a/.db.json.version
970
+ * Version file: /app-a/db.json.version
787
971
  */
788
972
 
789
973
  /**
790
974
  * Compute the sidecar version file path from a config file path.
791
975
  *
792
- * /app-a/db.json → /app-a/.db.json.version
793
- * /shared/flags.json → /shared/.flags.json.version
794
- * /nodes/s1/env.json → /nodes/s1/.env.json.version
976
+ * /app-a/db.json → /app-a/db.json.version
977
+ * /shared/flags.json → /shared/flags.json.version
978
+ * /nodes/s1/env.json → /nodes/s1/env.json.version
795
979
  *
796
980
  * Returns null for files that are already version sidecars (.version files),
797
- * to prevent creating version-of-version files (e.g. ..db.json.version.version).
981
+ * to prevent creating version-of-version files (e.g. db.json.version.version).
798
982
  */
799
983
  declare function versionPathFor(configFilePath: string): string | null;
984
+ /**
985
+ * Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
986
+ * file. Returns the old `.${name}.version` form for non-dot configs, or `null`
987
+ * for dotfile configs (whose version path is unchanged) and for version files
988
+ * themselves.
989
+ *
990
+ * Used as a read fallback so existing history in `.db.json.version` is still
991
+ * found after the naming scheme changed to `db.json.version`.
992
+ */
993
+ declare function legacyVersionPathFor(configFilePath: string): string | null;
800
994
  /**
801
995
  * Compute SHA-256 hash of a Uint8Array.
802
996
  * Returns "sha256:" prefix + hex digest.
@@ -826,4 +1020,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
826
1020
  */
827
1021
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
828
1022
 
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 };
1023
+ 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 MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeMtimeOptions, type PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };