zen-fs-config 0.5.32 → 0.5.34

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
@@ -25,7 +25,7 @@ import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
25
25
  * deleting them would cost network calls and lose mtime precision.
26
26
  */
27
27
  /** Minimal async FS surface needed for the purge. */
28
- interface PurgeableFS {
28
+ interface PurgeableFS$1 {
29
29
  readdir(path: string): Promise<string[]>;
30
30
  stat(path: string): Promise<{
31
31
  mode?: number;
@@ -57,11 +57,78 @@ declare function isMtimeSidecar(fileName: string): boolean;
57
57
  * Delete every `.mtime` sidecar stored in a local primary backend.
58
58
  *
59
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.
60
+ * because sidecars live next to their data file as `<name>.mtime` files.
61
61
  * Unreadable entries are skipped; failures on individual files are collected
62
62
  * in `failed` instead of aborting the walk.
63
63
  */
64
- declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
64
+ declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
65
+
66
+ /**
67
+ * zen-fs-config — `.keep` placeholder cleanup (local primary only)
68
+ *
69
+ * Backends that cannot store empty directories (Git, RemoteStorage…) keep a
70
+ * directory alive with an internal `.keep` placeholder — exactly like the
71
+ * `.mtime` sidecar, this is backend-internal metadata that MUST be hidden from
72
+ * callers (see zen-fs-sync/docs/SyncableFS.md §1/§2).
73
+ *
74
+ * Older builds leaked `.keep` files into the local primary (IndexedDB on
75
+ * browser, Folder on Node), where they linger forever: the sync engine now
76
+ * skips `.keep` paths on both sides, so nothing ever removes them, and every
77
+ * walk re-creates confusion about which directories are "real".
78
+ *
79
+ * This module walks the local primary and deletes those leaked `.keep` files.
80
+ * It never touches replica (remote) backends: their placeholders are live
81
+ * metadata that keep real (empty) directories alive, and deleting them would
82
+ * lose directory structure.
83
+ *
84
+ * The intentional `.keep` under `/.meta/` (used to keep the backends directory
85
+ * alive) is protected and never deleted — see `protectedDirs`.
86
+ */
87
+ /** Minimal async FS surface needed for the purge. */
88
+ interface PurgeableFS {
89
+ readdir(path: string): Promise<string[]>;
90
+ stat(path: string): Promise<{
91
+ mode?: number;
92
+ }>;
93
+ unlink(path: string): Promise<void>;
94
+ }
95
+ interface PurgeKeepOptions {
96
+ /** Root to scan. Default: `/` (the whole local primary). */
97
+ root?: string;
98
+ /** Report what would be deleted without unlinking anything. */
99
+ dryRun?: boolean;
100
+ /**
101
+ * Directories (and their subtrees) whose `.keep` files are NEVER deleted.
102
+ * Defaults to `['/.meta']` to protect the intentional backends placeholder
103
+ * that keeps `/.meta/backends` alive.
104
+ */
105
+ protectedDirs?: string[];
106
+ }
107
+ interface KeepPurgeResult {
108
+ /** Files visited during the walk (`.keep` included). */
109
+ scanned: number;
110
+ /** Placeholders deleted — or, in dryRun, that would be deleted. */
111
+ removed: string[];
112
+ /** Placeholders that could not be deleted (unlink failed). */
113
+ failed: string[];
114
+ }
115
+ /**
116
+ * True when a file name is the internal `.keep` directory placeholder.
117
+ *
118
+ * Matches exactly `.keep` (the file name), which is the only form produced by
119
+ * the backends — never a suffix match, so `foo.keep` is left untouched.
120
+ */
121
+ declare function isKeepFile(fileName: string): boolean;
122
+ /**
123
+ * Delete every leaked `.keep` placeholder stored in a local primary backend.
124
+ *
125
+ * Walks the whole tree from `options.root` — including dotfiles and `/.meta/`
126
+ * entries — but skips any directory listed in `protectedDirs` (default
127
+ * `/.meta`) so the intentional backends placeholder survives. Unreadable
128
+ * entries are skipped; failures on individual files are collected in `failed`
129
+ * instead of aborting the walk.
130
+ */
131
+ declare function purgeKeepFiles(fs: PurgeableFS, options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
65
132
 
66
133
  /** A single backend in the topology. */
67
134
  interface BackendDescriptor {
@@ -229,6 +296,30 @@ interface ConfigRepoOptions {
229
296
  * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
230
297
  */
231
298
  purgeMtimeSidecars?: boolean;
299
+ /**
300
+ * Delete leaked `.keep` placeholders from the local primary on startup.
301
+ *
302
+ * Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
303
+ * directory alive with an internal `.keep` placeholder. Older builds copied
304
+ * those backend-internal files into the local primary, where sync ignores
305
+ * them — so they linger forever. The intentional `/.meta/backends/.keep` is
306
+ * always protected.
307
+ *
308
+ * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
309
+ */
310
+ purgeKeepFiles?: boolean;
311
+ /**
312
+ * Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
313
+ * startup (rename locally, delete the remote residual).
314
+ *
315
+ * Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
316
+ * They are now `<name>.version`. This one-time sweep renames existing legacy
317
+ * sidecars (preserving version history) and removes the old copy from every
318
+ * replica so sync does not pull it back.
319
+ *
320
+ * Default: `true`. Pass `false` to skip the startup sweep.
321
+ */
322
+ migrateVersionSidecars?: boolean;
232
323
  }
233
324
  /** Type of sync group. */
234
325
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -421,6 +512,17 @@ interface IConfigRepo {
421
512
  root?: string;
422
513
  dryRun?: boolean;
423
514
  }): Promise<MtimePurgeResult>;
515
+ /**
516
+ * Delete leaked `.keep` placeholders from the local primary backend.
517
+ *
518
+ * Runs automatically on `createConfigRepo()` unless
519
+ * `options.purgeKeepFiles === false`. Call it manually to clean a
520
+ * long-lived store, or with `{ dryRun: true }` to only list them.
521
+ */
522
+ purgeKeepFiles(options?: {
523
+ root?: string;
524
+ dryRun?: boolean;
525
+ }): Promise<KeepPurgeResult>;
424
526
  /**
425
527
  * Create a data-sync group for this app.
426
528
  * Each backend can optionally reference a config-sync backend's account
@@ -585,6 +687,71 @@ declare function getAccountFields(type: string): string[];
585
687
  declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
586
688
  declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
587
689
 
690
+ /**
691
+ * zen-fs-config — Legacy Version Sidecar Migration
692
+ *
693
+ * Version sidecars used to be stored as hidden dotfiles:
694
+ *
695
+ * config file: /app-a/db.json
696
+ * version file: /app-a/.db.json.version (OLD — starts with `.`)
697
+ *
698
+ * They are now stored as `<name>.version` (no leading dot):
699
+ *
700
+ * version file: /app-a/db.json.version (NEW)
701
+ *
702
+ * This module migrates existing legacy sidecars to the new naming at startup,
703
+ * preserving version history (the file content is byte-identical after rename).
704
+ *
705
+ * Ambiguity safety: a legacy path `.x.version` could in theory belong to either
706
+ * a dotfile config `.x` (whose version is unchanged by the rename) or a
707
+ * non-dot config `x` (old naming). We resolve it by probing the directory:
708
+ * - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
709
+ * - `x` exists → rename legacy `.x.version` → `x.version`
710
+ * - neither → orphan (config gone) → delete
711
+ *
712
+ * We also delete the remote residual legacy path from every replica so the
713
+ * sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
714
+ * files on both ends).
715
+ */
716
+ /** Minimal async FS surface needed to read/rename/delete on the local primary. */
717
+ interface MigrateLocalFS {
718
+ readdir(path: string): Promise<string[]>;
719
+ stat(path: string): Promise<{
720
+ mode?: number;
721
+ }>;
722
+ readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
723
+ writeFile(path: string, data: Uint8Array): Promise<void>;
724
+ unlink(path: string): Promise<void>;
725
+ }
726
+ /** Minimal async FS surface needed to delete remote residual legacy sidecars. */
727
+ interface MigrateReplicaFS {
728
+ unlink(path: string): Promise<void>;
729
+ }
730
+ interface VersionMigrationOptions {
731
+ /** Root to scan. Default: `/` (the whole local primary). */
732
+ root?: string;
733
+ /** Report what would change without touching anything. */
734
+ dryRun?: boolean;
735
+ /** Replica backends whose legacy sidecars should be deleted. */
736
+ replicas?: MigrateReplicaFS[];
737
+ }
738
+ interface VersionMigrationResult {
739
+ /** Local legacy sidecars renamed to the new naming. */
740
+ renamed: string[];
741
+ /** Local orphan legacy sidecars deleted (no owning config). */
742
+ deleted: string[];
743
+ /** Remote residual legacy sidecars deleted (best-effort). */
744
+ remoteDeleted: string[];
745
+ /** Paths that failed to migrate. */
746
+ failed: string[];
747
+ }
748
+ /**
749
+ * Walk `fs` from `options.root` and migrate every legacy `.x.version` /
750
+ * `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
751
+ * naming. Best-effort: failures are collected in `failed`, never thrown.
752
+ */
753
+ declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
754
+
588
755
  /**
589
756
  * zen-fs-config — ConfigRepo Implementation
590
757
  *
@@ -690,6 +857,28 @@ declare class ConfigRepo implements IConfigRepo {
690
857
  * @param options.dryRun List the sidecars without deleting them.
691
858
  */
692
859
  purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
860
+ /**
861
+ * Delete leaked `.keep` placeholders from the local primary backend.
862
+ *
863
+ * Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
864
+ * directory alive with an internal `.keep` placeholder. Older builds copied
865
+ * those backend-internal files into the local primary, where sync ignores
866
+ * them — so they linger forever. `/.meta/backends/.keep` (intentional) is
867
+ * protected and never removed.
868
+ *
869
+ * @param options.root Scan only this subtree (default `/`).
870
+ * @param options.dryRun List the placeholders without deleting them.
871
+ */
872
+ purgeKeepFiles(options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
873
+ /**
874
+ * Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
875
+ * `<name>.version` naming. Renames local sidecars (preserving content/history)
876
+ * and deletes the residual legacy copy from every replica backend so sync
877
+ * does not pull it back. Best-effort: failures are logged, never thrown.
878
+ *
879
+ * Runs once at startup (see createConfigRepo). Safe to call again.
880
+ */
881
+ migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
693
882
  /**
694
883
  * Perform a full sync + dedup cycle without the watch snapshot cache.
695
884
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -876,24 +1065,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
876
1065
  /**
877
1066
  * zen-fs-config — Sidecar Version File Management
878
1067
  *
879
- * Each config file has a companion .version file for version-based change
880
- * detection and conflict resolution.
1068
+ * Each config file has a companion `.version` sidecar file for version-based
1069
+ * change detection and conflict resolution. The sidecar is named `<name>.version`
1070
+ * (NOT `.name.version`) so it is not a hidden dotfile.
881
1071
  *
882
1072
  * Config file: /app-a/db.json
883
- * Version file: /app-a/.db.json.version
1073
+ * Version file: /app-a/db.json.version
884
1074
  */
885
1075
 
886
1076
  /**
887
1077
  * Compute the sidecar version file path from a config file path.
888
1078
  *
889
- * /app-a/db.json → /app-a/.db.json.version
890
- * /shared/flags.json → /shared/.flags.json.version
891
- * /nodes/s1/env.json → /nodes/s1/.env.json.version
1079
+ * /app-a/db.json → /app-a/db.json.version
1080
+ * /shared/flags.json → /shared/flags.json.version
1081
+ * /nodes/s1/env.json → /nodes/s1/env.json.version
892
1082
  *
893
1083
  * Returns null for files that are already version sidecars (.version files),
894
- * to prevent creating version-of-version files (e.g. ..db.json.version.version).
1084
+ * to prevent creating version-of-version files (e.g. db.json.version.version).
895
1085
  */
896
1086
  declare function versionPathFor(configFilePath: string): string | null;
1087
+ /**
1088
+ * Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
1089
+ * file. Returns the old `.${name}.version` form for non-dot configs, or `null`
1090
+ * for dotfile configs (whose version path is unchanged) and for version files
1091
+ * themselves.
1092
+ *
1093
+ * Used as a read fallback so existing history in `.db.json.version` is still
1094
+ * found after the naming scheme changed to `db.json.version`.
1095
+ */
1096
+ declare function legacyVersionPathFor(configFilePath: string): string | null;
897
1097
  /**
898
1098
  * Compute SHA-256 hash of a Uint8Array.
899
1099
  * Returns "sha256:" prefix + hex digest.
@@ -923,4 +1123,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
923
1123
  */
924
1124
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
925
1125
 
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 };
1126
+ 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, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ import { SyncMapStore, SyncMapTransaction } from '@zenfs/core';
25
25
  * deleting them would cost network calls and lose mtime precision.
26
26
  */
27
27
  /** Minimal async FS surface needed for the purge. */
28
- interface PurgeableFS {
28
+ interface PurgeableFS$1 {
29
29
  readdir(path: string): Promise<string[]>;
30
30
  stat(path: string): Promise<{
31
31
  mode?: number;
@@ -57,11 +57,78 @@ declare function isMtimeSidecar(fileName: string): boolean;
57
57
  * Delete every `.mtime` sidecar stored in a local primary backend.
58
58
  *
59
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.
60
+ * because sidecars live next to their data file as `<name>.mtime` files.
61
61
  * Unreadable entries are skipped; failures on individual files are collected
62
62
  * in `failed` instead of aborting the walk.
63
63
  */
64
- declare function purgeMtimeSidecars(fs: PurgeableFS, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
64
+ declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
65
+
66
+ /**
67
+ * zen-fs-config — `.keep` placeholder cleanup (local primary only)
68
+ *
69
+ * Backends that cannot store empty directories (Git, RemoteStorage…) keep a
70
+ * directory alive with an internal `.keep` placeholder — exactly like the
71
+ * `.mtime` sidecar, this is backend-internal metadata that MUST be hidden from
72
+ * callers (see zen-fs-sync/docs/SyncableFS.md §1/§2).
73
+ *
74
+ * Older builds leaked `.keep` files into the local primary (IndexedDB on
75
+ * browser, Folder on Node), where they linger forever: the sync engine now
76
+ * skips `.keep` paths on both sides, so nothing ever removes them, and every
77
+ * walk re-creates confusion about which directories are "real".
78
+ *
79
+ * This module walks the local primary and deletes those leaked `.keep` files.
80
+ * It never touches replica (remote) backends: their placeholders are live
81
+ * metadata that keep real (empty) directories alive, and deleting them would
82
+ * lose directory structure.
83
+ *
84
+ * The intentional `.keep` under `/.meta/` (used to keep the backends directory
85
+ * alive) is protected and never deleted — see `protectedDirs`.
86
+ */
87
+ /** Minimal async FS surface needed for the purge. */
88
+ interface PurgeableFS {
89
+ readdir(path: string): Promise<string[]>;
90
+ stat(path: string): Promise<{
91
+ mode?: number;
92
+ }>;
93
+ unlink(path: string): Promise<void>;
94
+ }
95
+ interface PurgeKeepOptions {
96
+ /** Root to scan. Default: `/` (the whole local primary). */
97
+ root?: string;
98
+ /** Report what would be deleted without unlinking anything. */
99
+ dryRun?: boolean;
100
+ /**
101
+ * Directories (and their subtrees) whose `.keep` files are NEVER deleted.
102
+ * Defaults to `['/.meta']` to protect the intentional backends placeholder
103
+ * that keeps `/.meta/backends` alive.
104
+ */
105
+ protectedDirs?: string[];
106
+ }
107
+ interface KeepPurgeResult {
108
+ /** Files visited during the walk (`.keep` included). */
109
+ scanned: number;
110
+ /** Placeholders deleted — or, in dryRun, that would be deleted. */
111
+ removed: string[];
112
+ /** Placeholders that could not be deleted (unlink failed). */
113
+ failed: string[];
114
+ }
115
+ /**
116
+ * True when a file name is the internal `.keep` directory placeholder.
117
+ *
118
+ * Matches exactly `.keep` (the file name), which is the only form produced by
119
+ * the backends — never a suffix match, so `foo.keep` is left untouched.
120
+ */
121
+ declare function isKeepFile(fileName: string): boolean;
122
+ /**
123
+ * Delete every leaked `.keep` placeholder stored in a local primary backend.
124
+ *
125
+ * Walks the whole tree from `options.root` — including dotfiles and `/.meta/`
126
+ * entries — but skips any directory listed in `protectedDirs` (default
127
+ * `/.meta`) so the intentional backends placeholder survives. Unreadable
128
+ * entries are skipped; failures on individual files are collected in `failed`
129
+ * instead of aborting the walk.
130
+ */
131
+ declare function purgeKeepFiles(fs: PurgeableFS, options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
65
132
 
66
133
  /** A single backend in the topology. */
67
134
  interface BackendDescriptor {
@@ -229,6 +296,30 @@ interface ConfigRepoOptions {
229
296
  * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
230
297
  */
231
298
  purgeMtimeSidecars?: boolean;
299
+ /**
300
+ * Delete leaked `.keep` placeholders from the local primary on startup.
301
+ *
302
+ * Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
303
+ * directory alive with an internal `.keep` placeholder. Older builds copied
304
+ * those backend-internal files into the local primary, where sync ignores
305
+ * them — so they linger forever. The intentional `/.meta/backends/.keep` is
306
+ * always protected.
307
+ *
308
+ * Default: `true`. Pass `false` to skip the (local-only) startup sweep.
309
+ */
310
+ purgeKeepFiles?: boolean;
311
+ /**
312
+ * Migrate legacy `.x.version` sidecars to the new `<name>.version` naming on
313
+ * startup (rename locally, delete the remote residual).
314
+ *
315
+ * Older builds stored version sidecars as hidden dotfiles (`.db.json.version`).
316
+ * They are now `<name>.version`. This one-time sweep renames existing legacy
317
+ * sidecars (preserving version history) and removes the old copy from every
318
+ * replica so sync does not pull it back.
319
+ *
320
+ * Default: `true`. Pass `false` to skip the startup sweep.
321
+ */
322
+ migrateVersionSidecars?: boolean;
232
323
  }
233
324
  /** Type of sync group. */
234
325
  type SyncGroupType = 'config-sync' | 'data-sync';
@@ -421,6 +512,17 @@ interface IConfigRepo {
421
512
  root?: string;
422
513
  dryRun?: boolean;
423
514
  }): Promise<MtimePurgeResult>;
515
+ /**
516
+ * Delete leaked `.keep` placeholders from the local primary backend.
517
+ *
518
+ * Runs automatically on `createConfigRepo()` unless
519
+ * `options.purgeKeepFiles === false`. Call it manually to clean a
520
+ * long-lived store, or with `{ dryRun: true }` to only list them.
521
+ */
522
+ purgeKeepFiles(options?: {
523
+ root?: string;
524
+ dryRun?: boolean;
525
+ }): Promise<KeepPurgeResult>;
424
526
  /**
425
527
  * Create a data-sync group for this app.
426
528
  * Each backend can optionally reference a config-sync backend's account
@@ -585,6 +687,71 @@ declare function getAccountFields(type: string): string[];
585
687
  declare function mergeAccountFields(targetType: string, sourceOptions: Record<string, unknown>, targetOptions: Record<string, unknown>): Record<string, unknown>;
586
688
  declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
587
689
 
690
+ /**
691
+ * zen-fs-config — Legacy Version Sidecar Migration
692
+ *
693
+ * Version sidecars used to be stored as hidden dotfiles:
694
+ *
695
+ * config file: /app-a/db.json
696
+ * version file: /app-a/.db.json.version (OLD — starts with `.`)
697
+ *
698
+ * They are now stored as `<name>.version` (no leading dot):
699
+ *
700
+ * version file: /app-a/db.json.version (NEW)
701
+ *
702
+ * This module migrates existing legacy sidecars to the new naming at startup,
703
+ * preserving version history (the file content is byte-identical after rename).
704
+ *
705
+ * Ambiguity safety: a legacy path `.x.version` could in theory belong to either
706
+ * a dotfile config `.x` (whose version is unchanged by the rename) or a
707
+ * non-dot config `x` (old naming). We resolve it by probing the directory:
708
+ * - `.x` exists → it's the dotfile config's version → NEW path == legacy → skip
709
+ * - `x` exists → rename legacy `.x.version` → `x.version`
710
+ * - neither → orphan (config gone) → delete
711
+ *
712
+ * We also delete the remote residual legacy path from every replica so the
713
+ * sync engine does not pull the old sidecar back (zen-fs-sync syncs `.version`
714
+ * files on both ends).
715
+ */
716
+ /** Minimal async FS surface needed to read/rename/delete on the local primary. */
717
+ interface MigrateLocalFS {
718
+ readdir(path: string): Promise<string[]>;
719
+ stat(path: string): Promise<{
720
+ mode?: number;
721
+ }>;
722
+ readFile(path: string, encoding?: string): Promise<Uint8Array | string>;
723
+ writeFile(path: string, data: Uint8Array): Promise<void>;
724
+ unlink(path: string): Promise<void>;
725
+ }
726
+ /** Minimal async FS surface needed to delete remote residual legacy sidecars. */
727
+ interface MigrateReplicaFS {
728
+ unlink(path: string): Promise<void>;
729
+ }
730
+ interface VersionMigrationOptions {
731
+ /** Root to scan. Default: `/` (the whole local primary). */
732
+ root?: string;
733
+ /** Report what would change without touching anything. */
734
+ dryRun?: boolean;
735
+ /** Replica backends whose legacy sidecars should be deleted. */
736
+ replicas?: MigrateReplicaFS[];
737
+ }
738
+ interface VersionMigrationResult {
739
+ /** Local legacy sidecars renamed to the new naming. */
740
+ renamed: string[];
741
+ /** Local orphan legacy sidecars deleted (no owning config). */
742
+ deleted: string[];
743
+ /** Remote residual legacy sidecars deleted (best-effort). */
744
+ remoteDeleted: string[];
745
+ /** Paths that failed to migrate. */
746
+ failed: string[];
747
+ }
748
+ /**
749
+ * Walk `fs` from `options.root` and migrate every legacy `.x.version` /
750
+ * `.x.version.mtime` sidecar to the new `<name>.version` / `<name>.version.mtime`
751
+ * naming. Best-effort: failures are collected in `failed`, never thrown.
752
+ */
753
+ declare function migrateVersionSidecars(fs: MigrateLocalFS, options?: VersionMigrationOptions): Promise<VersionMigrationResult>;
754
+
588
755
  /**
589
756
  * zen-fs-config — ConfigRepo Implementation
590
757
  *
@@ -690,6 +857,28 @@ declare class ConfigRepo implements IConfigRepo {
690
857
  * @param options.dryRun List the sidecars without deleting them.
691
858
  */
692
859
  purgeMtimeSidecars(options?: PurgeMtimeOptions): Promise<MtimePurgeResult>;
860
+ /**
861
+ * Delete leaked `.keep` placeholders from the local primary backend.
862
+ *
863
+ * Backends that cannot store empty directories (Gitee, RemoteStorage…) keep a
864
+ * directory alive with an internal `.keep` placeholder. Older builds copied
865
+ * those backend-internal files into the local primary, where sync ignores
866
+ * them — so they linger forever. `/.meta/backends/.keep` (intentional) is
867
+ * protected and never removed.
868
+ *
869
+ * @param options.root Scan only this subtree (default `/`).
870
+ * @param options.dryRun List the placeholders without deleting them.
871
+ */
872
+ purgeKeepFiles(options?: PurgeKeepOptions): Promise<KeepPurgeResult>;
873
+ /**
874
+ * Migrate legacy `.x.version` sidecars (old dotfile naming) to the new
875
+ * `<name>.version` naming. Renames local sidecars (preserving content/history)
876
+ * and deletes the residual legacy copy from every replica backend so sync
877
+ * does not pull it back. Best-effort: failures are logged, never thrown.
878
+ *
879
+ * Runs once at startup (see createConfigRepo). Safe to call again.
880
+ */
881
+ migrateLegacyVersionSidecars(): Promise<VersionMigrationResult>;
693
882
  /**
694
883
  * Perform a full sync + dedup cycle without the watch snapshot cache.
695
884
  * Used by createConfigRepo to pull remote-only files (like duplicate
@@ -876,24 +1065,35 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
876
1065
  /**
877
1066
  * zen-fs-config — Sidecar Version File Management
878
1067
  *
879
- * Each config file has a companion .version file for version-based change
880
- * detection and conflict resolution.
1068
+ * Each config file has a companion `.version` sidecar file for version-based
1069
+ * change detection and conflict resolution. The sidecar is named `<name>.version`
1070
+ * (NOT `.name.version`) so it is not a hidden dotfile.
881
1071
  *
882
1072
  * Config file: /app-a/db.json
883
- * Version file: /app-a/.db.json.version
1073
+ * Version file: /app-a/db.json.version
884
1074
  */
885
1075
 
886
1076
  /**
887
1077
  * Compute the sidecar version file path from a config file path.
888
1078
  *
889
- * /app-a/db.json → /app-a/.db.json.version
890
- * /shared/flags.json → /shared/.flags.json.version
891
- * /nodes/s1/env.json → /nodes/s1/.env.json.version
1079
+ * /app-a/db.json → /app-a/db.json.version
1080
+ * /shared/flags.json → /shared/flags.json.version
1081
+ * /nodes/s1/env.json → /nodes/s1/env.json.version
892
1082
  *
893
1083
  * Returns null for files that are already version sidecars (.version files),
894
- * to prevent creating version-of-version files (e.g. ..db.json.version.version).
1084
+ * to prevent creating version-of-version files (e.g. db.json.version.version).
895
1085
  */
896
1086
  declare function versionPathFor(configFilePath: string): string | null;
1087
+ /**
1088
+ * Compute the *legacy* (pre-dotfile-removal) version sidecar path for a config
1089
+ * file. Returns the old `.${name}.version` form for non-dot configs, or `null`
1090
+ * for dotfile configs (whose version path is unchanged) and for version files
1091
+ * themselves.
1092
+ *
1093
+ * Used as a read fallback so existing history in `.db.json.version` is still
1094
+ * found after the naming scheme changed to `db.json.version`.
1095
+ */
1096
+ declare function legacyVersionPathFor(configFilePath: string): string | null;
897
1097
  /**
898
1098
  * Compute SHA-256 hash of a Uint8Array.
899
1099
  * Returns "sha256:" prefix + hex digest.
@@ -923,4 +1123,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
923
1123
  */
924
1124
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
925
1125
 
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 };
1126
+ 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, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };