@narumitw/pi-sync 0.50.3 → 0.51.1

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.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -11,430 +11,397 @@ export { isDeniedPath } from "../paths.js";
11
11
  export { sessionStorageRoot } from "./snapshot-paths.js";
12
12
 
13
13
  import {
14
- canonicalSnapshotPathForConfig,
15
- customIncludePathsByLower,
16
- DEFAULT_SYNC_INCLUDE,
17
- includeFromSelectionConfig,
18
- isConfiguredSnapshotPath,
19
- isPreservableUnmanagedSnapshotPath,
20
- normalizeExtraFiles,
21
- normalizeSyncFiles,
22
- type SyncSelectionConfig,
23
- selectionForSnapshot,
24
- snapshotSelectionInclude,
25
- syncIncludeSelection,
14
+ canonicalSnapshotPathForConfig,
15
+ customIncludePathsByLower,
16
+ DEFAULT_SYNC_INCLUDE,
17
+ includeFromSelectionConfig,
18
+ isConfiguredSnapshotPath,
19
+ isPreservableUnmanagedSnapshotPath,
20
+ normalizeExtraFiles,
21
+ normalizeSyncFiles,
22
+ type SyncSelectionConfig,
23
+ selectionForSnapshot,
24
+ snapshotSelectionInclude,
25
+ syncIncludeSelection,
26
26
  } from "../sync/sync-policy.js";
27
27
  import type { Snapshot, SnapshotFile, SnapshotOptions } from "./snapshot-types.js";
28
28
 
29
29
  export { canonicalSnapshotPathForConfig, isConfiguredSnapshotPath } from "../sync/sync-policy.js";
30
30
 
31
31
  const VERSION = 1;
32
- const TOP_LEVEL_FILES: readonly string[] = DEFAULT_SYNC_INCLUDE.filter((name) =>
33
- name.includes("."),
34
- );
32
+ const TOP_LEVEL_FILES: readonly string[] = DEFAULT_SYNC_INCLUDE.filter((name) => name.includes("."));
35
33
  const TOP_LEVEL_DIRS = new Set<string>(DEFAULT_SYNC_INCLUDE.filter((name) => !name.includes(".")));
36
34
  const SECRET_PATTERNS = [
37
- /AWS_SECRET_ACCESS_KEY\s*[=:]\s*['"]?[A-Za-z0-9/+]{35,}/i,
38
- /(ANTHROPIC|OPENAI|GEMINI|GOOGLE|FIRECRAWL|GITHUB|CLOUDFLARE|R2|S3)_[A-Z0-9_]*(KEY|TOKEN|SECRET)\s*[=:]\s*['"]?[^\s'"]{12,}/i,
39
- /sk-ant-[A-Za-z0-9_-]{20,}/,
40
- /sk-[A-Za-z0-9]{20,}/,
41
- /gh[pousr]_[A-Za-z0-9_]{20,}/,
35
+ /AWS_SECRET_ACCESS_KEY\s*[=:]\s*['"]?[A-Za-z0-9/+]{35,}/i,
36
+ /(ANTHROPIC|OPENAI|GEMINI|GOOGLE|FIRECRAWL|GITHUB|CLOUDFLARE|R2|S3)_[A-Z0-9_]*(KEY|TOKEN|SECRET)\s*[=:]\s*['"]?[^\s'"]{12,}/i,
37
+ /sk-ant-[A-Za-z0-9_-]{20,}/,
38
+ /sk-[A-Za-z0-9]{20,}/,
39
+ /gh[pousr]_[A-Za-z0-9_]{20,}/,
42
40
  ];
43
41
 
44
42
  function selectTopLevelFileEntry(entries: Dirent[], fileName: string) {
45
- const exact = entries.find((entry) => entry.isFile() && entry.name === fileName);
46
- if (exact) return exact;
47
- const lower = fileName.toLowerCase();
48
- return entries
49
- .filter((entry) => entry.isFile() && entry.name.toLowerCase() === lower)
50
- .sort((left, right) => left.name.localeCompare(right.name))[0];
43
+ const exact = entries.find((entry) => entry.isFile() && entry.name === fileName);
44
+ if (exact) return exact;
45
+ const lower = fileName.toLowerCase();
46
+ return entries
47
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase() === lower)
48
+ .sort((left, right) => left.name.localeCompare(right.name))[0];
51
49
  }
52
50
 
53
51
  function sha256(value: Buffer) {
54
- return createHash("sha256").update(value).digest("hex");
52
+ return createHash("sha256").update(value).digest("hex");
55
53
  }
56
54
 
57
55
  function isSafeSnapshotPath(relativePath: string) {
58
- if (relativePath.includes("\\")) return false;
59
- const normalized = toPosix(relativePath);
60
- return (
61
- Boolean(normalized) &&
62
- normalized !== "." &&
63
- normalized !== ".." &&
64
- !normalized.startsWith("../") &&
65
- !path.posix.isAbsolute(normalized) &&
66
- path.posix.normalize(normalized) === normalized &&
67
- !isDeniedPath(normalized)
68
- );
56
+ if (relativePath.includes("\\")) return false;
57
+ const normalized = toPosix(relativePath);
58
+ return (
59
+ Boolean(normalized) &&
60
+ normalized !== "." &&
61
+ normalized !== ".." &&
62
+ !normalized.startsWith("../") &&
63
+ !path.posix.isAbsolute(normalized) &&
64
+ path.posix.normalize(normalized) === normalized &&
65
+ !isDeniedPath(normalized)
66
+ );
69
67
  }
70
68
 
71
69
  function snapshotsMatch(left: Snapshot, right: Snapshot) {
72
- const leftHashes = new Map(left.files.map((file) => [file.path, file.sha256]));
73
- const rightHashes = new Map(right.files.map((file) => [file.path, file.sha256]));
74
- return (
75
- left.syncSessions === right.syncSessions &&
76
- sameOptionalInclude(snapshotSelectionInclude(left), snapshotSelectionInclude(right)) &&
77
- leftHashes.size === rightHashes.size &&
78
- [...leftHashes].every(([filePath, hash]) => rightHashes.get(filePath) === hash)
79
- );
70
+ const leftHashes = new Map(left.files.map((file) => [file.path, file.sha256]));
71
+ const rightHashes = new Map(right.files.map((file) => [file.path, file.sha256]));
72
+ return (
73
+ left.syncSessions === right.syncSessions &&
74
+ sameOptionalInclude(snapshotSelectionInclude(left), snapshotSelectionInclude(right)) &&
75
+ leftHashes.size === rightHashes.size &&
76
+ [...leftHashes].every(([filePath, hash]) => rightHashes.get(filePath) === hash)
77
+ );
80
78
  }
81
79
 
82
80
  function snapshotId() {
83
- return `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
81
+ return `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
84
82
  }
85
83
 
86
84
  export function regenerateSnapshotIdentity(snapshot: Snapshot): Snapshot {
87
- return {
88
- ...snapshot,
89
- id: snapshotId(),
90
- createdAt: new Date().toISOString(),
91
- machine: os.hostname(),
92
- };
85
+ return {
86
+ ...snapshot,
87
+ id: snapshotId(),
88
+ createdAt: new Date().toISOString(),
89
+ machine: os.hostname(),
90
+ };
93
91
  }
94
92
 
95
- export async function createSnapshot(
96
- profile: string,
97
- options: SnapshotOptions = {},
98
- ): Promise<Snapshot> {
99
- options.signal?.throwIfAborted();
100
- const include = effectiveInclude(options);
101
- const syncSessions = include.includes("sessions");
102
- const sessionDir = options.sessionDir ?? (await configuredSessionDir());
103
- options.signal?.throwIfAborted();
104
- const files = await collectFiles(agentDir(), { include, sessionDir, signal: options.signal });
105
- options.signal?.throwIfAborted();
106
- return {
107
- version: VERSION,
108
- id: snapshotId(),
109
- createdAt: new Date().toISOString(),
110
- machine: os.hostname(),
111
- profile,
112
- syncSessions,
113
- selection: selectionForSnapshot(include),
114
- files,
115
- };
93
+ export async function createSnapshot(profile: string, options: SnapshotOptions = {}): Promise<Snapshot> {
94
+ options.signal?.throwIfAborted();
95
+ const include = effectiveInclude(options);
96
+ const syncSessions = include.includes("sessions");
97
+ const sessionDir = options.sessionDir ?? (await configuredSessionDir());
98
+ options.signal?.throwIfAborted();
99
+ const files = await collectFiles(agentDir(), { include, sessionDir, signal: options.signal });
100
+ options.signal?.throwIfAborted();
101
+ return {
102
+ version: VERSION,
103
+ id: snapshotId(),
104
+ createdAt: new Date().toISOString(),
105
+ machine: os.hostname(),
106
+ profile,
107
+ syncSessions,
108
+ selection: selectionForSnapshot(include),
109
+ files,
110
+ };
116
111
  }
117
112
 
118
113
  function effectiveInclude(options: SnapshotOptions) {
119
- if (options.include) return options.include;
120
- return [
121
- ...normalizeSyncFiles(options.syncFiles),
122
- ...normalizeExtraFiles(options.extraFiles),
123
- ...(options.syncSessions ? ["sessions"] : []),
124
- ];
114
+ if (options.include) return options.include;
115
+ return [
116
+ ...normalizeSyncFiles(options.syncFiles),
117
+ ...normalizeExtraFiles(options.extraFiles),
118
+ ...(options.syncSessions ? ["sessions"] : []),
119
+ ];
125
120
  }
126
121
 
127
- export async function collectFiles(
128
- root: string,
129
- options: SnapshotOptions = {},
130
- ): Promise<SnapshotFile[]> {
131
- const signal = options.signal;
132
- signal?.throwIfAborted();
133
- const results: SnapshotFile[] = [];
134
- const entries = await fs.readdir(root, { withFileTypes: true });
135
- signal?.throwIfAborted();
136
- const selection = syncIncludeSelection(effectiveInclude(options));
137
- const selectedFiles = new Set<string>(selection.builtIns);
138
- for (const entry of entries) {
139
- if (entry.isDirectory() && TOP_LEVEL_DIRS.has(entry.name) && selectedFiles.has(entry.name)) {
140
- await collectDirectory(results, root, entry.name, { signal });
141
- }
142
- }
143
- for (const fileName of TOP_LEVEL_FILES) {
144
- if (!selectedFiles.has(fileName)) continue;
145
- const entry = selectTopLevelFileEntry(entries, fileName);
146
- if (entry) await addFile(results, root, entry.name, fileName, signal);
147
- }
148
- for (const relativePath of selection.custom) {
149
- await collectIncludedPath(results, root, relativePath, signal);
150
- }
151
- if (selection.sessions) {
152
- try {
153
- await collectDirectory(results, sessionStorageRoot(root, options.sessionDir), "", {
154
- sessionsOnly: true,
155
- virtualPrefix: "sessions",
156
- signal,
157
- });
158
- } catch (error) {
159
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
160
- }
161
- }
162
- signal?.throwIfAborted();
163
- return results.sort((left, right) => left.path.localeCompare(right.path));
122
+ export async function collectFiles(root: string, options: SnapshotOptions = {}): Promise<SnapshotFile[]> {
123
+ const signal = options.signal;
124
+ signal?.throwIfAborted();
125
+ const results: SnapshotFile[] = [];
126
+ const entries = await fs.readdir(root, { withFileTypes: true });
127
+ signal?.throwIfAborted();
128
+ const selection = syncIncludeSelection(effectiveInclude(options));
129
+ const selectedFiles = new Set<string>(selection.builtIns);
130
+ for (const entry of entries) {
131
+ if (entry.isDirectory() && TOP_LEVEL_DIRS.has(entry.name) && selectedFiles.has(entry.name)) {
132
+ await collectDirectory(results, root, entry.name, { signal });
133
+ }
134
+ }
135
+ for (const fileName of TOP_LEVEL_FILES) {
136
+ if (!selectedFiles.has(fileName)) continue;
137
+ const entry = selectTopLevelFileEntry(entries, fileName);
138
+ if (entry) await addFile(results, root, entry.name, fileName, signal);
139
+ }
140
+ for (const relativePath of selection.custom) {
141
+ await collectIncludedPath(results, root, relativePath, signal);
142
+ }
143
+ if (selection.sessions) {
144
+ try {
145
+ await collectDirectory(results, sessionStorageRoot(root, options.sessionDir), "", {
146
+ sessionsOnly: true,
147
+ virtualPrefix: "sessions",
148
+ signal,
149
+ });
150
+ } catch (error) {
151
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
152
+ }
153
+ }
154
+ signal?.throwIfAborted();
155
+ return results.sort((left, right) => left.path.localeCompare(right.path));
164
156
  }
165
157
 
166
- async function collectIncludedPath(
167
- results: SnapshotFile[],
168
- root: string,
169
- relativePath: string,
170
- signal?: AbortSignal,
171
- ) {
172
- signal?.throwIfAborted();
173
- const absolutePath = safeJoin(root, relativePath);
174
- try {
175
- const stat = await fs.lstat(absolutePath);
176
- signal?.throwIfAborted();
177
- if (stat.isFile()) await addFile(results, root, relativePath, relativePath, signal);
178
- else if (stat.isDirectory()) await collectDirectory(results, root, relativePath, { signal });
179
- } catch (error) {
180
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
181
- if (!relativePath.includes("/")) {
182
- const entries = await fs.readdir(root, { withFileTypes: true });
183
- signal?.throwIfAborted();
184
- const entry = selectTopLevelFileEntry(entries, relativePath);
185
- if (entry) await addFile(results, root, entry.name, relativePath, signal);
186
- }
187
- }
158
+ async function collectIncludedPath(results: SnapshotFile[], root: string, relativePath: string, signal?: AbortSignal) {
159
+ signal?.throwIfAborted();
160
+ const absolutePath = safeJoin(root, relativePath);
161
+ try {
162
+ const stat = await fs.lstat(absolutePath);
163
+ signal?.throwIfAborted();
164
+ if (stat.isFile()) await addFile(results, root, relativePath, relativePath, signal);
165
+ else if (stat.isDirectory()) await collectDirectory(results, root, relativePath, { signal });
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
168
+ if (!relativePath.includes("/")) {
169
+ const entries = await fs.readdir(root, { withFileTypes: true });
170
+ signal?.throwIfAborted();
171
+ const entry = selectTopLevelFileEntry(entries, relativePath);
172
+ if (entry) await addFile(results, root, entry.name, relativePath, signal);
173
+ }
174
+ }
188
175
  }
189
176
 
190
177
  async function collectDirectory(
191
- results: SnapshotFile[],
192
- root: string,
193
- relativeDirectory: string,
194
- options: { sessionsOnly?: boolean; virtualPrefix?: string; signal?: AbortSignal } = {},
178
+ results: SnapshotFile[],
179
+ root: string,
180
+ relativeDirectory: string,
181
+ options: { sessionsOnly?: boolean; virtualPrefix?: string; signal?: AbortSignal } = {},
195
182
  ) {
196
- options.signal?.throwIfAborted();
197
- const absoluteDirectory = path.join(root, relativeDirectory);
198
- for (const entry of await fs.readdir(absoluteDirectory, { withFileTypes: true })) {
199
- options.signal?.throwIfAborted();
200
- const relativePath = relativeDirectory ? posixJoin(relativeDirectory, entry.name) : entry.name;
201
- const snapshotPath = options.virtualPrefix
202
- ? posixJoin(options.virtualPrefix, relativePath)
203
- : relativePath;
204
- if (isDeniedPath(snapshotPath)) continue;
205
- if (entry.isDirectory()) {
206
- await collectDirectory(results, root, relativePath, options);
207
- } else if (entry.isFile() && (!options.sessionsOnly || isSessionFilePath(snapshotPath))) {
208
- await addFile(results, root, relativePath, snapshotPath, options.signal);
209
- }
210
- }
183
+ options.signal?.throwIfAborted();
184
+ const absoluteDirectory = path.join(root, relativeDirectory);
185
+ for (const entry of await fs.readdir(absoluteDirectory, { withFileTypes: true })) {
186
+ options.signal?.throwIfAborted();
187
+ const relativePath = relativeDirectory ? posixJoin(relativeDirectory, entry.name) : entry.name;
188
+ const snapshotPath = options.virtualPrefix ? posixJoin(options.virtualPrefix, relativePath) : relativePath;
189
+ if (isDeniedPath(snapshotPath)) continue;
190
+ if (entry.isDirectory()) {
191
+ await collectDirectory(results, root, relativePath, options);
192
+ } else if (entry.isFile() && (!options.sessionsOnly || isSessionFilePath(snapshotPath))) {
193
+ await addFile(results, root, relativePath, snapshotPath, options.signal);
194
+ }
195
+ }
211
196
  }
212
197
 
213
198
  async function addFile(
214
- results: SnapshotFile[],
215
- root: string,
216
- relativePath: string,
217
- snapshotPath = relativePath,
218
- signal?: AbortSignal,
199
+ results: SnapshotFile[],
200
+ root: string,
201
+ relativePath: string,
202
+ snapshotPath = relativePath,
203
+ signal?: AbortSignal,
219
204
  ) {
220
- signal?.throwIfAborted();
221
- if (!isSafeSnapshotPath(snapshotPath)) return;
222
- const absolutePath = safeJoin(root, relativePath);
223
- const content = await fs.readFile(absolutePath, { signal });
224
- signal?.throwIfAborted();
225
- results.push({
226
- path: snapshotPath,
227
- contentBase64: content.toString("base64"),
228
- sha256: sha256(content),
229
- });
205
+ signal?.throwIfAborted();
206
+ if (!isSafeSnapshotPath(snapshotPath)) return;
207
+ const absolutePath = safeJoin(root, relativePath);
208
+ const content = await fs.readFile(absolutePath, { signal });
209
+ signal?.throwIfAborted();
210
+ results.push({
211
+ path: snapshotPath,
212
+ contentBase64: content.toString("base64"),
213
+ sha256: sha256(content),
214
+ });
230
215
  }
231
216
 
232
217
  export function isSessionPath(relativePath: string) {
233
- return toPosix(relativePath).startsWith("sessions/");
218
+ return toPosix(relativePath).startsWith("sessions/");
234
219
  }
235
220
 
236
221
  export function isSessionFilePath(relativePath: string) {
237
- const normalized = toPosix(relativePath);
238
- return isSessionPath(normalized) && normalized.endsWith(".jsonl");
222
+ const normalized = toPosix(relativePath);
223
+ return isSessionPath(normalized) && normalized.endsWith(".jsonl");
239
224
  }
240
225
 
241
- export function sessionSnapshotPathFromAbsolute(
242
- sessionFile: string,
243
- configuredSessionDir?: string,
244
- ) {
245
- const relativePath = toPosix(
246
- path.relative(sessionStorageRoot(agentDir(), configuredSessionDir), sessionFile),
247
- );
248
- if (!relativePath || relativePath.startsWith("../") || path.posix.isAbsolute(relativePath)) {
249
- return undefined;
250
- }
251
- const snapshotPath = posixJoin("sessions", relativePath);
252
- return isSessionFilePath(snapshotPath) ? snapshotPath : undefined;
226
+ export function sessionSnapshotPathFromAbsolute(sessionFile: string, configuredSessionDir?: string) {
227
+ const relativePath = toPosix(path.relative(sessionStorageRoot(agentDir(), configuredSessionDir), sessionFile));
228
+ if (!relativePath || relativePath.startsWith("../") || path.posix.isAbsolute(relativePath)) {
229
+ return undefined;
230
+ }
231
+ const snapshotPath = posixJoin("sessions", relativePath);
232
+ return isSessionFilePath(snapshotPath) ? snapshotPath : undefined;
253
233
  }
254
234
 
255
235
  export function snapshotTarget(root: string, relativePath: string, configuredSessionDir?: string) {
256
- if (isSessionPath(relativePath)) {
257
- return safeJoin(
258
- sessionStorageRoot(root, configuredSessionDir),
259
- relativePath.slice("sessions/".length),
260
- );
261
- }
262
- return safeJoin(root, relativePath);
236
+ if (isSessionPath(relativePath)) {
237
+ return safeJoin(sessionStorageRoot(root, configuredSessionDir), relativePath.slice("sessions/".length));
238
+ }
239
+ return safeJoin(root, relativePath);
263
240
  }
264
241
 
265
242
  export function snapshotIncludesSessions(snapshot: Snapshot) {
266
- return (
267
- snapshot.syncSessions === true ||
268
- snapshotSelectionInclude(snapshot)?.includes("sessions") === true ||
269
- snapshot.files.some((file) => isSessionPath(file.path))
270
- );
243
+ return (
244
+ snapshot.syncSessions === true ||
245
+ snapshotSelectionInclude(snapshot)?.includes("sessions") === true ||
246
+ snapshot.files.some((file) => isSessionPath(file.path))
247
+ );
271
248
  }
272
249
 
273
250
  export function filterSnapshotForConfigPolicy(
274
- snapshot: Snapshot,
275
- config: SyncSelectionConfig,
276
- options: { regenerateId?: boolean } = {},
251
+ snapshot: Snapshot,
252
+ config: SyncSelectionConfig,
253
+ options: { regenerateId?: boolean } = {},
277
254
  ) {
278
- const include = includeFromSelectionConfig(config);
279
- const includePaths = customIncludePathsByLower(include);
280
- const filtered = {
281
- ...snapshot,
282
- syncSessions: include.includes("sessions") ? snapshot.syncSessions : false,
283
- selection: selectionForSnapshot(include),
284
- files: canonicalizeSnapshotFilesForConfig(snapshot.files, config, includePaths),
285
- };
286
- if (!options.regenerateId || snapshotsMatch(snapshot, filtered)) return filtered;
287
- return {
288
- ...filtered,
289
- id: snapshotId(),
290
- createdAt: new Date().toISOString(),
291
- machine: os.hostname(),
292
- };
255
+ const include = includeFromSelectionConfig(config);
256
+ const includePaths = customIncludePathsByLower(include);
257
+ const filtered = {
258
+ ...snapshot,
259
+ syncSessions: include.includes("sessions") ? snapshot.syncSessions : false,
260
+ selection: selectionForSnapshot(include),
261
+ files: canonicalizeSnapshotFilesForConfig(snapshot.files, config, includePaths),
262
+ };
263
+ if (!options.regenerateId || snapshotsMatch(snapshot, filtered)) return filtered;
264
+ return {
265
+ ...filtered,
266
+ id: snapshotId(),
267
+ createdAt: new Date().toISOString(),
268
+ machine: os.hostname(),
269
+ };
293
270
  }
294
271
 
295
272
  function canonicalizeSnapshotFilesForConfig(
296
- files: SnapshotFile[],
297
- config: SyncSelectionConfig,
298
- includePaths: Map<string, string>,
273
+ files: SnapshotFile[],
274
+ config: SyncSelectionConfig,
275
+ includePaths: Map<string, string>,
299
276
  ) {
300
- const configuredFiles: SnapshotFile[] = [];
301
- const extraCandidates = new Map<
302
- string,
303
- { exact: boolean; file: SnapshotFile; originalPath: string }
304
- >();
305
- for (const file of files) {
306
- const normalized = toPosix(file.path);
307
- if (!isSafeSnapshotPath(file.path) || !isConfiguredSnapshotPath(normalized, config)) {
308
- continue;
309
- }
310
- if (normalized.includes("/")) {
311
- configuredFiles.push(normalized === file.path ? file : { ...file, path: normalized });
312
- continue;
313
- }
314
- const topLevelPath = canonicalSnapshotPathForConfig(normalized, includePaths);
315
- const candidate = {
316
- exact: normalized === topLevelPath,
317
- file: { ...file, path: topLevelPath },
318
- originalPath: normalized,
319
- };
320
- const current = extraCandidates.get(topLevelPath.toLowerCase());
321
- if (!current || isPreferredExtraCandidate(candidate, current)) {
322
- extraCandidates.set(topLevelPath.toLowerCase(), candidate);
323
- }
324
- }
325
- return [
326
- ...configuredFiles,
327
- ...[...extraCandidates.values()].map((candidate) => candidate.file),
328
- ].sort((left, right) => left.path.localeCompare(right.path));
277
+ const configuredFiles: SnapshotFile[] = [];
278
+ const extraCandidates = new Map<string, { exact: boolean; file: SnapshotFile; originalPath: string }>();
279
+ for (const file of files) {
280
+ const normalized = toPosix(file.path);
281
+ if (!isSafeSnapshotPath(file.path) || !isConfiguredSnapshotPath(normalized, config)) {
282
+ continue;
283
+ }
284
+ if (normalized.includes("/")) {
285
+ configuredFiles.push(normalized === file.path ? file : { ...file, path: normalized });
286
+ continue;
287
+ }
288
+ const topLevelPath = canonicalSnapshotPathForConfig(normalized, includePaths);
289
+ const candidate = {
290
+ exact: normalized === topLevelPath,
291
+ file: { ...file, path: topLevelPath },
292
+ originalPath: normalized,
293
+ };
294
+ const current = extraCandidates.get(topLevelPath.toLowerCase());
295
+ if (!current || isPreferredExtraCandidate(candidate, current)) {
296
+ extraCandidates.set(topLevelPath.toLowerCase(), candidate);
297
+ }
298
+ }
299
+ return [...configuredFiles, ...[...extraCandidates.values()].map((candidate) => candidate.file)].sort((left, right) =>
300
+ left.path.localeCompare(right.path),
301
+ );
329
302
  }
330
303
 
331
304
  function isPreferredExtraCandidate(
332
- left: { exact: boolean; originalPath: string },
333
- right: { exact: boolean; originalPath: string },
305
+ left: { exact: boolean; originalPath: string },
306
+ right: { exact: boolean; originalPath: string },
334
307
  ) {
335
- if (left.exact !== right.exact) return left.exact;
336
- return left.originalPath.localeCompare(right.originalPath) < 0;
308
+ if (left.exact !== right.exact) return left.exact;
309
+ return left.originalPath.localeCompare(right.originalPath) < 0;
337
310
  }
338
311
 
339
312
  export function snapshotWithoutSessions(snapshot: Snapshot) {
340
- const files = snapshot.files.filter((file) => !isSessionPath(file.path));
341
- const include = snapshotSelectionInclude(snapshot)?.filter((item) => item !== "sessions");
342
- if (
343
- files.length === snapshot.files.length &&
344
- snapshot.syncSessions !== true &&
345
- include?.length === snapshot.selection?.include.length
346
- ) {
347
- return snapshot;
348
- }
349
- return {
350
- ...snapshot,
351
- id: snapshotId(),
352
- createdAt: new Date().toISOString(),
353
- machine: os.hostname(),
354
- syncSessions: false,
355
- ...(include ? { selection: selectionForSnapshot(include) } : {}),
356
- files,
357
- };
313
+ const files = snapshot.files.filter((file) => !isSessionPath(file.path));
314
+ const include = snapshotSelectionInclude(snapshot)?.filter((item) => item !== "sessions");
315
+ if (
316
+ files.length === snapshot.files.length &&
317
+ snapshot.syncSessions !== true &&
318
+ include?.length === snapshot.selection?.include.length
319
+ ) {
320
+ return snapshot;
321
+ }
322
+ return {
323
+ ...snapshot,
324
+ id: snapshotId(),
325
+ createdAt: new Date().toISOString(),
326
+ machine: os.hostname(),
327
+ syncSessions: false,
328
+ ...(include ? { selection: selectionForSnapshot(include) } : {}),
329
+ files,
330
+ };
358
331
  }
359
332
 
360
333
  export function scanSnapshot(snapshot: Snapshot) {
361
- const findings: string[] = [];
362
- for (const file of snapshot.files) {
363
- const content = Buffer.from(file.contentBase64, "base64");
364
- if (content.includes(0)) continue;
365
- const text = content.toString("utf8");
366
- for (const pattern of SECRET_PATTERNS) {
367
- if (pattern.test(text)) {
368
- findings.push(file.path);
369
- break;
370
- }
371
- }
372
- }
373
- return findings;
334
+ const findings: string[] = [];
335
+ for (const file of snapshot.files) {
336
+ const content = Buffer.from(file.contentBase64, "base64");
337
+ if (content.includes(0)) continue;
338
+ const text = content.toString("utf8");
339
+ for (const pattern of SECRET_PATTERNS) {
340
+ if (pattern.test(text)) {
341
+ findings.push(file.path);
342
+ break;
343
+ }
344
+ }
345
+ }
346
+ return findings;
374
347
  }
375
348
 
376
- export function mergeRemotePreservedFiles(
377
- local: Snapshot,
378
- remote: Snapshot,
379
- config: SyncSelectionConfig,
380
- ) {
381
- const localPathNames = new Set(local.files.map((file) => file.path.toLowerCase()));
382
- const preservedPathNames = new Set<string>();
383
- const preserved = remote.files.filter((file) => {
384
- const normalized = toPosix(file.path);
385
- const lower = normalized.toLowerCase();
386
- if (
387
- localPathNames.has(lower) ||
388
- preservedPathNames.has(lower) ||
389
- !isSafeSnapshotPath(file.path) ||
390
- isConfiguredSnapshotPath(normalized, config) ||
391
- !isPreservableUnmanagedSnapshotPath(normalized)
392
- ) {
393
- return false;
394
- }
395
- preservedPathNames.add(lower);
396
- return true;
397
- });
398
- if (preserved.length === 0) return local;
399
- return {
400
- ...local,
401
- id: snapshotId(),
402
- createdAt: new Date().toISOString(),
403
- machine: os.hostname(),
404
- syncSessions: snapshotIncludesSessions(local) || snapshotIncludesSessions(remote),
405
- files: [...local.files, ...preserved].sort((left, right) =>
406
- left.path.localeCompare(right.path),
407
- ),
408
- };
349
+ export function mergeRemotePreservedFiles(local: Snapshot, remote: Snapshot, config: SyncSelectionConfig) {
350
+ const localPathNames = new Set(local.files.map((file) => file.path.toLowerCase()));
351
+ const preservedPathNames = new Set<string>();
352
+ const preserved = remote.files.filter((file) => {
353
+ const normalized = toPosix(file.path);
354
+ const lower = normalized.toLowerCase();
355
+ if (
356
+ localPathNames.has(lower) ||
357
+ preservedPathNames.has(lower) ||
358
+ !isSafeSnapshotPath(file.path) ||
359
+ isConfiguredSnapshotPath(normalized, config) ||
360
+ !isPreservableUnmanagedSnapshotPath(normalized)
361
+ ) {
362
+ return false;
363
+ }
364
+ preservedPathNames.add(lower);
365
+ return true;
366
+ });
367
+ if (preserved.length === 0) return local;
368
+ return {
369
+ ...local,
370
+ id: snapshotId(),
371
+ createdAt: new Date().toISOString(),
372
+ machine: os.hostname(),
373
+ syncSessions: snapshotIncludesSessions(local) || snapshotIncludesSessions(remote),
374
+ files: [...local.files, ...preserved].sort((left, right) => left.path.localeCompare(right.path)),
375
+ };
409
376
  }
410
377
 
411
378
  export function mergeRemoteSessionFiles(local: Snapshot, remote: Snapshot) {
412
- const remoteSessions = remote.files.filter((file) => {
413
- const normalized = toPosix(file.path);
414
- return isSessionFilePath(normalized) && isSafeSnapshotPath(file.path);
415
- });
416
- if (remoteSessions.length === 0 && !snapshotIncludesSessions(remote)) return local;
417
- const localInclude = snapshotSelectionInclude(local);
418
- return {
419
- ...local,
420
- id: snapshotId(),
421
- createdAt: new Date().toISOString(),
422
- machine: os.hostname(),
423
- syncSessions: true,
424
- ...(localInclude
425
- ? {
426
- selection: selectionForSnapshot(
427
- localInclude.includes("sessions") ? localInclude : [...localInclude, "sessions"],
428
- ),
429
- }
430
- : {}),
431
- files: [...local.files.filter((file) => !isSessionPath(file.path)), ...remoteSessions].sort(
432
- (left, right) => left.path.localeCompare(right.path),
433
- ),
434
- };
379
+ const remoteSessions = remote.files.filter((file) => {
380
+ const normalized = toPosix(file.path);
381
+ return isSessionFilePath(normalized) && isSafeSnapshotPath(file.path);
382
+ });
383
+ if (remoteSessions.length === 0 && !snapshotIncludesSessions(remote)) return local;
384
+ const localInclude = snapshotSelectionInclude(local);
385
+ return {
386
+ ...local,
387
+ id: snapshotId(),
388
+ createdAt: new Date().toISOString(),
389
+ machine: os.hostname(),
390
+ syncSessions: true,
391
+ ...(localInclude
392
+ ? {
393
+ selection: selectionForSnapshot(
394
+ localInclude.includes("sessions") ? localInclude : [...localInclude, "sessions"],
395
+ ),
396
+ }
397
+ : {}),
398
+ files: [...local.files.filter((file) => !isSessionPath(file.path)), ...remoteSessions].sort((left, right) =>
399
+ left.path.localeCompare(right.path),
400
+ ),
401
+ };
435
402
  }
436
403
 
437
404
  function sameOptionalInclude(left: string[] | undefined, right: string[] | undefined) {
438
- if (!left || !right) return left === right;
439
- return left.length === right.length && left.every((item, index) => item === right[index]);
405
+ if (!left || !right) return left === right;
406
+ return left.length === right.length && left.every((item, index) => item === right[index]);
440
407
  }