@narumitw/pi-sync 0.49.15 → 0.50.0

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 (209) hide show
  1. package/README.md +49 -16
  2. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  3. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  4. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  5. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  6. package/dist/chunks/chunk-6UHKGALE.ts +134 -0
  7. package/dist/chunks/chunk-6UHKGALE.ts.map +7 -0
  8. package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
  13. package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
  14. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  15. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  16. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  17. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  18. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  19. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  20. package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
  23. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  24. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  25. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  26. package/dist/chunks/chunk-JSYMGJQQ.ts +51 -0
  27. package/dist/chunks/chunk-JSYMGJQQ.ts.map +7 -0
  28. package/dist/chunks/chunk-L7TBO32M.ts +25 -0
  29. package/dist/chunks/chunk-L7TBO32M.ts.map +7 -0
  30. package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
  31. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  32. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  33. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  34. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  35. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  36. package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
  37. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  38. package/dist/chunks/chunk-OUHGBFYF.ts +1255 -0
  39. package/dist/chunks/chunk-OUHGBFYF.ts.map +7 -0
  40. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  41. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  42. package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
  43. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  44. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  45. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  46. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  47. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  48. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  49. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  50. package/dist/chunks/{chunk-44LPGQTI.ts → chunk-X5OM6GXT.ts} +9 -7
  51. package/dist/chunks/chunk-X5OM6GXT.ts.map +7 -0
  52. package/dist/chunks/chunk-XBX4HBLG.ts +57 -0
  53. package/dist/chunks/chunk-XBX4HBLG.ts.map +7 -0
  54. package/dist/chunks/{chunk-ARN2672F.ts → chunk-YKJHPTGO.ts} +23 -55
  55. package/dist/chunks/chunk-YKJHPTGO.ts.map +7 -0
  56. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  57. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  58. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  59. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  60. package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
  61. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  62. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-OQW46G2X.ts} +42 -17
  63. package/dist/chunks/git-backend-OQW46G2X.ts.map +7 -0
  64. package/dist/chunks/manager-result-dispatcher-U23ODD32.ts +22 -0
  65. package/dist/chunks/manager-ui-ABATHC36.ts +1680 -0
  66. package/dist/chunks/manager-ui-ABATHC36.ts.map +7 -0
  67. package/dist/chunks/remote-selection-ui-ZE3TY67S.ts +21 -0
  68. package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  71. package/dist/chunks/setup-wizard-VB3VDOV5.ts +89 -0
  72. package/dist/chunks/setup-wizard-VB3VDOV5.ts.map +7 -0
  73. package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-ONJ27QRL.ts} +374 -294
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
  78. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  79. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  80. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  81. package/dist/index.ts +477 -342
  82. package/dist/index.ts.map +4 -4
  83. package/docs/settings.md +30 -6
  84. package/package.json +1 -1
  85. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  86. package/src/backends/backend-identity.ts +40 -0
  87. package/src/backends/backend-types.ts +81 -0
  88. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  89. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  90. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  91. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  92. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  93. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  94. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  95. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  96. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  97. package/src/commands/command-execution.ts +330 -0
  98. package/src/commands/command-handler.ts +201 -0
  99. package/src/commands/command-types.ts +18 -0
  100. package/src/{command.ts → commands/command.ts} +2 -1
  101. package/src/commands/setup-completions.ts +6 -0
  102. package/src/settings/config.ts +238 -0
  103. package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
  104. package/src/settings/settings-store.ts +109 -0
  105. package/src/settings/settings-types.ts +128 -0
  106. package/src/settings/settings-validation.ts +443 -0
  107. package/src/snapshot/session-paths.ts +84 -0
  108. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  109. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  110. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  111. package/src/snapshot/snapshot-types.ts +38 -0
  112. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  113. package/src/state/json-file.ts +20 -0
  114. package/src/{lock.ts → state/lock.ts} +3 -2
  115. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  116. package/src/state/state-types.ts +20 -0
  117. package/src/state/sync-state-store.ts +62 -0
  118. package/src/sync/automatic-sync.ts +103 -0
  119. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  120. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  121. package/src/sync/signals.ts +10 -0
  122. package/src/sync/startup-check.ts +128 -0
  123. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  124. package/src/sync/sync-error-guidance.ts +53 -0
  125. package/src/sync/sync-inspection.ts +51 -0
  126. package/src/sync/sync-loaders.ts +54 -0
  127. package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
  128. package/src/sync/sync-operations.ts +12 -0
  129. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  130. package/src/sync/sync-queries.ts +235 -0
  131. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  132. package/src/sync-extension.ts +64 -708
  133. package/src/sync.ts +8 -8
  134. package/src/types.ts +47 -286
  135. package/src/ui/automatic-sync-summary.ts +6 -0
  136. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  137. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  138. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  139. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  140. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  141. package/src/ui/manager-ui.ts +386 -0
  142. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  143. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  144. package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
  145. package/src/ui/setup/git-ui.ts +293 -0
  146. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  147. package/src/ui/setup/s3-ui.ts +217 -0
  148. package/src/ui/setup/setup-actions.ts +127 -0
  149. package/src/ui/setup/setup-location-ui.ts +167 -0
  150. package/src/ui/setup/setup-name-ui.ts +34 -0
  151. package/src/ui/setup/setup-prompts.ts +92 -0
  152. package/src/ui/setup/setup-review.ts +70 -0
  153. package/src/ui/setup/setup-switcher.ts +181 -0
  154. package/src/ui/setup/setup-wizard.ts +39 -0
  155. package/src/ui/setup/text-input.ts +78 -0
  156. package/src/ui/setup/webdav-ui.ts +330 -0
  157. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  158. package/src/{sync-attention.ts → ui/sync-attention.ts} +75 -6
  159. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  160. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  161. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  162. package/src/ui/terminal-text.ts +4 -0
  163. package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
  164. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  165. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  168. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  169. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  170. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  171. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  172. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  173. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  174. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  175. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  176. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  177. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  178. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  179. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  180. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  185. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  186. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  188. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  189. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -923
  192. package/src/git-ui.ts +0 -307
  193. package/src/manager-helpers.ts +0 -64
  194. package/src/manager-ui.ts +0 -997
  195. package/src/webdav-ui.ts +0 -421
  196. /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-FEP4L5TR.ts.map} +0 -0
  201. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  202. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  203. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  204. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  205. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  206. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  207. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  208. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  209. /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
package/README.md CHANGED
@@ -41,10 +41,23 @@ Extensions run with Pi's permissions, so install only packages from sources you
41
41
  ## 🚀 Quick start
42
42
 
43
43
  Run `/sync` and choose **Set up sync**.
44
- Before saving, review the storage connection, exact remote path, included content, automatic-sync choice, and masked credentials.
44
+ Name the setup once; its first storage connection uses the same name.
45
+ Input prompts show examples or an explicit default that you can accept by submitting an empty value; cancelling never accepts a default.
46
+ Remote URLs, endpoints, usernames, and credentials require your own values, not the displayed examples.
47
+ All new setups default to storage path `./`: the Git repository root, the WebDAV collection URL, or the selected R2/S3 bucket root.
48
+ Git also suggests branch `main`.
49
+ Use different folders or prefixes for independent setups sharing a WebDAV collection or bucket; existing settings and paths are unchanged.
50
+ If another configured setup already uses the chosen location, setup asks for a different path before the content selection and review; Git requires a different branch, even when the directory differs.
51
+ Every new setup asks for included content and automatic sync; automatic sync is off unless you enable it. Recommended includes nine Pi roots; Minimal includes `settings.json` and `AGENTS.md`. Sessions stay off unless explicitly included with a privacy acknowledgement.
52
+ Before saving, scroll through the exact destination, every included path, automatic-sync choice, and hidden-credential summary. R2/S3 always asks for an existing bucket; an example bucket name is not a default.
53
+
54
+ Invalid fields ask for a correction without restarting setup. A temporary local save failure keeps the review for another Save; changed settings require reopening and reviewing the current values. Saving only changes local settings, not remote data. A connection saved through **Add a new storage connection…** remains saved if you later cancel the setup.
45
55
 
46
56
  Buckets and remote repositories must already exist.
47
57
  Git uses existing non-interactive SSH or credential-helper configuration and never stores Git credentials.
58
+ It owns the entire selected branch: use an empty repository or a new branch if `main` already contains unrelated content.
59
+ At `./`, Git stores `manifest.json` and `files/` at the repository root; it does not modify your local working tree.
60
+ WebDAV and R2/S3 store `latest.json`, `history.json`, and `snapshots/` directly under their selected root, not under a literal `./` prefix.
48
61
 
49
62
  ## 🧭 Manager, conflicts, and recovery
50
63
 
@@ -54,7 +67,7 @@ The `/sync` manager shows local state without contacting remote storage:
54
67
  Current sync setup: home
55
68
  Storage: Cloudflare R2 · r2 · personal-pi
56
69
  Included: 5 built-in groups · 0 extra files · Sessions off
57
- Automatic sync: On
70
+ Automatic sync: On (startup check; shutdown pushes selected content if sessions included)
58
71
  Remote status: Not checked
59
72
  ```
60
73
 
@@ -63,9 +76,11 @@ Primary actions include **Sync now**, **Switch sync setup**, **Status & changes*
63
76
  After Pi Sync detects an included-content mismatch, the manager shows **Sync status: Review needed** and puts **Review synced content (recommended)** first.
64
77
  **Sync now** remains unavailable until you review the mismatch.
65
78
  Opening the manager does not make another remote request.
66
- The manager also provides setup and connection details, history, and recovery.
79
+ Under **More…**, **Storage connections** holds reusable server addresses and sign-in details; **Sync setups** holds content, destination, and automatic-sync choices. **Edit storage location…** changes only bucket, branch, or path, not the connection or included content. Use **Settings** for the current setup's content and automatic sync; make another setup current before changing those settings.
67
80
 
68
- On secondary screens, **Back** and Escape return to the previous screen, while Ctrl+C closes the flow.
81
+ **More… → Check setup** (also `/sync doctor`) reports configuration and backend-specific access checks. S3/R2 performs a read-only request: success does not prove write access or snapshot validity, and an ambiguous 404 does not prove a bucket is missing. Check the indicated credentials, address, bucket, or path before retrying. Git checks remote reads and the local cache, not write access. WebDAV uses an isolated write/cleanup probe and repairs the active snapshot's history entry if missing. The existing **History & recovery** route remains available.
82
+
83
+ On secondary menus, **Back** and Escape return to the previous screen. In setup inputs and save reviews, cancellation (including Ctrl+C) discards the current unsaved draft and returns to its owning menu. Session replacement or shutdown closes the owning flow.
69
84
  Specialized operation and masked-credential prompts show the effective cancellation bindings and keep Ctrl+C as a hard-cancel input when Back is remapped.
70
85
  Destructive, credential-bearing, and externally visible operations show exact previews and confirmations.
71
86
 
@@ -102,7 +117,6 @@ Then choose one action:
102
117
  - **Keep this device's content list and update remote…** opens the existing `push --force` preparation and exact confirmation without `--yes`.
103
118
  Cancelling preparation or confirmation returns to the content-list choice without changing remote data.
104
119
  - **Cancel** returns to the manager without changing settings, files, remote data, or sync state.
105
- - **Later** appears when automatic startup sync detected the mismatch and returns immediately to the Pi editor without changing anything.
106
120
 
107
121
  Choose **Review differences (recommended)** in an ordinary file-direction conflict to inspect the exact affected paths without changing local files, remote data, or sync state.
108
122
  Then choose one reviewed direction:
@@ -117,15 +131,14 @@ Then choose one reviewed direction:
117
131
  Cancelling a preparation or confirmation returns to conflict resolution with no side effects.
118
132
  Back returns to the sync manager, and Ctrl+C closes the complete flow.
119
133
 
120
- When automatic startup sync detects an explicit content-list mismatch in TUI mode, it opens **Synced content differs** once for that session instead of ending at a warning.
121
- Choosing **Later** or closing the flow leaves a compact **Pi Sync needs review** widget above the editor and a **review needed** footer status.
122
- The attention state is in memory only, contains no credentials or file contents, and is revalidated before any settings or remote mutation.
123
- It clears after verified resolution, invalidation, session replacement, or shutdown.
134
+ Startup checks never open a dialog. Detected changes leave a compact hint above the editor; open `/sync` when ready. An included-content mismatch puts **Review synced content (recommended)** in the manager, where a fresh review verifies the remote snapshot before offering changes.
135
+ Check results are advisory observations against the last sync baseline, not proof of a file conflict or current equality. Opening the manager uses local information and shows when the check completed; it does not contact remote storage. Transfer actions recheck current content.
136
+ Attention stays in memory and is invalidated by relevant settings/state changes or a foreground transfer's commit boundary, and cleared on session replacement or shutdown. Cancelling a review or a failure before commit preserves a still-valid hint.
124
137
 
125
138
  Interactive TUI `/sync sync`, `/sync pull`, and `/sync push` routes without `--yes` open the same review flow when they detect the mismatch.
126
139
  Explicit `--yes` routes remain non-interactive and report exact remote-only, device-only, or order-only guidance while leaving visible attention for later review.
127
140
  Shutdown automatic sync never opens a dialog because Pi is exiting.
128
- RPC startup mismatch review remains read-only and notification-based.
141
+ RPC startup check results use nonblocking notifications; included-content review remains read-only.
129
142
  Print and JSON modes do not support `/sync` because UI output is not observable there.
130
143
 
131
144
  ## ⚙️ Settings
@@ -133,7 +146,18 @@ Print and JSON modes do not support `/sync` because UI output is not observable
133
146
  Run `/sync` → **Set up sync** to create the canonical private user file at `<getAgentDir()>/pi-sync.json` (normally `~/.pi/agent/pi-sync.json`).
134
147
  Use **Settings** to manage an existing setup.
135
148
  Missing settings stay unconfigured without creating files or locks.
136
- **Settings → Skip secret scan** defaults to **Off**; enable it only after reviewing the destination and selected content because it disables push scanning for every setup.
149
+
150
+ ### Background startup checks
151
+
152
+ With **Automatic sync** enabled, session startup schedules a background check instead of waiting for a transfer. Pi remains usable while Git, WebDAV, R2, or S3 checks local hashes and remote metadata. Unchanged results are quiet; changes or failures provide `/sync` guidance without opening a dialog. Use **Sync now** to start a reviewed transfer, or `/sync status` to retry a check. Foreground `/sync` cancels and drains the background check before starting.
153
+
154
+ Checks run once per session start, including `/reload`, new, resumed, and forked sessions, in TUI and RPC only. Print/JSON skip startup checks. There is no polling or automatic check retry loop. The overall deadline is 30 seconds, followed by underlying cleanup where needed; Git process termination and temporary-ref cleanup can take additional time. Git checks can still fetch objects and update the extension's private bare cache, but never push, apply managed files, update the sync baseline, or reload Pi.
155
+
156
+ **Compatibility change:** the existing `sync.automatic` boolean and Off default are unchanged, but On no longer performs startup push/pull. Startup is not guaranteed to use the latest remote content. Local transaction recovery remains an awaited safety barrier and can restore interrupted file changes before use; existing legacy settings-file initialization is also retained.
157
+
158
+ Shutdown behavior is unchanged: when **Automatic sync** is On and sessions are included, pi-sync can automatically push the selected content, not only session files. This also applies to headless modes; shutdown never opens a dialog, and `/reload` skips this push. Turning the setting Off disables both future startup checks and automatic shutdown pushes.
159
+
160
+ **Settings → Skip secret scan (all setups)** defaults to **Off**; enable it only after reviewing the destination and selected content because it disables push scanning for every setup.
137
161
  See [Secret scanning](./docs/settings.md#secret-scanning) for the setting and diagnostic behavior.
138
162
 
139
163
  A minimal Git setup uses an existing private remote and keeps automatic sync off:
@@ -172,7 +196,7 @@ Malformed, invalid, unsupported, symlinked, or concurrently changed documents re
172
196
  Version 1, version 2, and non-empty unversioned settings require manual recovery rather than automatic migration.
173
197
 
174
198
  Adding `sessions` can upload prompts, tool output, paths, images, and secrets; interactive flows require a privacy acknowledgement.
175
- Automatic sync and pull pause when the remote included-content policy differs instead of silently expanding local scope.
199
+ Startup checks report included-content differences without changing anything. Transfers retain their existing included-content policy checks instead of silently expanding local scope.
176
200
 
177
201
  Read the [settings reference](./docs/settings.md) for complete S3/R2, Git, and WebDAV examples, backend fields, included-content rules, legacy paths, and recovery steps.
178
202
 
@@ -224,7 +248,7 @@ The private bare cache under `<agent-dir>/pi-sync/git/` is rebuildable.
224
248
 
225
249
  WebDAV requires HTTPS except loopback tests.
226
250
  URL credentials, query strings, fragments, unsafe redirects, weak/missing ETags, and ignored conditional headers fail closed.
227
- `/sync doctor` verifies collection and conditional-write behavior with an isolated probe.
251
+ `/sync doctor` verifies collection and conditional-write behavior with an isolated probe, then repairs a missing active-snapshot history entry.
228
252
 
229
253
  S3/R2 stages immutable bundles, rechecks the visible head before publication, and verifies afterward.
230
254
  Unlike Git/WebDAV, generic S3 does not provide an atomic compare-and-swap for `latest.json`; status review remains important for simultaneous writers.
@@ -256,16 +280,23 @@ Preserve both roots before manual recovery; with every Pi process closed and no
256
280
 
257
281
  Pi exposes terminal components rather than a semantic or ARIA tree.
258
282
  Release checks cover textual state, keyboard operation, Escape and Back behavior, control escaping, and narrow rendering.
259
- Critical meaning appears in text such as `(current)`, `Review needed`, `Later`, `Warning`, `Invalid`, `Saved`, `Cancelled`, and `Applied`.
283
+ Critical meaning appears in text such as `(current)`, `Review needed`, `No startup transfer`, `Warning`, `Invalid`, `Saved`, `Cancelled`, and `Applied`.
260
284
  Color is supplementary, and the attention widget is informational rather than interactive.
261
285
 
262
286
  ## 🗂️ Package layout
263
287
 
264
288
  ```text
265
289
  packages/pi-sync/
266
- ├── src/ # Backends, settings, snapshots, and recovery modules
290
+ ├── src/
267
291
  │ ├── index.ts # Thin Pi entrypoint
268
- │ └── sync-extension.ts # Sync lifecycle and lazy loading
292
+ │ ├── sync-extension.ts # Registration and session lifecycle ownership
293
+ │ ├── commands/ # Parsing, execution, and attention dispatch
294
+ │ ├── settings/ # Schema, validation, resolution, and persistence
295
+ │ ├── sync/ # Queries, mutations, policy, and lazy loaders
296
+ │ ├── snapshot/ # Collection, session paths, apply, and recovery
297
+ │ ├── backends/ # Contract and Git, S3, and WebDAV transports
298
+ │ ├── state/ # Sync-state persistence, locks, and migration
299
+ │ └── ui/ # Manager, settings, reviews, and setup flows
269
300
  ├── dist/ # Generated Jiti runtime
270
301
  ├── scripts/build-runtime.mjs # Runtime builder
271
302
  ├── docs/ # Published reference documentation
@@ -273,6 +304,8 @@ packages/pi-sync/
273
304
  ```
274
305
 
275
306
  The generated runtime is built from `src/index.ts` and does not import back into `src`.
307
+ Internal modules use direct imports from their owners; `src/sync.ts` and `src/types.ts` retain compatibility exports.
308
+ Backend transports do not depend on UI, and first-use operation and setup implementations remain lazy.
276
309
 
277
310
  ## 🔎 Keywords
278
311
 
@@ -0,0 +1,29 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/sync/sync-errors.ts
5
+ var SetupPullRequiresUiError = class extends Error {
6
+ };
7
+ var SyncDecisionRequiredError = class extends Error {
8
+ decision;
9
+ constructor(decision) {
10
+ super(decision.directMessage);
11
+ this.name = "SyncDecisionRequiredError";
12
+ this.decision = decision;
13
+ }
14
+ };
15
+ function isSyncDecisionRequiredError(error) {
16
+ return error instanceof SyncDecisionRequiredError;
17
+ }
18
+ function errorMessage(error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return message.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
21
+ }
22
+
23
+ export {
24
+ SetupPullRequiresUiError,
25
+ SyncDecisionRequiredError,
26
+ isSyncDecisionRequiredError,
27
+ errorMessage
28
+ };
29
+ //# sourceMappingURL=chunk-2B2ELUF5.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync/sync-errors.ts"],
4
+ "sourcesContent": ["import type { SyncDecision } from \"./sync-decision.js\";\n\nexport class SetupPullRequiresUiError extends Error {}\n\nexport class SyncDecisionRequiredError extends Error {\n\treadonly decision: SyncDecision;\n\n\tconstructor(decision: SyncDecision) {\n\t\tsuper(decision.directMessage);\n\t\tthis.name = \"SyncDecisionRequiredError\";\n\t\tthis.decision = decision;\n\t}\n}\n\nexport function isSyncDecisionRequiredError(error: unknown): error is SyncDecisionRequiredError {\n\treturn error instanceof SyncDecisionRequiredError;\n}\n\nexport function errorMessage(error: unknown) {\n\tconst message = error instanceof Error ? error.message : String(error);\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.\n\treturn message.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/gu, \"?\");\n}\n"],
5
+ "mappings": ";;;;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAC;AAE9C,IAAM,4BAAN,cAAwC,MAAM;AAAA,EAC3C;AAAA,EAET,YAAY,UAAwB;AACnC,UAAM,SAAS,aAAa;AAC5B,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EACjB;AACD;AAEO,SAAS,4BAA4B,OAAoD;AAC/F,SAAO,iBAAiB;AACzB;AAEO,SAAS,aAAa,OAAgB;AAC5C,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAErE,SAAO,QAAQ,QAAQ,kCAAkC,GAAG;AAC7D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,115 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ normalizeGitRemoteIdentity
5
+ } from "./chunk-TEN2ZBJV.ts";
6
+
7
+ // src/backends/webdav/webdav-config.ts
8
+ var DEFAULT_PATH = "pi-sync";
9
+ function normalizeWebDavIdentityUrl(value) {
10
+ try {
11
+ const url = new URL(value.trim());
12
+ url.username = "";
13
+ url.password = "";
14
+ url.search = "";
15
+ url.hash = "";
16
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
17
+ return url.toString();
18
+ } catch {
19
+ return value.trim();
20
+ }
21
+ }
22
+ function normalizeWebDavUrl(value) {
23
+ const normalized = normalizeOptionalString(value);
24
+ if (!normalized) return void 0;
25
+ let url;
26
+ try {
27
+ url = new URL(normalized);
28
+ } catch {
29
+ throw new Error("Invalid pi-sync WebDAV URL.");
30
+ }
31
+ if (url.username || url.password || url.search || url.hash) {
32
+ throw new Error(
33
+ "Invalid pi-sync WebDAV URL: credentials, query, and fragment are not allowed."
34
+ );
35
+ }
36
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1";
37
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
38
+ throw new Error("Invalid pi-sync WebDAV URL: HTTPS is required except for loopback.");
39
+ }
40
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
41
+ return url.toString();
42
+ }
43
+ function normalizeWebDavPath(value) {
44
+ if (value?.trim() === "." || value?.trim() === "./") return "./";
45
+ const normalized = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_PATH);
46
+ if (!normalized || normalized.includes("\\") || hasControlCharacter(normalized) || normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
47
+ throw new Error("Invalid pi-sync WebDAV path.");
48
+ }
49
+ return normalized;
50
+ }
51
+ function validateWebDavCredentials(username, password) {
52
+ if (username.includes(":") || hasControlCharacter(username) || password !== void 0 && hasControlCharacter(password)) {
53
+ throw new Error("Invalid pi-sync WebDAV credentials.");
54
+ }
55
+ }
56
+ function normalizeOptionalString(value) {
57
+ const normalized = value?.trim();
58
+ return normalized || void 0;
59
+ }
60
+ function trimSlashes(value) {
61
+ return value.replace(/^\/+|\/+$/gu, "");
62
+ }
63
+ function hasControlCharacter(value) {
64
+ return [...value].some((character) => {
65
+ const code = character.codePointAt(0) ?? 0;
66
+ return code < 32 || code >= 127 && code <= 159;
67
+ });
68
+ }
69
+
70
+ // src/backends/backend-identity.ts
71
+ function backendIdentityCoordinates(config) {
72
+ switch (config.backend.type) {
73
+ case "s3":
74
+ return JSON.stringify([
75
+ "s3",
76
+ normalizeEndpointIdentity(config.backend.profile.endpoint),
77
+ config.backend.destination.bucket,
78
+ config.storagePath
79
+ ]);
80
+ case "git":
81
+ return JSON.stringify([
82
+ "git",
83
+ normalizeGitRemoteIdentity(config.backend.profile.remote),
84
+ config.backend.destination.branch,
85
+ config.storagePath
86
+ ]);
87
+ case "webdav":
88
+ return JSON.stringify([
89
+ "webdav",
90
+ normalizeWebDavIdentityUrl(config.backend.profile.url),
91
+ config.backend.profile.username,
92
+ config.storagePath
93
+ ]);
94
+ }
95
+ }
96
+ function normalizeEndpointIdentity(endpoint) {
97
+ try {
98
+ const url = new URL(endpoint.trim());
99
+ url.hostname = url.hostname.toLowerCase();
100
+ url.pathname = url.pathname.replace(/\/+$/gu, "");
101
+ return url.toString().replace(/\/$/u, "");
102
+ } catch {
103
+ return endpoint.trim();
104
+ }
105
+ }
106
+
107
+ export {
108
+ normalizeWebDavIdentityUrl,
109
+ normalizeWebDavUrl,
110
+ normalizeWebDavPath,
111
+ validateWebDavCredentials,
112
+ backendIdentityCoordinates,
113
+ normalizeEndpointIdentity
114
+ };
115
+ //# sourceMappingURL=chunk-6LP3MCR2.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backends/webdav/webdav-config.ts", "../../src/backends/backend-identity.ts"],
4
+ "sourcesContent": ["const DEFAULT_PATH = \"pi-sync\";\n\nexport function normalizeWebDavIdentityUrl(value: string) {\n\ttry {\n\t\tconst url = new URL(value.trim());\n\t\turl.username = \"\";\n\t\turl.password = \"\";\n\t\turl.search = \"\";\n\t\turl.hash = \"\";\n\t\turl.pathname = `${url.pathname.replace(/\\/+$/u, \"\")}/`;\n\t\treturn url.toString();\n\t} catch {\n\t\treturn value.trim();\n\t}\n}\n\nexport function normalizeWebDavUrl(value: string | undefined) {\n\tconst normalized = normalizeOptionalString(value);\n\tif (!normalized) return undefined;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(normalized);\n\t} catch {\n\t\tthrow new Error(\"Invalid pi-sync WebDAV URL.\");\n\t}\n\tif (url.username || url.password || url.search || url.hash) {\n\t\tthrow new Error(\n\t\t\t\"Invalid pi-sync WebDAV URL: credentials, query, and fragment are not allowed.\",\n\t\t);\n\t}\n\tconst loopback =\n\t\turl.hostname === \"127.0.0.1\" ||\n\t\turl.hostname === \"localhost\" ||\n\t\turl.hostname === \"[::1]\" ||\n\t\turl.hostname === \"::1\";\n\tif (url.protocol !== \"https:\" && !(url.protocol === \"http:\" && loopback)) {\n\t\tthrow new Error(\"Invalid pi-sync WebDAV URL: HTTPS is required except for loopback.\");\n\t}\n\turl.pathname = `${url.pathname.replace(/\\/+$/u, \"\")}/`;\n\treturn url.toString();\n}\n\nexport function normalizeWebDavPath(value: string | undefined) {\n\tif (value?.trim() === \".\" || value?.trim() === \"./\") return \"./\";\n\tconst normalized = trimSlashes(normalizeOptionalString(value) ?? DEFAULT_PATH);\n\tif (\n\t\t!normalized ||\n\t\tnormalized.includes(\"\\\\\") ||\n\t\thasControlCharacter(normalized) ||\n\t\tnormalized.split(\"/\").some((segment) => !segment || segment === \".\" || segment === \"..\")\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync WebDAV path.\");\n\t}\n\treturn normalized;\n}\n\nexport function validateWebDavNamespace(value: string) {\n\tif (\n\t\tvalue === \".\" ||\n\t\tvalue === \"..\" ||\n\t\tvalue.includes(\"/\") ||\n\t\tvalue.includes(\"\\\\\") ||\n\t\thasControlCharacter(value)\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync WebDAV namespace.\");\n\t}\n}\n\nexport function validateWebDavCredentials(username: string, password?: string) {\n\tif (\n\t\tusername.includes(\":\") ||\n\t\thasControlCharacter(username) ||\n\t\t(password !== undefined && hasControlCharacter(password))\n\t) {\n\t\tthrow new Error(\"Invalid pi-sync WebDAV credentials.\");\n\t}\n}\n\nfunction normalizeOptionalString(value: string | undefined) {\n\tconst normalized = value?.trim();\n\treturn normalized || undefined;\n}\n\nfunction trimSlashes(value: string) {\n\treturn value.replace(/^\\/+|\\/+$/gu, \"\");\n}\n\nfunction hasControlCharacter(value: string) {\n\treturn [...value].some((character) => {\n\t\tconst code = character.codePointAt(0) ?? 0;\n\t\treturn code < 0x20 || (code >= 0x7f && code <= 0x9f);\n\t});\n}\n", "import type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport { normalizeGitRemoteIdentity } from \"./git/git-config.js\";\nimport { normalizeWebDavIdentityUrl } from \"./webdav/webdav-config.js\";\n\nexport function backendIdentityCoordinates(config: AnySyncConfig) {\n\tswitch (config.backend.type) {\n\t\tcase \"s3\":\n\t\t\treturn JSON.stringify([\n\t\t\t\t\"s3\",\n\t\t\t\tnormalizeEndpointIdentity(config.backend.profile.endpoint),\n\t\t\t\tconfig.backend.destination.bucket,\n\t\t\t\tconfig.storagePath,\n\t\t\t]);\n\t\tcase \"git\":\n\t\t\treturn JSON.stringify([\n\t\t\t\t\"git\",\n\t\t\t\tnormalizeGitRemoteIdentity(config.backend.profile.remote),\n\t\t\t\tconfig.backend.destination.branch,\n\t\t\t\tconfig.storagePath,\n\t\t\t]);\n\t\tcase \"webdav\":\n\t\t\treturn JSON.stringify([\n\t\t\t\t\"webdav\",\n\t\t\t\tnormalizeWebDavIdentityUrl(config.backend.profile.url),\n\t\t\t\tconfig.backend.profile.username,\n\t\t\t\tconfig.storagePath,\n\t\t\t]);\n\t}\n}\n\nexport function normalizeEndpointIdentity(endpoint: string) {\n\ttry {\n\t\tconst url = new URL(endpoint.trim());\n\t\turl.hostname = url.hostname.toLowerCase();\n\t\turl.pathname = url.pathname.replace(/\\/+$/gu, \"\");\n\t\treturn url.toString().replace(/\\/$/u, \"\");\n\t} catch {\n\t\treturn endpoint.trim();\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;AAAA,IAAM,eAAe;AAEd,SAAS,2BAA2B,OAAe;AACzD,MAAI;AACH,UAAM,MAAM,IAAI,IAAI,MAAM,KAAK,CAAC;AAChC,QAAI,WAAW;AACf,QAAI,WAAW;AACf,QAAI,SAAS;AACb,QAAI,OAAO;AACX,QAAI,WAAW,GAAG,IAAI,SAAS,QAAQ,SAAS,EAAE,CAAC;AACnD,WAAO,IAAI,SAAS;AAAA,EACrB,QAAQ;AACP,WAAO,MAAM,KAAK;AAAA,EACnB;AACD;AAEO,SAAS,mBAAmB,OAA2B;AAC7D,QAAM,aAAa,wBAAwB,KAAK;AAChD,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI;AACJ,MAAI;AACH,UAAM,IAAI,IAAI,UAAU;AAAA,EACzB,QAAQ;AACP,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC9C;AACA,MAAI,IAAI,YAAY,IAAI,YAAY,IAAI,UAAU,IAAI,MAAM;AAC3D,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACA,QAAM,WACL,IAAI,aAAa,eACjB,IAAI,aAAa,eACjB,IAAI,aAAa,WACjB,IAAI,aAAa;AAClB,MAAI,IAAI,aAAa,YAAY,EAAE,IAAI,aAAa,WAAW,WAAW;AACzE,UAAM,IAAI,MAAM,oEAAoE;AAAA,EACrF;AACA,MAAI,WAAW,GAAG,IAAI,SAAS,QAAQ,SAAS,EAAE,CAAC;AACnD,SAAO,IAAI,SAAS;AACrB;AAEO,SAAS,oBAAoB,OAA2B;AAC9D,MAAI,OAAO,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,KAAM,QAAO;AAC5D,QAAM,aAAa,YAAY,wBAAwB,KAAK,KAAK,YAAY;AAC7E,MACC,CAAC,cACD,WAAW,SAAS,IAAI,KACxB,oBAAoB,UAAU,KAC9B,WAAW,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY,CAAC,WAAW,YAAY,OAAO,YAAY,IAAI,GACtF;AACD,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAC/C;AACA,SAAO;AACR;AAcO,SAAS,0BAA0B,UAAkB,UAAmB;AAC9E,MACC,SAAS,SAAS,GAAG,KACrB,oBAAoB,QAAQ,KAC3B,aAAa,UAAa,oBAAoB,QAAQ,GACtD;AACD,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACtD;AACD;AAEA,SAAS,wBAAwB,OAA2B;AAC3D,QAAM,aAAa,OAAO,KAAK;AAC/B,SAAO,cAAc;AACtB;AAEA,SAAS,YAAY,OAAe;AACnC,SAAO,MAAM,QAAQ,eAAe,EAAE;AACvC;AAEA,SAAS,oBAAoB,OAAe;AAC3C,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,cAAc;AACrC,UAAM,OAAO,UAAU,YAAY,CAAC,KAAK;AACzC,WAAO,OAAO,MAAS,QAAQ,OAAQ,QAAQ;AAAA,EAChD,CAAC;AACF;;;ACxFO,SAAS,2BAA2B,QAAuB;AACjE,UAAQ,OAAO,QAAQ,MAAM;AAAA,IAC5B,KAAK;AACJ,aAAO,KAAK,UAAU;AAAA,QACrB;AAAA,QACA,0BAA0B,OAAO,QAAQ,QAAQ,QAAQ;AAAA,QACzD,OAAO,QAAQ,YAAY;AAAA,QAC3B,OAAO;AAAA,MACR,CAAC;AAAA,IACF,KAAK;AACJ,aAAO,KAAK,UAAU;AAAA,QACrB;AAAA,QACA,2BAA2B,OAAO,QAAQ,QAAQ,MAAM;AAAA,QACxD,OAAO,QAAQ,YAAY;AAAA,QAC3B,OAAO;AAAA,MACR,CAAC;AAAA,IACF,KAAK;AACJ,aAAO,KAAK,UAAU;AAAA,QACrB;AAAA,QACA,2BAA2B,OAAO,QAAQ,QAAQ,GAAG;AAAA,QACrD,OAAO,QAAQ,QAAQ;AAAA,QACvB,OAAO;AAAA,MACR,CAAC;AAAA,EACH;AACD;AAEO,SAAS,0BAA0B,UAAkB;AAC3D,MAAI;AACH,UAAM,MAAM,IAAI,IAAI,SAAS,KAAK,CAAC;AACnC,QAAI,WAAW,IAAI,SAAS,YAAY;AACxC,QAAI,WAAW,IAAI,SAAS,QAAQ,UAAU,EAAE;AAChD,WAAO,IAAI,SAAS,EAAE,QAAQ,QAAQ,EAAE;AAAA,EACzC,QAAQ;AACP,WAAO,SAAS,KAAK;AAAA,EACtB;AACD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,134 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ safeTerminalText
5
+ } from "./chunk-LTBWGSBV.ts";
6
+ import {
7
+ syncCheckConfigFingerprint,
8
+ syncConfigReviewFingerprint
9
+ } from "./chunk-C7XY5NF5.ts";
10
+ import {
11
+ compareSyncInclude,
12
+ sameSyncInclude
13
+ } from "./chunk-JAAKLL6M.ts";
14
+
15
+ // src/ui/sync-attention.ts
16
+ import { truncateToWidth } from "@earendil-works/pi-tui";
17
+ var STATUS_KEY = "sync";
18
+ var WIDGET_KEY = "sync:attention";
19
+ function syncAttentionMatchesConfig(attention, config) {
20
+ return attention.decision.setupName === config.setupName && attention.decision.configIdentity === syncConfigReviewFingerprint(config) && sameSyncInclude(attention.decision.localInclude, config.include);
21
+ }
22
+ function createSyncAttentionController() {
23
+ let state;
24
+ let observation;
25
+ return {
26
+ observe(value) {
27
+ observation = value;
28
+ },
29
+ observation() {
30
+ return observation;
31
+ },
32
+ clearObservation() {
33
+ observation = void 0;
34
+ },
35
+ notifyObservation(ctx) {
36
+ if (observation && observationNeedsAttention(observation)) {
37
+ ctx.ui.notify(observationLines(observation).join("\n"), "warning");
38
+ }
39
+ },
40
+ set(decision, origin) {
41
+ state = { decision, origin, offered: false };
42
+ },
43
+ current() {
44
+ return state;
45
+ },
46
+ markOffered() {
47
+ if (!state || state.offered) return false;
48
+ state = { ...state, offered: true };
49
+ return true;
50
+ },
51
+ clear(ctx) {
52
+ state = void 0;
53
+ clearAttentionPresentation(ctx);
54
+ },
55
+ reset(ctx) {
56
+ state = void 0;
57
+ observation = void 0;
58
+ clearAttentionPresentation(ctx);
59
+ },
60
+ publish(ctx) {
61
+ if (!state && (!observation || !observationNeedsAttention(observation))) {
62
+ clearAttentionPresentation(ctx);
63
+ return;
64
+ }
65
+ const presentation = state ? attentionPresentation(state.decision) : {
66
+ status: "changes to review",
67
+ lines: observationLines(observation)
68
+ };
69
+ ctx.ui.setStatus(STATUS_KEY, presentation.status);
70
+ if (ctx.mode !== "tui") return;
71
+ ctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({
72
+ render(width) {
73
+ const safeWidth = Math.max(1, width);
74
+ return presentation.lines.map(
75
+ (line, index) => truncateToWidth(theme.fg(index === 0 ? "warning" : "muted", line), safeWidth, "\u2026")
76
+ );
77
+ },
78
+ invalidate() {
79
+ }
80
+ }));
81
+ }
82
+ };
83
+ }
84
+ function attentionPresentation(decision) {
85
+ const setupName = safeTerminalText(decision.setupName);
86
+ const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
87
+ const difference = comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0 ? "Only list order differs" : `Remote ${comparison.remoteOnly.length} \xB7 Device ${comparison.localOnly.length}`;
88
+ return {
89
+ status: "review needed",
90
+ lines: [`Pi Sync needs review \xB7 ${setupName}`, difference, "No changes \xB7 Run /sync to review"]
91
+ };
92
+ }
93
+ function observationMatchesConfig(observation, config) {
94
+ return observation.configIdentity === syncCheckConfigFingerprint(config);
95
+ }
96
+ function observationSummary(observation) {
97
+ const result = observation.inspection;
98
+ if (result.emptyInclude) return "No included content selected";
99
+ if (result.selectionState?.kind === "different")
100
+ return "Synced-content list differs; review needed";
101
+ if (result.firstSync)
102
+ return result.head ? "No sync baseline; review remote and local content" : "Remote empty; no sync baseline";
103
+ if (!result.head) return "Remote snapshot missing; review needed";
104
+ if (result.localChanged && result.remoteChanged)
105
+ return "Local and remote changed since last sync; review needed";
106
+ if (result.remoteChanged) return "Remote changed since last sync";
107
+ if (result.localChanged) return "Local content changed since last sync";
108
+ if (result.selectionState?.kind === "legacy")
109
+ return "Legacy remote has no authoritative content list";
110
+ return "No changes detected since last sync";
111
+ }
112
+ function observationNeedsAttention(observation) {
113
+ const result = observation.inspection;
114
+ return result.emptyInclude || result.firstSync || !result.head || result.localChanged || result.remoteChanged || result.selectionState?.kind !== "same";
115
+ }
116
+ function observationLines(observation) {
117
+ return [
118
+ `Pi Sync check \xB7 ${safeTerminalText(observation.setupName)}`,
119
+ observationSummary(observation),
120
+ "No startup transfer \xB7 Run /sync to review"
121
+ ];
122
+ }
123
+ function clearAttentionPresentation(ctx) {
124
+ ctx.ui.setStatus(STATUS_KEY, void 0);
125
+ ctx.ui.setWidget(WIDGET_KEY, void 0);
126
+ }
127
+
128
+ export {
129
+ syncAttentionMatchesConfig,
130
+ createSyncAttentionController,
131
+ observationMatchesConfig,
132
+ observationSummary
133
+ };
134
+ //# sourceMappingURL=chunk-6UHKGALE.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/sync-attention.ts"],
4
+ "sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { syncCheckConfigFingerprint, syncConfigReviewFingerprint } from \"../settings/config.js\";\nimport type { AnySyncConfig } from \"../settings/settings-types.js\";\nimport type { StartupObservation } from \"../sync/sync-inspection.js\";\nimport {\n\tcompareSyncInclude,\n\ttype RemoteSelectionDecision,\n\tsameSyncInclude,\n} from \"../sync/sync-policy.js\";\nimport { safeTerminalText } from \"./terminal-text.js\";\n\nconst STATUS_KEY = \"sync\";\nconst WIDGET_KEY = \"sync:attention\";\n\nexport type SyncAttentionOrigin = \"sync\" | \"pull\" | \"push\";\n\nexport interface SyncAttentionState {\n\tdecision: RemoteSelectionDecision;\n\torigin: SyncAttentionOrigin;\n\toffered: boolean;\n}\n\nexport function syncAttentionMatchesConfig(attention: SyncAttentionState, config: AnySyncConfig) {\n\treturn (\n\t\tattention.decision.setupName === config.setupName &&\n\t\tattention.decision.configIdentity === syncConfigReviewFingerprint(config) &&\n\t\tsameSyncInclude(attention.decision.localInclude, config.include)\n\t);\n}\n\nexport interface SyncAttentionController {\n\tobserve(observation: StartupObservation): void;\n\tobservation(): StartupObservation | undefined;\n\tclearObservation(): void;\n\tnotifyObservation(ctx: ExtensionContext): void;\n\tset(decision: RemoteSelectionDecision, origin: SyncAttentionOrigin): void;\n\tcurrent(): SyncAttentionState | undefined;\n\tmarkOffered(): boolean;\n\tclear(ctx: ExtensionContext): void;\n\treset(ctx: ExtensionContext): void;\n\tpublish(ctx: ExtensionContext): void;\n}\n\nexport function createSyncAttentionController(): SyncAttentionController {\n\tlet state: SyncAttentionState | undefined;\n\tlet observation: StartupObservation | undefined;\n\n\treturn {\n\t\tobserve(value) {\n\t\t\tobservation = value;\n\t\t},\n\t\tobservation() {\n\t\t\treturn observation;\n\t\t},\n\t\tclearObservation() {\n\t\t\tobservation = undefined;\n\t\t},\n\t\tnotifyObservation(ctx) {\n\t\t\tif (observation && observationNeedsAttention(observation)) {\n\t\t\t\tctx.ui.notify(observationLines(observation).join(\"\\n\"), \"warning\");\n\t\t\t}\n\t\t},\n\t\tset(decision, origin) {\n\t\t\tstate = { decision, origin, offered: false };\n\t\t},\n\t\tcurrent() {\n\t\t\treturn state;\n\t\t},\n\t\tmarkOffered() {\n\t\t\tif (!state || state.offered) return false;\n\t\t\tstate = { ...state, offered: true };\n\t\t\treturn true;\n\t\t},\n\t\tclear(ctx) {\n\t\t\tstate = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\treset(ctx) {\n\t\t\tstate = undefined;\n\t\t\tobservation = undefined;\n\t\t\tclearAttentionPresentation(ctx);\n\t\t},\n\t\tpublish(ctx) {\n\t\t\tif (!state && (!observation || !observationNeedsAttention(observation))) {\n\t\t\t\tclearAttentionPresentation(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst presentation = state\n\t\t\t\t? attentionPresentation(state.decision)\n\t\t\t\t: {\n\t\t\t\t\t\tstatus: \"changes to review\",\n\t\t\t\t\t\tlines: observationLines(observation as StartupObservation),\n\t\t\t\t\t};\n\t\t\tctx.ui.setStatus(STATUS_KEY, presentation.status);\n\t\t\tif (ctx.mode !== \"tui\") return;\n\t\t\tctx.ui.setWidget(WIDGET_KEY, (_tui, theme) => ({\n\t\t\t\trender(width: number) {\n\t\t\t\t\tconst safeWidth = Math.max(1, width);\n\t\t\t\t\treturn presentation.lines.map((line, index) =>\n\t\t\t\t\t\ttruncateToWidth(theme.fg(index === 0 ? \"warning\" : \"muted\", line), safeWidth, \"\u2026\"),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tinvalidate() {},\n\t\t\t}));\n\t\t},\n\t};\n}\n\nfunction attentionPresentation(decision: RemoteSelectionDecision) {\n\tconst setupName = safeTerminalText(decision.setupName);\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\tconst difference =\n\t\tcomparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? \"Only list order differs\"\n\t\t\t: `Remote ${comparison.remoteOnly.length} \u00B7 Device ${comparison.localOnly.length}`;\n\treturn {\n\t\tstatus: \"review needed\",\n\t\tlines: [`Pi Sync needs review \u00B7 ${setupName}`, difference, \"No changes \u00B7 Run /sync to review\"],\n\t};\n}\n\nexport function observationMatchesConfig(observation: StartupObservation, config: AnySyncConfig) {\n\treturn observation.configIdentity === syncCheckConfigFingerprint(config);\n}\n\nexport function observationSummary(observation: StartupObservation) {\n\tconst result = observation.inspection;\n\tif (result.emptyInclude) return \"No included content selected\";\n\tif (result.selectionState?.kind === \"different\")\n\t\treturn \"Synced-content list differs; review needed\";\n\tif (result.firstSync)\n\t\treturn result.head\n\t\t\t? \"No sync baseline; review remote and local content\"\n\t\t\t: \"Remote empty; no sync baseline\";\n\tif (!result.head) return \"Remote snapshot missing; review needed\";\n\tif (result.localChanged && result.remoteChanged)\n\t\treturn \"Local and remote changed since last sync; review needed\";\n\tif (result.remoteChanged) return \"Remote changed since last sync\";\n\tif (result.localChanged) return \"Local content changed since last sync\";\n\tif (result.selectionState?.kind === \"legacy\")\n\t\treturn \"Legacy remote has no authoritative content list\";\n\treturn \"No changes detected since last sync\";\n}\n\nexport function observationNeedsAttention(observation: StartupObservation) {\n\tconst result = observation.inspection;\n\treturn (\n\t\tresult.emptyInclude ||\n\t\tresult.firstSync ||\n\t\t!result.head ||\n\t\tresult.localChanged ||\n\t\tresult.remoteChanged ||\n\t\tresult.selectionState?.kind !== \"same\"\n\t);\n}\n\nfunction observationLines(observation: StartupObservation) {\n\treturn [\n\t\t`Pi Sync check \u00B7 ${safeTerminalText(observation.setupName)}`,\n\t\tobservationSummary(observation),\n\t\t\"No startup transfer \u00B7 Run /sync to review\",\n\t];\n}\n\nfunction clearAttentionPresentation(ctx: ExtensionContext) {\n\tctx.ui.setStatus(STATUS_KEY, undefined);\n\tctx.ui.setWidget(WIDGET_KEY, undefined);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  filterSnapshotForConfigPolicy,
5
5
  isSessionPath
6
- } from "./chunk-HVUSHMVL.ts";
6
+ } from "./chunk-OEX4ABY6.ts";
7
7
  import {
8
8
  canonicalSnapshotPathForConfig,
9
9
  customIncludePathsByLower,
@@ -11,9 +11,9 @@ import {
11
11
  isConfiguredSnapshotPath,
12
12
  normalizeSyncInclude,
13
13
  toPosix
14
- } from "./chunk-YQ6UW7IF.ts";
14
+ } from "./chunk-JAAKLL6M.ts";
15
15
 
16
- // src/sync-state.ts
16
+ // src/sync/sync-state.ts
17
17
  function hasLocalChanges(local, state, config) {
18
18
  return !sameHashes(fileHashMap(local), stateHashMapForConfig(state, config));
19
19
  }
@@ -125,4 +125,4 @@ export {
125
125
  canPullRemoteSettingsOnFirstSync,
126
126
  canPullRemoteSessionsOnFirstSync
127
127
  };
128
- //# sourceMappingURL=chunk-JF6MZCT4.ts.map
128
+ //# sourceMappingURL=chunk-AUSDLD6F.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync/sync-state.ts"],
4
+ "sourcesContent": ["import type { RemoteHead } from \"../backends/sync-backend.js\";\nimport { toPosix } from \"../paths.js\";\nimport {\n\tcanonicalSnapshotPathForConfig,\n\tfilterSnapshotForConfigPolicy,\n\tisConfiguredSnapshotPath,\n\tisSessionPath,\n} from \"../snapshot/snapshot.js\";\nimport type { Snapshot } from \"../snapshot/snapshot-types.js\";\nimport type { SyncState } from \"../state/state-types.js\";\nimport {\n\tcustomIncludePathsByLower,\n\tincludeFromSelectionConfig,\n\tnormalizeSyncInclude,\n\ttype SyncSelectionConfig,\n} from \"./sync-policy.js\";\n\ntype SyncPolicyConfig = SyncSelectionConfig;\n\nexport function hasLocalChanges(local: Snapshot, state: SyncState, config: SyncPolicyConfig) {\n\treturn !sameHashes(fileHashMap(local), stateHashMapForConfig(state, config));\n}\n\nexport function remoteChangedSinceState(\n\thead: RemoteHead | undefined,\n\tstate: SyncState,\n\tconfig: SyncPolicyConfig,\n\tsameRevision: (left: string, right: string) => boolean,\n) {\n\tif (!head) return Boolean(state.lastAppliedSnapshot);\n\tif (head.snapshotId !== state.lastAppliedSnapshot) return true;\n\tif (state.lastRemoteRevision && !sameRevision(head.revision, state.lastRemoteRevision))\n\t\treturn true;\n\tif (syncIncludeChanged(state, config)) return true;\n\treturn (\n\t\tincludeFromSelectionConfig(config).includes(\"sessions\") &&\n\t\t!state.include?.includes(\"sessions\") &&\n\t\thead.syncSessions\n\t);\n}\n\nexport function hasRemoteChanges(\n\tremote: Snapshot,\n\tstate: SyncState,\n\tconfig: SyncPolicyConfig,\n\tignoredPaths = new Set<string>(),\n) {\n\tif (remote.id === state.lastAppliedSnapshot && !syncPolicyChanged(state, config)) return false;\n\treturn !snapshotHashesMatchState(\n\t\tfilterSnapshotForConfigPolicy(remote, config),\n\t\tstate,\n\t\tconfig,\n\t\tignoredPaths,\n\t);\n}\n\nexport function sameHashes(left: Record<string, string>, right: Record<string, string>) {\n\tconst keys = new Set([...Object.keys(left), ...Object.keys(right)]);\n\tfor (const key of keys) if (left[key] !== right[key]) return false;\n\treturn true;\n}\n\nexport function fileHashMap(snapshot: Snapshot) {\n\treturn Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));\n}\n\nfunction stateHashMapForConfig(state: SyncState, config: SyncPolicyConfig) {\n\tconst includePaths = customIncludePathsByLower(includeFromSelectionConfig(config));\n\treturn Object.fromEntries(\n\t\tObject.entries(state.lastFileHashes)\n\t\t\t.filter(([filePath]) => isConfiguredSnapshotPath(filePath, config))\n\t\t\t.map(([filePath, hash]) => [canonicalSnapshotPathForConfig(filePath, includePaths), hash]),\n\t);\n}\n\nexport function snapshotHashesMatchState(\n\tsnapshot: Snapshot,\n\tstate: SyncState,\n\tconfig: SyncPolicyConfig,\n\tignoredPaths = new Set<string>(),\n) {\n\treturn sameHashes(\n\t\twithoutHashPaths(fileHashMap(snapshot), ignoredPaths),\n\t\twithoutHashPaths(stateHashMapForConfig(state, config), ignoredPaths),\n\t);\n}\n\nexport function snapshotsMatch(left: Snapshot, right: Snapshot) {\n\treturn (\n\t\tleft.syncSessions === right.syncSessions && sameHashes(fileHashMap(left), fileHashMap(right))\n\t);\n}\n\nfunction withoutHashPaths(hashes: Record<string, string>, ignoredPaths: Set<string>) {\n\tif (ignoredPaths.size === 0) return hashes;\n\treturn Object.fromEntries(\n\t\tObject.entries(hashes).filter(([filePath]) => !ignoredPaths.has(toPosix(filePath))),\n\t);\n}\n\nexport function syncPolicyChanged(state: SyncState, config: SyncPolicyConfig) {\n\treturn syncIncludeChanged(state, config);\n}\n\nexport function shouldRefreshSyncedState(\n\tremote: Snapshot,\n\thead: RemoteHead | undefined,\n\tstate: SyncState,\n\tconfig: SyncPolicyConfig,\n\tsameRevision: (left: string, right: string) => boolean,\n) {\n\treturn (\n\t\tremote.id !== state.lastAppliedSnapshot ||\n\t\tBoolean(\n\t\t\thead && (!state.lastRemoteRevision || !sameRevision(head.revision, state.lastRemoteRevision)),\n\t\t) ||\n\t\tsyncPolicyChanged(state, config)\n\t);\n}\n\nfunction syncIncludeChanged(state: SyncState, config: SyncPolicyConfig) {\n\tconst stored = state.include\n\t\t? normalizeSyncInclude(state.include)\n\t\t: includeFromSelectionConfig(state);\n\tconst current = includeFromSelectionConfig(config);\n\treturn stored.length !== current.length || stored.some((item, index) => item !== current[index]);\n}\n\nexport function settingsHashMap(snapshot: Snapshot) {\n\treturn Object.fromEntries(\n\t\tsnapshot.files\n\t\t\t.filter((file) => !isSessionPath(file.path))\n\t\t\t.map((file) => [file.path, file.sha256]),\n\t);\n}\n\nexport function sessionHashMap(snapshot: Snapshot) {\n\treturn Object.fromEntries(\n\t\tsnapshot.files\n\t\t\t.filter((file) => isSessionPath(file.path))\n\t\t\t.map((file) => [file.path, file.sha256]),\n\t);\n}\n\nexport function settingsHashMapFromState(state: SyncState) {\n\treturn Object.fromEntries(\n\t\tObject.entries(state.lastFileHashes).filter(([filePath]) => !isSessionPath(filePath)),\n\t);\n}\n\nexport function settingsHashesMatchState(remote: Snapshot, state: SyncState) {\n\treturn sameHashes(settingsHashMap(remote), settingsHashMapFromState(state));\n}\n\nexport function canPullRemoteSettingsOnFirstSync(local: Snapshot, remote: Snapshot) {\n\tconst remoteSettings = settingsHashMap(remote);\n\treturn Object.entries(settingsHashMap(local)).every(\n\t\t([filePath, hash]) => remoteSettings[filePath] === hash,\n\t);\n}\n\nexport function canPullRemoteSessionsOnFirstSync(local: Snapshot, remote: Snapshot) {\n\tconst localSessions = sessionHashMap(local);\n\tconst remoteSessions = sessionHashMap(remote);\n\treturn Object.entries(localSessions).every(\n\t\t([filePath, hash]) => remoteSessions[filePath] === hash,\n\t);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -2,22 +2,22 @@
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
4
  withLock
5
- } from "./chunk-WSOBYHIZ.ts";
5
+ } from "./chunk-GWMHAQG4.ts";
6
+ import {
7
+ stateDir
8
+ } from "./chunk-ZI5MXMEX.ts";
6
9
  import {
7
10
  sessionStorageRoot
8
- } from "./chunk-HVUSHMVL.ts";
11
+ } from "./chunk-THMF4BVL.ts";
9
12
  import {
10
13
  agentDir
11
- } from "./chunk-5T442OP3.ts";
12
- import {
13
- stateDir
14
- } from "./chunk-F5QL2GPY.ts";
14
+ } from "./chunk-T3HH3UEJ.ts";
15
15
  import {
16
16
  assertWithinRoot,
17
17
  isPathInside
18
- } from "./chunk-YQ6UW7IF.ts";
18
+ } from "./chunk-JAAKLL6M.ts";
19
19
 
20
- // src/snapshot-transaction.ts
20
+ // src/snapshot/snapshot-transaction.ts
21
21
  import { randomUUID } from "node:crypto";
22
22
  import fs from "node:fs/promises";
23
23
  import path from "node:path";
@@ -200,4 +200,4 @@ export {
200
200
  recoverSnapshotTransactionsOnStartup,
201
201
  recoverPendingSnapshotTransactions
202
202
  };
203
- //# sourceMappingURL=chunk-X7SQ2EQM.ts.map
203
+ //# sourceMappingURL=chunk-AXXS2BPA.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/snapshot/snapshot-transaction.ts"],
4
+ "sourcesContent": ["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { assertWithinRoot, isPathInside } from \"../paths.js\";\nimport { withLock } from \"../state/lock.js\";\nimport { stateDir } from \"../state/state-directory.js\";\nimport { agentDir } from \"./session-paths.js\";\nimport { sessionStorageRoot } from \"./snapshot-paths.js\";\nimport type { SnapshotApplyPlan } from \"./snapshot-types.js\";\n\nconst JOURNAL_VERSION = 1;\n\ninterface TransactionEntry {\n\ttarget: string;\n\tbackupName: string;\n\tkind: \"missing\" | \"file\" | \"directory\" | \"symlink\";\n\tlinkTarget?: string;\n}\n\ninterface TransactionJournal {\n\tversion: number;\n\troot: string;\n\tsessionRoot?: string;\n\tentries: TransactionEntry[];\n}\n\nexport async function applySnapshotTransaction(\n\tplan: SnapshotApplyPlan,\n\toptions: { sessionDir?: string } = {},\n) {\n\tawait recoverPendingSnapshotTransactions();\n\tconst transaction = await prepareTransaction(plan, options.sessionDir);\n\ttry {\n\t\tfor (const target of plan.deletes) {\n\t\t\tawait fs.rm(target, { force: true, recursive: true });\n\t\t}\n\t\tfor (const item of plan.writes) {\n\t\t\tawait fs.mkdir(path.dirname(item.target), { recursive: true });\n\t\t\tawait fs.writeFile(item.target, item.content);\n\t\t}\n\t\tawait fs.rm(transaction.directory, { recursive: true, force: true });\n\t} catch (error) {\n\t\ttry {\n\t\t\tawait restoreTransaction(transaction.directory, transaction.journal);\n\t\t} catch (recoveryError) {\n\t\t\tthrow new AggregateError(\n\t\t\t\t[error, recoveryError],\n\t\t\t\t`Snapshot apply failed and automatic recovery also failed. Transaction retained at ${transaction.directory}.`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nexport async function recoverSnapshotTransactionsOnStartup() {\n\tif (!(await pendingTransactionEntries()).some((entry) => entry.isDirectory())) return;\n\tawait withLock(\"recovery\", recoverPendingSnapshotTransactions, { reclaimStale: true });\n}\n\nexport async function recoverPendingSnapshotTransactions() {\n\tconst directory = transactionRoot();\n\tconst entries = await pendingTransactionEntries();\n\tfor (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst transactionDirectory = path.join(directory, entry.name);\n\t\tconst journalPath = path.join(transactionDirectory, \"journal.json\");\n\t\tlet journal: TransactionJournal;\n\t\ttry {\n\t\t\tjournal = JSON.parse(await fs.readFile(journalPath, \"utf8\")) as TransactionJournal;\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\tawait fs.rm(transactionDirectory, { recursive: true, force: true });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthrow new Error(`Cannot recover malformed pi-sync transaction: ${journalPath}`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t\tawait restoreTransaction(transactionDirectory, journal);\n\t}\n}\n\nasync function pendingTransactionEntries() {\n\ttry {\n\t\treturn await fs.readdir(transactionRoot(), { withFileTypes: true });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n}\n\nasync function prepareTransaction(plan: SnapshotApplyPlan, sessionDir?: string) {\n\tconst root = path.resolve(agentDir());\n\tconst sessionRoot = sessionDir ? path.resolve(sessionStorageRoot(root, sessionDir)) : undefined;\n\tconst directory = path.join(transactionRoot(), randomUUID());\n\tconst backupDirectory = path.join(directory, \"before\");\n\tawait fs.mkdir(backupDirectory, { recursive: true, mode: 0o700 });\n\tconst targets = [...new Set([...plan.deletes, ...plan.writes.map((item) => item.target)])].sort();\n\tconst entries: TransactionEntry[] = [];\n\tfor (let index = 0; index < targets.length; index += 1) {\n\t\tconst target = targets[index];\n\t\tif (!target) continue;\n\t\tassertAllowedTarget(root, sessionRoot, target);\n\t\tconst backupName = `${index}`;\n\t\tconst backupPath = path.join(backupDirectory, backupName);\n\t\ttry {\n\t\t\tconst stat = await fs.lstat(target);\n\t\t\tif (stat.isSymbolicLink()) {\n\t\t\t\tentries.push({\n\t\t\t\t\ttarget,\n\t\t\t\t\tbackupName,\n\t\t\t\t\tkind: \"symlink\",\n\t\t\t\t\tlinkTarget: await fs.readlink(target),\n\t\t\t\t});\n\t\t\t} else if (stat.isDirectory()) {\n\t\t\t\tawait fs.cp(target, backupPath, {\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tdereference: false,\n\t\t\t\t\tpreserveTimestamps: true,\n\t\t\t\t});\n\t\t\t\tentries.push({ target, backupName, kind: \"directory\" });\n\t\t\t} else if (stat.isFile()) {\n\t\t\t\tawait fs.copyFile(target, backupPath);\n\t\t\t\tawait fs.chmod(backupPath, stat.mode);\n\t\t\t\tentries.push({ target, backupName, kind: \"file\" });\n\t\t\t} else {\n\t\t\t\tthrow new Error(`Unsupported existing snapshot target: ${target}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code !== \"ENOENT\" && code !== \"ENOTDIR\") throw error;\n\t\t\tentries.push({ target, backupName, kind: \"missing\" });\n\t\t}\n\t}\n\tconst journal: TransactionJournal = {\n\t\tversion: JOURNAL_VERSION,\n\t\troot,\n\t\tsessionRoot,\n\t\tentries,\n\t};\n\tawait fs.writeFile(\n\t\tpath.join(directory, \"journal.json\"),\n\t\t`${JSON.stringify(journal, null, \"\\t\")}\\n`,\n\t\t{\n\t\t\tmode: 0o600,\n\t\t},\n\t);\n\treturn { directory, journal };\n}\n\nasync function restoreTransaction(directory: string, journal: TransactionJournal) {\n\tvalidateJournal(directory, journal);\n\tfor (const entry of [...journal.entries].sort(\n\t\t(left, right) => right.target.length - left.target.length,\n\t)) {\n\t\tawait fs.rm(entry.target, { recursive: true, force: true });\n\t}\n\tfor (const entry of journal.entries) {\n\t\tif (entry.kind === \"missing\") continue;\n\t\tawait fs.mkdir(path.dirname(entry.target), { recursive: true });\n\t\tconst backupPath = path.join(directory, \"before\", entry.backupName);\n\t\tassertWithinRoot(directory, backupPath);\n\t\tif (entry.kind === \"file\") await fs.copyFile(backupPath, entry.target);\n\t\telse if (entry.kind === \"directory\") {\n\t\t\tawait fs.cp(backupPath, entry.target, {\n\t\t\t\trecursive: true,\n\t\t\t\tdereference: false,\n\t\t\t\tpreserveTimestamps: true,\n\t\t\t});\n\t\t} else if (entry.kind === \"symlink\" && entry.linkTarget !== undefined) {\n\t\t\tawait fs.symlink(entry.linkTarget, entry.target);\n\t\t}\n\t}\n\tawait fs.rm(directory, { recursive: true, force: true });\n}\n\nfunction validateJournal(directory: string, journal: TransactionJournal) {\n\tif (journal.version !== JOURNAL_VERSION || !Array.isArray(journal.entries)) {\n\t\tthrow new Error(`Unsupported pi-sync transaction journal: ${directory}`);\n\t}\n\tconst expectedRoot = path.resolve(agentDir());\n\tif (path.resolve(journal.root) !== expectedRoot) {\n\t\tthrow new Error(`Transaction root no longer matches the Pi agent directory: ${directory}`);\n\t}\n\tfor (const entry of journal.entries) {\n\t\tif (\n\t\t\t!entry ||\n\t\t\ttypeof entry.target !== \"string\" ||\n\t\t\ttypeof entry.backupName !== \"string\" ||\n\t\t\t!/^\\d+$/u.test(entry.backupName)\n\t\t) {\n\t\t\tthrow new Error(`Invalid pi-sync transaction entry: ${directory}`);\n\t\t}\n\t\tassertAllowedTarget(journal.root, journal.sessionRoot, entry.target);\n\t}\n}\n\nfunction assertAllowedTarget(root: string, sessionRoot: string | undefined, target: string) {\n\tconst resolved = path.resolve(target);\n\tif (isPathInside(root, resolved)) {\n\t\tassertWithinRoot(root, resolved);\n\t\treturn;\n\t}\n\tif (sessionRoot && isPathInside(sessionRoot, resolved)) {\n\t\tassertWithinRoot(sessionRoot, resolved);\n\t\treturn;\n\t}\n\tthrow new Error(`Transaction target is outside configured roots: ${target}`);\n}\n\nfunction transactionRoot() {\n\treturn path.join(stateDir(), \"transactions\");\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }