@narumitw/pi-sync 0.49.15 → 0.50.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 (213) hide show
  1. package/README.md +49 -16
  2. package/dist/chunks/attention-widget-Z7G3SYYJ.ts +9 -0
  3. package/dist/chunks/attention-widget-Z7G3SYYJ.ts.map +7 -0
  4. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  5. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  6. package/dist/chunks/{chunk-ARN2672F.ts → chunk-4CHTZMEO.ts} +23 -55
  7. package/dist/chunks/chunk-4CHTZMEO.ts.map +7 -0
  8. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  9. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  10. package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  11. package/dist/chunks/chunk-AUSDLD6F.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-CCSN3F47.ts +51 -0
  15. package/dist/chunks/chunk-CCSN3F47.ts.map +7 -0
  16. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  17. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  18. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  19. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  20. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  21. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  22. package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-ETTRJDNP.ts} +4 -4
  23. package/dist/chunks/chunk-ETTRJDNP.ts.map +7 -0
  24. package/dist/chunks/{chunk-44LPGQTI.ts → chunk-G4RNQQZS.ts} +9 -7
  25. package/dist/chunks/chunk-G4RNQQZS.ts.map +7 -0
  26. package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
  27. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  28. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-IJ6FJFA5.ts} +9 -9
  29. package/dist/chunks/chunk-IJ6FJFA5.ts.map +7 -0
  30. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  31. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  32. package/dist/chunks/chunk-KW7YQEZN.ts +1255 -0
  33. package/dist/chunks/chunk-KW7YQEZN.ts.map +7 -0
  34. package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
  35. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  36. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  37. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  38. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  39. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  40. package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
  41. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  42. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  43. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  44. package/dist/chunks/chunk-S7YBZR42.ts +146 -0
  45. package/dist/chunks/chunk-S7YBZR42.ts.map +7 -0
  46. package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
  47. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  48. package/dist/chunks/chunk-SZWBGPID.ts +57 -0
  49. package/dist/chunks/chunk-SZWBGPID.ts.map +7 -0
  50. package/dist/chunks/chunk-T2B5IU74.ts +25 -0
  51. package/dist/chunks/chunk-T2B5IU74.ts.map +7 -0
  52. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  53. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  54. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  55. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  56. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  57. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  58. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  59. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  60. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  61. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  62. package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
  63. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  64. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-GMVS4PD4.ts} +39 -14
  65. package/dist/chunks/git-backend-GMVS4PD4.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-FKIBM3GO.ts +22 -0
  67. package/dist/chunks/manager-ui-R3XPUFFS.ts +1680 -0
  68. package/dist/chunks/manager-ui-R3XPUFFS.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-D7RBRJNL.ts +21 -0
  70. package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-66O4FGBH.ts} +94 -16
  71. package/dist/chunks/s3-backend-66O4FGBH.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  73. package/dist/chunks/setup-wizard-IUB6ESEU.ts +89 -0
  74. package/dist/chunks/setup-wizard-IUB6ESEU.ts.map +7 -0
  75. package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  76. package/dist/chunks/sync-inspection-CST2KL65.ts +20 -0
  77. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-LBX3GKCM.ts} +369 -289
  78. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +7 -0
  79. package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
  80. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  81. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  82. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  83. package/dist/index.ts +476 -341
  84. package/dist/index.ts.map +4 -4
  85. package/docs/settings.md +30 -6
  86. package/package.json +1 -1
  87. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  88. package/src/backends/backend-identity.ts +40 -0
  89. package/src/backends/backend-types.ts +81 -0
  90. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  91. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  92. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  93. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  94. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  95. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  96. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  97. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  98. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  99. package/src/commands/command-execution.ts +330 -0
  100. package/src/commands/command-handler.ts +201 -0
  101. package/src/commands/command-types.ts +18 -0
  102. package/src/{command.ts → commands/command.ts} +2 -1
  103. package/src/commands/setup-completions.ts +6 -0
  104. package/src/settings/config.ts +238 -0
  105. package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
  106. package/src/settings/settings-store.ts +109 -0
  107. package/src/settings/settings-types.ts +128 -0
  108. package/src/settings/settings-validation.ts +443 -0
  109. package/src/snapshot/session-paths.ts +84 -0
  110. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  111. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  112. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  113. package/src/snapshot/snapshot-types.ts +38 -0
  114. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  115. package/src/state/json-file.ts +20 -0
  116. package/src/{lock.ts → state/lock.ts} +3 -2
  117. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  118. package/src/state/state-types.ts +20 -0
  119. package/src/state/sync-state-store.ts +62 -0
  120. package/src/sync/automatic-sync.ts +103 -0
  121. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  122. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  123. package/src/sync/signals.ts +10 -0
  124. package/src/sync/startup-check.ts +128 -0
  125. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  126. package/src/sync/sync-error-guidance.ts +53 -0
  127. package/src/sync/sync-inspection.ts +51 -0
  128. package/src/sync/sync-loaders.ts +54 -0
  129. package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
  130. package/src/sync/sync-operations.ts +12 -0
  131. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  132. package/src/sync/sync-queries.ts +235 -0
  133. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  134. package/src/sync-extension.ts +64 -708
  135. package/src/sync.ts +8 -8
  136. package/src/types.ts +47 -286
  137. package/src/ui/attention-widget.ts +1 -0
  138. package/src/ui/automatic-sync-summary.ts +6 -0
  139. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  140. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  141. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  142. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  143. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  144. package/src/ui/manager-ui.ts +386 -0
  145. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  146. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  147. package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
  148. package/src/ui/setup/git-ui.ts +293 -0
  149. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  150. package/src/ui/setup/s3-ui.ts +217 -0
  151. package/src/ui/setup/setup-actions.ts +127 -0
  152. package/src/ui/setup/setup-location-ui.ts +167 -0
  153. package/src/ui/setup/setup-name-ui.ts +34 -0
  154. package/src/ui/setup/setup-prompts.ts +92 -0
  155. package/src/ui/setup/setup-review.ts +70 -0
  156. package/src/ui/setup/setup-switcher.ts +181 -0
  157. package/src/ui/setup/setup-wizard.ts +39 -0
  158. package/src/ui/setup/text-input.ts +78 -0
  159. package/src/ui/setup/webdav-ui.ts +330 -0
  160. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  161. package/src/ui/sync-attention.ts +187 -0
  162. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  163. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  164. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  165. package/src/ui/terminal-text.ts +4 -0
  166. package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
  167. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  168. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  169. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  170. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  171. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  172. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  173. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  174. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  175. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  176. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  177. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  178. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  179. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  180. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  181. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  182. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  183. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  184. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  185. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  186. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  187. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  188. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  189. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  190. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  191. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  192. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  193. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  194. package/src/config.ts +0 -923
  195. package/src/git-ui.ts +0 -307
  196. package/src/manager-helpers.ts +0 -64
  197. package/src/manager-ui.ts +0 -997
  198. package/src/sync-attention.ts +0 -100
  199. package/src/webdav-ui.ts +0 -421
  200. /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-FKIBM3GO.ts.map} +0 -0
  201. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-D7RBRJNL.ts.map} +0 -0
  202. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  203. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  204. /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-CST2KL65.ts.map} +0 -0
  205. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  206. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  207. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  208. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  209. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  210. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  211. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  212. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  213. /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,9 @@
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/ui/attention-widget.ts
5
+ import { EditorStatusWidget } from "@narumitw/pi-tui-kit";
6
+ export {
7
+ EditorStatusWidget
8
+ };
9
+ //# sourceMappingURL=attention-widget-Z7G3SYYJ.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/attention-widget.ts"],
4
+ "sourcesContent": ["export { EditorStatusWidget } from \"@narumitw/pi-tui-kit\";\n"],
5
+ "mappings": ";;;;AAAA,SAAS,0BAA0B;",
6
+ "names": []
7
+ }
@@ -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
+ }
@@ -1,72 +1,41 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
- SyncSetupReviewChangedError,
5
- updateSyncSetup
6
- } from "./chunk-DFXBKHGN.ts";
7
- import {
8
- createSyncBackend,
9
4
  readSnapshotForHead
10
- } from "./chunk-5LZALPCD.ts";
5
+ } from "./chunk-HUDQATCL.ts";
6
+ import {
7
+ createSyncBackend
8
+ } from "./chunk-T2B5IU74.ts";
11
9
  import {
12
- errorMessage,
13
10
  safeTerminalText
14
- } from "./chunk-C7LXZ3UH.ts";
11
+ } from "./chunk-LTBWGSBV.ts";
12
+ import {
13
+ SyncSetupReviewChangedError,
14
+ updateSyncSetup
15
+ } from "./chunk-LCRI3ABW.ts";
15
16
  import {
16
17
  loadConfig,
17
18
  loadPartialConfig,
18
19
  syncConfigReviewFingerprint
19
- } from "./chunk-5T442OP3.ts";
20
+ } from "./chunk-C7XY5NF5.ts";
21
+ import {
22
+ errorMessage
23
+ } from "./chunk-2B2ELUF5.ts";
20
24
  import {
21
25
  compareSyncInclude,
22
26
  inspectRemoteSelection,
23
27
  sameSyncInclude
24
- } from "./chunk-YQ6UW7IF.ts";
28
+ } from "./chunk-JAAKLL6M.ts";
25
29
 
26
- // src/remote-selection-ui.ts
30
+ // src/ui/remote-selection-ui.ts
27
31
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
28
32
 
29
- // src/cancellable-operation.ts
33
+ // src/ui/cancellable-operation.ts
30
34
  import {
31
35
  BorderedLoader
32
36
  } from "@earendil-works/pi-coding-agent";
33
37
  import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
34
38
  import { runCustomInteraction } from "@narumitw/pi-tui-kit";
35
-
36
- // src/manager-helpers.ts
37
- async function requiredExistingBucket(ctx, example, signal) {
38
- const value = await ctx.ui.input("Existing bucket", `Example: ${example}`, { signal });
39
- if (signal?.aborted) {
40
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
41
- }
42
- if (value === void 0) return void 0;
43
- const normalized = value.trim();
44
- if (!normalized) {
45
- ctx.ui.notify("Enter the name of an existing R2/S3 bucket, or cancel setup.", "warning");
46
- return void 0;
47
- }
48
- return normalized;
49
- }
50
- async function requiredInput(ctx, title, placeholder, signal) {
51
- const value = await ctx.ui.input(title, placeholder, { signal });
52
- if (signal?.aborted) {
53
- throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
54
- }
55
- if (value === void 0) return void 0;
56
- const normalized = value.trim() || placeholder;
57
- return normalized.includes("<") || normalized.includes(">") ? void 0 : normalized;
58
- }
59
- function ownRecord(value) {
60
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
61
- }
62
- function safeTerminalText2(value) {
63
- return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
64
- }
65
- function errorMessage2(error) {
66
- return error instanceof Error ? error.message : String(error);
67
- }
68
-
69
- // src/cancellable-operation.ts
70
39
  async function runCancellableOperation(ctx, message, route, runRoute, options = {}) {
71
40
  const {
72
41
  commitAware = false,
@@ -140,12 +109,12 @@ function keybindingText(keybindings, binding, fallback) {
140
109
  const keys = [.../* @__PURE__ */ new Set([...keybindings.getKeys(binding), "ctrl+c"])].map(String).map((key) => {
141
110
  if (key === "return") return "enter";
142
111
  if (key === "escape") return "esc";
143
- return safeTerminalText2(key);
112
+ return safeTerminalText(key);
144
113
  }).filter(Boolean);
145
114
  return keys.join("/") || fallback;
146
115
  }
147
116
 
148
- // src/remote-selection-ui.ts
117
+ // src/ui/remote-selection-ui.ts
149
118
  var STATUS_KEY = "sync";
150
119
  async function showRemoteSelectionReview(ctx, setupName, signal, factory = createSyncBackend, options = {}) {
151
120
  try {
@@ -153,6 +122,9 @@ async function showRemoteSelectionReview(ctx, setupName, signal, factory = creat
153
122
  if (!decision) {
154
123
  const inspected = await inspectConfiguredRemoteSelection(ctx, setupName, signal, factory);
155
124
  if (!inspected || signal?.aborted) return { kind: "stale" };
125
+ if (inspected.kind === "empty" || inspected.state.kind !== "different") {
126
+ options.onSelectionResolved?.();
127
+ }
156
128
  if (inspected.kind === "empty") {
157
129
  ctx.ui.notify("Remote storage has no snapshot or synced-content list yet.", "info");
158
130
  return { kind: "back" };
@@ -194,6 +166,7 @@ async function showRemoteSelectionReview(ctx, setupName, signal, factory = creat
194
166
  );
195
167
  if (!refreshed || signal?.aborted) return { kind: "stale" };
196
168
  if (refreshed.kind === "empty") {
169
+ options.onSelectionResolved?.();
197
170
  ctx.ui.notify("Remote storage no longer has a snapshot or synced-content list.", "warning");
198
171
  return { kind: "back" };
199
172
  }
@@ -644,12 +617,7 @@ function throwIfAborted(signal) {
644
617
  }
645
618
 
646
619
  export {
647
- requiredExistingBucket,
648
- requiredInput,
649
- ownRecord,
650
- safeTerminalText2 as safeTerminalText,
651
- errorMessage2 as errorMessage,
652
620
  runCancellableOperation,
653
621
  showRemoteSelectionReview
654
622
  };
655
- //# sourceMappingURL=chunk-ARN2672F.ts.map
623
+ //# sourceMappingURL=chunk-4CHTZMEO.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/remote-selection-ui.ts", "../../src/ui/cancellable-operation.ts"],
4
+ "sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { defineMenu, runMenu } from \"@narumitw/pi-tui-kit\";\nimport { createSyncBackend, type SyncBackendFactory } from \"../backends/backend-factory.js\";\nimport type { RemoteHead, SyncBackend } from \"../backends/sync-backend.js\";\nimport {\n\tloadConfig,\n\tloadPartialConfig,\n\ttype SyncSetupStorageReview,\n\tsyncConfigReviewFingerprint,\n} from \"../settings/config.js\";\nimport { SyncSetupReviewChangedError, updateSyncSetup } from \"../settings/settings-management.js\";\nimport type { AnySyncConfig, PartialConfig } from \"../settings/settings-types.js\";\nimport { readSnapshotForHead } from \"../sync/remote-snapshot.js\";\nimport { errorMessage } from \"../sync/sync-errors.js\";\nimport {\n\tcompareSyncInclude,\n\tinspectRemoteSelection,\n\ttype RemoteSelectionDecision,\n\ttype RemoteSelectionState,\n\tsameSyncInclude,\n} from \"../sync/sync-policy.js\";\nimport {\n\ttype RunRoute,\n\ttype RunRouteResult,\n\trunCancellableOperation,\n} from \"./cancellable-operation.js\";\nimport { safeTerminalText } from \"./terminal-text.js\";\n\nconst STATUS_KEY = \"sync\";\n\nexport type RemoteSelectionOrigin = \"settings\" | \"sync\" | \"pull\" | \"push\";\n\nexport interface RemoteSelectionReviewOptions {\n\tdecision?: RemoteSelectionDecision;\n\torigin?: RemoteSelectionOrigin;\n\trunRoute?: RunRoute;\n\tcancelLabel?: string;\n\t/** The explicit mismatch is resolved or superseded; this does not imply file equality. */\n\tonSelectionResolved?: () => void;\n\twithStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;\n}\n\nexport type RemoteSelectionReviewResult =\n\t| { kind: \"back\" }\n\t| { kind: \"closed\" }\n\t| { kind: \"done\" }\n\t| { kind: \"stale\" }\n\t| { kind: \"route-result\"; result: RunRouteResult; route: \"sync\" | \"pull\" | \"push\" };\n\nexport async function showRemoteSelectionReview(\n\tctx: ExtensionContext,\n\tsetupName?: string,\n\tsignal?: AbortSignal,\n\tfactory: SyncBackendFactory = createSyncBackend,\n\toptions: RemoteSelectionReviewOptions = {},\n): Promise<RemoteSelectionReviewResult> {\n\ttry {\n\t\tlet decision = options.decision;\n\t\tif (!decision) {\n\t\t\tconst inspected = await inspectConfiguredRemoteSelection(ctx, setupName, signal, factory);\n\t\t\tif (!inspected || signal?.aborted) return { kind: \"stale\" };\n\t\t\t// A successful fresh read can supersede an old explicit mismatch without\n\t\t\t// changing local settings. This is not user cancellation or proof of file equality.\n\t\t\tif (inspected.kind === \"empty\" || inspected.state.kind !== \"different\") {\n\t\t\t\toptions.onSelectionResolved?.();\n\t\t\t}\n\t\t\tif (inspected.kind === \"empty\") {\n\t\t\t\tctx.ui.notify(\"Remote storage has no snapshot or synced-content list yet.\", \"info\");\n\t\t\t\treturn { kind: \"back\" };\n\t\t\t}\n\t\t\tif (inspected.state.kind === \"same\") {\n\t\t\t\tctx.ui.notify(\"Remote synced content already matches this sync setup.\", \"info\");\n\t\t\t\treturn { kind: \"back\" };\n\t\t\t}\n\t\t\tif (inspected.state.kind === \"legacy\") {\n\t\t\t\tif (ctx.mode !== \"tui\") {\n\t\t\t\t\tctx.ui.notify(formatLegacySummary(inspected.config, inspected.state.discovered), \"info\");\n\t\t\t\t\treturn { kind: \"back\" };\n\t\t\t\t}\n\t\t\t\tawait showLegacyDiscovery(ctx, inspected.config, inspected.state.discovered, signal);\n\t\t\t\treturn signal?.aborted ? { kind: \"stale\" } : { kind: \"back\" };\n\t\t\t}\n\t\t\tdecision = decisionFromState(inspected.config, inspected.state);\n\t\t}\n\n\t\tif (ctx.mode !== \"tui\") {\n\t\t\tctx.ui.notify(formatRemoteSelectionSummary(decision), \"warning\");\n\t\t\treturn { kind: \"back\" };\n\t\t}\n\n\t\tlet currentDecision = decision;\n\t\tfor (;;) {\n\t\t\tif (signal?.aborted) return { kind: \"stale\" };\n\t\t\tconst result = await showSelectionDifference(\n\t\t\t\tctx,\n\t\t\t\tcurrentDecision,\n\t\t\t\toptions.origin ?? \"settings\",\n\t\t\t\toptions.runRoute,\n\t\t\t\tsignal,\n\t\t\t\tfactory,\n\t\t\t\toptions,\n\t\t\t);\n\t\t\tif (result.kind !== \"refresh\") return result;\n\t\t\tconst refreshed = await runWithOptionalStateAccess(options, () =>\n\t\t\t\tinspectConfiguredRemoteSelection(ctx, currentDecision.setupName, signal, factory),\n\t\t\t);\n\t\t\tif (!refreshed || signal?.aborted) return { kind: \"stale\" };\n\t\t\tif (refreshed.kind === \"empty\") {\n\t\t\t\toptions.onSelectionResolved?.();\n\t\t\t\tctx.ui.notify(\"Remote storage no longer has a snapshot or synced-content list.\", \"warning\");\n\t\t\t\treturn { kind: \"back\" };\n\t\t\t}\n\t\t\tif (refreshed.state.kind !== \"different\") {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\trefreshed.state.kind === \"same\"\n\t\t\t\t\t\t? \"Remote synced content now matches this sync setup.\"\n\t\t\t\t\t\t: \"The refreshed legacy snapshot has no authoritative synced-content list.\",\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\toptions.onSelectionResolved?.();\n\t\t\t\treturn { kind: \"done\" };\n\t\t\t}\n\t\t\tcurrentDecision = decisionFromState(refreshed.config, refreshed.state);\n\t\t}\n\t} catch (error) {\n\t\tif (signal?.aborted) return { kind: \"stale\" };\n\t\tctx.ui.notify(`Could not review synced content: ${errorMessage(error)}`, \"error\");\n\t\treturn { kind: \"back\" };\n\t} finally {\n\t\tctx.ui.setStatus(STATUS_KEY, undefined);\n\t}\n}\n\ntype DifferenceFlowResult = RemoteSelectionReviewResult | { kind: \"refresh\" };\n\nasync function showSelectionDifference(\n\tctx: ExtensionContext,\n\tinitialDecision: RemoteSelectionDecision,\n\torigin: RemoteSelectionOrigin,\n\trunRoute: RunRoute | undefined,\n\tsessionSignal: AbortSignal | undefined,\n\tfactory: SyncBackendFactory,\n\toptions: Pick<\n\t\tRemoteSelectionReviewOptions,\n\t\t\"cancelLabel\" | \"onSelectionResolved\" | \"withStateAccess\"\n\t>,\n): Promise<DifferenceFlowResult> {\n\ttype Screen = \"choice\" | \"review\" | \"saved\";\n\ttype Action = \"adopt\" | \"keep\" | \"cancel\" | \"continue\" | \"done\";\n\tinterface FlowState {\n\t\tdecision: RemoteSelectionDecision;\n\t\tsaved: boolean;\n\t}\n\tlet flowState: FlowState = { decision: initialDecision, saved: false };\n\tlet continuationReview: PartialConfig | undefined;\n\tlet nextResult: RemoteSelectionReviewResult | undefined;\n\tlet refreshRequested = false;\n\tconst route = origin === \"settings\" ? \"sync\" : origin;\n\tconst menu = defineMenu<FlowState, Screen, Action, ExtensionContext>({\n\t\tstart: \"choice\",\n\t\tscreens: {\n\t\t\tchoice: ({ state }) => ({\n\t\t\t\tkind: \"actions\",\n\t\t\t\ttitle: \"Synced content differs\",\n\t\t\t\tlines: selectionSummaryLines(state.decision),\n\t\t\t\titems: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"review\",\n\t\t\t\t\t\tlabel: \"Review all paths (recommended)\",\n\t\t\t\t\t\tdescription: \"Compare exact remote-only, device-only, and ordered lists.\",\n\t\t\t\t\t\tto: \"review\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"adopt\",\n\t\t\t\t\t\tlabel: \"Use remote content list\",\n\t\t\t\t\t\tdescription: \"Save the reviewed list on this device without pulling files.\",\n\t\t\t\t\t\taction: \"adopt\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"keep\",\n\t\t\t\t\t\tlabel: \"Keep this device's content list and update remote\u2026\",\n\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\"Open the existing exact force-push preview without skipping confirmation.\",\n\t\t\t\t\t\taction: \"keep\",\n\t\t\t\t\t},\n\t\t\t\t\t{ id: \"cancel\", label: options.cancelLabel ?? \"Cancel\", action: \"cancel\" },\n\t\t\t\t],\n\t\t\t\thint: \"back\",\n\t\t\t}),\n\t\t\treview: ({ state }) => ({\n\t\t\t\tkind: \"review\",\n\t\t\t\ttitle: `Review synced content \u00B7 ${safeTerminalText(state.decision.setupName)}`,\n\t\t\t\tcontent: formatSelectionDifference(state.decision),\n\t\t\t\tformat: { kind: \"text\" },\n\t\t\t\tviewportSize: \"adaptive\",\n\t\t\t\thint: \"back\",\n\t\t\t}),\n\t\t\tsaved: ({ state }) => ({\n\t\t\t\tkind: \"actions\",\n\t\t\t\ttitle: \"Remote content list saved\",\n\t\t\t\tlines: [\n\t\t\t\t\t`Sync setup: ${safeTerminalText(state.decision.setupName)}`,\n\t\t\t\t\t\"Only the included-content setting was saved.\",\n\t\t\t\t\t\"No files were pulled and sync state was not changed.\",\n\t\t\t\t],\n\t\t\t\titems: [\n\t\t\t\t\t...(runRoute\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: \"continue\",\n\t\t\t\t\t\t\t\t\tlabel: continueLabel(origin),\n\t\t\t\t\t\t\t\t\tdescription: \"Start a fresh check and exact preview for this sync setup.\",\n\t\t\t\t\t\t\t\t\taction: \"continue\" as const,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: []),\n\t\t\t\t\t{ id: \"done\", label: \"Done\", action: \"done\" },\n\t\t\t\t],\n\t\t\t\thint: \"close\",\n\t\t\t}),\n\t\t},\n\t\tactions: {\n\t\t\tadopt: async ({ state, signal: actionSignal }) => {\n\t\t\t\tconst signal = combineSignals(sessionSignal, actionSignal);\n\t\t\t\ttry {\n\t\t\t\t\tconst currentConfig = await loadConfig(state.decision.setupName);\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tassertLocalSelectionCurrent(currentConfig, state.decision);\n\t\t\t\t\tif (\n\t\t\t\t\t\t!currentConfig.include.includes(\"sessions\") &&\n\t\t\t\t\t\tstate.decision.remoteInclude.includes(\"sessions\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst acknowledged = await ctx.ui.confirm(\n\t\t\t\t\t\t\t\"Use a content list that includes session conversations?\",\n\t\t\t\t\t\t\t\"Session JSONL may contain prompts, tool output, file paths, images, and secrets. This saves the list only; it does not pull files.\",\n\t\t\t\t\t\t\t{ signal },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\t\tif (!acknowledged) return { kind: \"stay\" as const };\n\t\t\t\t\t}\n\t\t\t\t\tconst review = await runWithOptionalStateAccess(options, async () => {\n\t\t\t\t\t\tconst loaded = await loadAdoptionReview(state.decision, signal, factory);\n\t\t\t\t\t\tif (signal.aborted) throw signal.reason;\n\t\t\t\t\t\tawait revalidateAndAdopt(loaded, state.decision, signal);\n\t\t\t\t\t\treturn loaded;\n\t\t\t\t\t});\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\toptions.onSelectionResolved?.();\n\t\t\t\t\tcontinuationReview = {\n\t\t\t\t\t\t...review.storageReview,\n\t\t\t\t\t\tsetupName: state.decision.setupName,\n\t\t\t\t\t\tinclude: [...state.decision.remoteInclude],\n\t\t\t\t\t\tautomatic: review.config.automatic,\n\t\t\t\t\t\tonSwitch: review.config.onSwitch,\n\t\t\t\t\t};\n\t\t\t\t\tflowState = { decision: state.decision, saved: true };\n\t\t\t\t\treturn { kind: \"to\" as const, screen: \"saved\" as const };\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tif (isStaleReviewError(error)) {\n\t\t\t\t\t\tctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, \"warning\");\n\t\t\t\t\t\trefreshRequested = true;\n\t\t\t\t\t\treturn { kind: \"close\" as const };\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(`Could not save the remote content list: ${errorMessage(error)}`, \"error\");\n\t\t\t\t\treturn { kind: \"stay\" as const };\n\t\t\t\t}\n\t\t\t},\n\t\t\tkeep: async ({ state, signal: actionSignal }) => {\n\t\t\t\tif (!runRoute) {\n\t\t\t\t\tctx.ui.notify(\"The reviewed update-remote route is unavailable.\", \"error\");\n\t\t\t\t\treturn { kind: \"stay\" as const };\n\t\t\t\t}\n\t\t\t\tconst signal = combineSignals(sessionSignal, actionSignal);\n\t\t\t\ttry {\n\t\t\t\t\tconst latest = await loadConfig(state.decision.setupName);\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tassertLocalSelectionCurrent(latest, state.decision);\n\t\t\t\t\tconst result = await runCancellableOperation(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"Preparing this device's push preview\u2026\",\n\t\t\t\t\t\t\"push --force\",\n\t\t\t\t\t\trunRoute,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommitAware: true,\n\t\t\t\t\t\t\tcancelledMessage: \"Push preparation cancelled; no remote files were changed.\",\n\t\t\t\t\t\t\ttarget: state.decision.setupName,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\treturn handleNestedRouteResult(result, \"push\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tif (isStaleReviewError(error)) {\n\t\t\t\t\t\tctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, \"warning\");\n\t\t\t\t\t\trefreshRequested = true;\n\t\t\t\t\t\treturn { kind: \"close\" as const };\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(`Could not prepare the remote update: ${errorMessage(error)}`, \"error\");\n\t\t\t\t\treturn { kind: \"stay\" as const };\n\t\t\t\t}\n\t\t\t},\n\t\t\tcontinue: async ({ state, signal: actionSignal }) => {\n\t\t\t\tif (!runRoute || !continuationReview) return { kind: \"stay\" as const };\n\t\t\t\tconst signal = combineSignals(sessionSignal, actionSignal);\n\t\t\t\ttry {\n\t\t\t\t\tconst latest = await loadPartialConfig(state.decision.setupName);\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tif (!sameContinuationReview(latest, continuationReview)) {\n\t\t\t\t\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t\t\t\t\t`Sync setup \u201C${safeTerminalText(state.decision.setupName)}\u201D changed after the remote content list was saved.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst result = await runCancellableOperation(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\tcontinueBusyLabel(origin),\n\t\t\t\t\t\troute,\n\t\t\t\t\t\trunRoute,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcommitAware: true,\n\t\t\t\t\t\t\tcancelledMessage: continuationCancelledMessage(route),\n\t\t\t\t\t\t\ttarget: state.decision.setupName,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\treturn handleNestedRouteResult(result, route);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal.aborted) return { kind: \"close\" as const };\n\t\t\t\t\tctx.ui.notify(`Could not continue: ${errorMessage(error)}`, \"error\");\n\t\t\t\t\treturn { kind: \"stay\" as const };\n\t\t\t\t}\n\t\t\t},\n\t\t\tcancel: async () => ({ kind: \"back\" }),\n\t\t\tdone: async () => {\n\t\t\t\tnextResult = { kind: \"done\" };\n\t\t\t\treturn { kind: \"close\" };\n\t\t\t},\n\t\t},\n\t});\n\n\tfunction handleNestedRouteResult(\n\t\tresult: Awaited<ReturnType<typeof runCancellableOperation>>,\n\t\tnestedRoute: \"sync\" | \"pull\" | \"push\",\n\t) {\n\t\tif (result.kind === \"completed\" && result.outcome === \"applied\") {\n\t\t\toptions.onSelectionResolved?.();\n\t\t}\n\t\tif (result.kind === \"closed\") {\n\t\t\tnextResult = { kind: \"closed\" };\n\t\t\treturn { kind: \"close\" as const };\n\t\t}\n\t\tif (\n\t\t\tresult.kind === \"cancelled\" ||\n\t\t\t(result.kind === \"completed\" && result.outcome === \"cancelled\") ||\n\t\t\tresult.kind === \"failed\"\n\t\t) {\n\t\t\treturn { kind: \"stay\" as const };\n\t\t}\n\t\tnextResult = { kind: \"route-result\", result, route: nestedRoute };\n\t\treturn { kind: \"close\" as const };\n\t}\n\n\tconst menuResult = await runMenu(ctx, menu, {\n\t\tgetState: () => flowState,\n\t\tsignal: sessionSignal,\n\t\tisCurrent: () => !sessionSignal?.aborted,\n\t\tonError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), \"error\"),\n\t});\n\tif (sessionSignal?.aborted || menuResult.kind === \"stale\") return { kind: \"stale\" };\n\tif (refreshRequested) return { kind: \"refresh\" };\n\tif (nextResult) return nextResult;\n\tif (menuResult.kind === \"closed\") {\n\t\treturn menuResult.reason === \"back\" ? { kind: \"back\" } : { kind: \"closed\" };\n\t}\n\treturn { kind: \"closed\" };\n}\n\ninterface AdoptionReview {\n\tconfig: AnySyncConfig;\n\tstorageReview: SyncSetupStorageReview;\n\tbackend: SyncBackend;\n\treviewedHead: RemoteHead;\n}\n\nasync function loadAdoptionReview(\n\tdecision: RemoteSelectionDecision,\n\tsignal: AbortSignal,\n\tfactory: SyncBackendFactory,\n): Promise<AdoptionReview> {\n\tconst config = await loadConfig(decision.setupName);\n\tthrowIfAborted(signal);\n\tassertLocalSelectionCurrent(config, decision);\n\tconst partial = await loadPartialConfig(decision.setupName);\n\tthrowIfAborted(signal);\n\tif (!sameSyncInclude(partial.include, decision.localInclude)) {\n\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t`Sync setup \u201C${safeTerminalText(decision.setupName)}\u201D changed while the comparison was open.`,\n\t\t);\n\t}\n\tconst backend = await factory(config);\n\tthrowIfAborted(signal);\n\tconst reviewedHead = await backend.readHead(signal);\n\tthrowIfAborted(signal);\n\tif (!reviewedHead) {\n\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t\"Remote storage changed while the comparison was open.\",\n\t\t);\n\t}\n\tconst snapshot = await readSnapshotForHead(backend, reviewedHead, signal);\n\tthrowIfAborted(signal);\n\tconst state = inspectRemoteSelection(config.include, snapshot);\n\tif (state.kind !== \"different\" || !sameSyncInclude(state.include, decision.remoteInclude)) {\n\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t\"Remote synced content changed while the comparison was open.\",\n\t\t);\n\t}\n\treturn { config, storageReview: partial, backend, reviewedHead };\n}\n\nasync function revalidateAndAdopt(\n\treview: AdoptionReview,\n\tdecision: RemoteSelectionDecision,\n\tsignal: AbortSignal,\n) {\n\tconst currentHead = await review.backend.readHead(signal);\n\tthrowIfAborted(signal);\n\tif (\n\t\t!currentHead ||\n\t\t!review.backend.sameRevision(review.reviewedHead.revision, currentHead.revision)\n\t) {\n\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t\"Remote storage changed while the comparison was open.\",\n\t\t);\n\t}\n\tconst currentSnapshot = await readSnapshotForHead(review.backend, currentHead, signal);\n\tthrowIfAborted(signal);\n\tconst currentState = inspectRemoteSelection(review.config.include, currentSnapshot);\n\tif (\n\t\tcurrentState.kind !== \"different\" ||\n\t\t!sameSyncInclude(currentState.include, decision.remoteInclude)\n\t) {\n\t\tthrow new StaleRemoteSelectionReviewError(\n\t\t\t\"Remote synced content changed while the comparison was open.\",\n\t\t);\n\t}\n\tawait updateSyncSetup(\n\t\tdecision.setupName,\n\t\t(setup) => ({\n\t\t\t...setup,\n\t\t\tsync: { ...setup.sync, include: [...decision.remoteInclude] },\n\t\t}),\n\t\t{\n\t\t\texpectedStorage: review.storageReview,\n\t\t\texpectedInclude: decision.localInclude,\n\t\t\tsignal,\n\t\t},\n\t);\n}\n\nasync function inspectConfiguredRemoteSelection(\n\tctx: ExtensionContext,\n\tsetupName: string | undefined,\n\tsignal: AbortSignal | undefined,\n\tfactory: SyncBackendFactory,\n): Promise<\n\t| { kind: \"empty\"; config: AnySyncConfig }\n\t| { kind: \"selection\"; config: AnySyncConfig; state: RemoteSelectionState }\n\t| undefined\n> {\n\tconst config = await loadConfig(setupName);\n\tif (signal?.aborted) return undefined;\n\tctx.ui.setStatus(STATUS_KEY, `checking synced content for ${safeTerminalText(config.setupName)}`);\n\tconst backend = await factory(config);\n\tif (signal?.aborted) return undefined;\n\tconst head = await backend.readHead(signal);\n\tif (signal?.aborted) return undefined;\n\tif (!head) return { kind: \"empty\", config };\n\tconst snapshot = await readSnapshotForHead(backend, head, signal);\n\tif (signal?.aborted) return undefined;\n\treturn {\n\t\tkind: \"selection\",\n\t\tconfig,\n\t\tstate: inspectRemoteSelection(config.include, snapshot),\n\t};\n}\n\nasync function showLegacyDiscovery(\n\tctx: ExtensionContext,\n\tconfig: AnySyncConfig,\n\tdiscovered: string[],\n\tsignal?: AbortSignal,\n) {\n\tconst menu = defineMenu<undefined, \"choice\" | \"review\", \"back\", ExtensionContext>({\n\t\tstart: \"choice\",\n\t\tscreens: {\n\t\t\tchoice: () => ({\n\t\t\t\tkind: \"actions\",\n\t\t\t\ttitle: `Compare synced content \u00B7 ${safeTerminalText(config.setupName)}`,\n\t\t\t\tlines: [\n\t\t\t\t\t\"This legacy snapshot has no portable synced-content list.\",\n\t\t\t\t\t\"Discovered paths are partial and read-only; preserved files may not have been selected.\",\n\t\t\t\t],\n\t\t\t\titems: [\n\t\t\t\t\t{ id: \"review\", label: \"Review discovered paths\", to: \"review\" },\n\t\t\t\t\t{ id: \"back\", label: \"Back\", action: \"back\" },\n\t\t\t\t],\n\t\t\t\thint: \"close\",\n\t\t\t}),\n\t\t\treview: () => ({\n\t\t\t\tkind: \"review\",\n\t\t\t\ttitle: \"Partial discovery from legacy snapshot\",\n\t\t\t\tcontent: [\n\t\t\t\t\t\"Partial discovery only \u2014 not an authoritative selection.\",\n\t\t\t\t\t\"\",\n\t\t\t\t\t...(discovered.length > 0\n\t\t\t\t\t\t? discovered.map((item) => `Discovered: ${safeTerminalText(item)}`)\n\t\t\t\t\t\t: [\"No safe paths were discovered.\"]),\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"Use Add custom path\u2026 in the local Included Content editor if needed.\",\n\t\t\t\t].join(\"\\n\"),\n\t\t\t\tformat: { kind: \"text\" },\n\t\t\t\tviewportSize: \"adaptive\",\n\t\t\t\thint: \"back\",\n\t\t\t}),\n\t\t},\n\t\tactions: { back: async () => ({ kind: \"close\" }) },\n\t});\n\tawait runMenu(ctx, menu, {\n\t\tgetState: () => undefined,\n\t\tsignal,\n\t\tisCurrent: () => !signal?.aborted,\n\t});\n}\n\nfunction selectionSummaryLines(decision: RemoteSelectionDecision) {\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\treturn [\n\t\t`Sync setup: ${safeTerminalText(decision.setupName)}`,\n\t\t\"Nothing changed. Review both lists before choosing what happens next.\",\n\t\t...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? [\"Only the ordering differs; membership is the same.\"]\n\t\t\t: [\n\t\t\t\t\t`Remote-only paths: ${comparison.remoteOnly.length} \u00B7 Device-only paths: ${comparison.localOnly.length}`,\n\t\t\t\t]),\n\t];\n}\n\nfunction formatSelectionDifference(decision: RemoteSelectionDecision) {\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\treturn [\n\t\t...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? [\"Only ordering differs; both lists contain the same paths.\", \"\"]\n\t\t\t: []),\n\t\t\"Remote-only paths:\",\n\t\t...(comparison.remoteOnly.length > 0\n\t\t\t? comparison.remoteOnly.map((item) => `+ ${safeTerminalText(item)}`)\n\t\t\t: [\"(none)\"]),\n\t\t\"\",\n\t\t\"Device-only paths:\",\n\t\t...(comparison.localOnly.length > 0\n\t\t\t? comparison.localOnly.map((item) => `- ${safeTerminalText(item)}`)\n\t\t\t: [\"(none)\"]),\n\t\t\"\",\n\t\t\"Remote ordered list:\",\n\t\t...(decision.remoteInclude.length > 0\n\t\t\t? decision.remoteInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`)\n\t\t\t: [\"(none)\"]),\n\t\t\"\",\n\t\t\"This device's ordered list:\",\n\t\t...(decision.localInclude.length > 0\n\t\t\t? decision.localInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`)\n\t\t\t: [\"(none)\"]),\n\t\t\"\",\n\t\t\"Using the remote list saves settings only and does not pull files.\",\n\t].join(\"\\n\");\n}\n\nfunction formatRemoteSelectionSummary(decision: RemoteSelectionDecision) {\n\tconst comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);\n\treturn [\n\t\t`Synced content for \u201C${safeTerminalText(decision.setupName)}\u201D differs from this device.`,\n\t\t`Remote-only: ${safeList(comparison.remoteOnly)}`,\n\t\t`Device-only: ${safeList(comparison.localOnly)}`,\n\t\t...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0\n\t\t\t? [\n\t\t\t\t\t\"Only ordering differs.\",\n\t\t\t\t\t`Remote order: ${safeList(decision.remoteInclude)}`,\n\t\t\t\t\t`Device order: ${safeList(decision.localInclude)}`,\n\t\t\t\t]\n\t\t\t: []),\n\t\t\"Run /sync in TUI to choose a content list; RPC review is read-only.\",\n\t].join(\"\\n\");\n}\n\nfunction formatLegacySummary(config: AnySyncConfig, discovered: readonly string[]) {\n\treturn `Remote snapshot for \u201C${safeTerminalText(config.setupName)}\u201D has no portable synced-content list; ${discovered.length} safe path${discovered.length === 1 ? \" was\" : \"s were\"} discovered, but the result is partial and read-only.`;\n}\n\nfunction decisionFromState(\n\tconfig: AnySyncConfig,\n\tstate: Extract<RemoteSelectionState, { kind: \"different\" }>,\n): RemoteSelectionDecision {\n\treturn {\n\t\tsetupName: config.setupName,\n\t\tconfigIdentity: syncConfigReviewFingerprint(config),\n\t\tlocalInclude: [...config.include],\n\t\tremoteInclude: [...state.include],\n\t};\n}\n\nfunction assertLocalSelectionCurrent(config: AnySyncConfig, decision: RemoteSelectionDecision) {\n\tif (\n\t\tsyncConfigReviewFingerprint(config) === decision.configIdentity &&\n\t\tsameSyncInclude(config.include, decision.localInclude)\n\t) {\n\t\treturn;\n\t}\n\tthrow new StaleRemoteSelectionReviewError(\n\t\t`Sync setup \u201C${safeTerminalText(config.setupName)}\u201D changed while the comparison was open.`,\n\t);\n}\n\nfunction sameContinuationReview(left: PartialConfig, right: PartialConfig) {\n\treturn (\n\t\tleft.setupName === right.setupName &&\n\t\tleft.connectionName === right.connectionName &&\n\t\tleft.storageKind === right.storageKind &&\n\t\tleft.storagePath === right.storagePath &&\n\t\tleft.bucket === right.bucket &&\n\t\tleft.branch === right.branch &&\n\t\tsameSyncInclude(left.include, right.include)\n\t);\n}\n\nfunction continueLabel(origin: RemoteSelectionOrigin) {\n\tif (origin === \"pull\") return \"Continue Pull now\u2026\";\n\tif (origin === \"push\") return \"Continue Push now\u2026\";\n\treturn \"Continue Sync now\u2026\";\n}\n\nfunction continueBusyLabel(origin: RemoteSelectionOrigin) {\n\tif (origin === \"pull\") return \"Checking remote changes\u2026\";\n\tif (origin === \"push\") return \"Preparing push preview\u2026\";\n\treturn \"Checking current sync setup\u2026\";\n}\n\nfunction continuationCancelledMessage(route: \"sync\" | \"pull\" | \"push\") {\n\tif (route === \"pull\") return \"Pull check cancelled; no local files were changed.\";\n\tif (route === \"push\") return \"Push preparation cancelled; no remote files were changed.\";\n\treturn \"Sync check cancelled; no settings or files were changed.\";\n}\n\nfunction safeList(values: readonly string[]) {\n\treturn values.length > 0 ? values.map(safeTerminalText).join(\", \") : \"none\";\n}\n\nclass StaleRemoteSelectionReviewError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"StaleRemoteSelectionReviewError\";\n\t}\n}\n\nfunction isStaleReviewError(error: unknown) {\n\treturn (\n\t\terror instanceof StaleRemoteSelectionReviewError || error instanceof SyncSetupReviewChangedError\n\t);\n}\n\nfunction runWithOptionalStateAccess<T>(\n\toptions: Pick<RemoteSelectionReviewOptions, \"withStateAccess\">,\n\ttask: () => Promise<T>,\n) {\n\treturn options.withStateAccess ? options.withStateAccess(task) : task();\n}\n\nfunction combineSignals(sessionSignal: AbortSignal | undefined, actionSignal: AbortSignal) {\n\treturn sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;\n}\n\nfunction throwIfAborted(signal: AbortSignal) {\n\tif (!signal.aborted) return;\n\tthrow signal.reason instanceof Error\n\t\t? signal.reason\n\t\t: new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n", "import {\n\tBorderedLoader,\n\ttype ExtensionContext,\n\ttype KeybindingsManager,\n} from \"@earendil-works/pi-coding-agent\";\nimport { Key, matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\nimport { runCustomInteraction } from \"@narumitw/pi-tui-kit\";\nimport type { SetupPullOutcome } from \"../sync/setup-switch.js\";\nimport type { SyncDecision } from \"../sync/sync-decision.js\";\nimport type { RemoteSelectionDecision } from \"../sync/sync-policy.js\";\nimport { safeTerminalText } from \"./terminal-text.js\";\n\nexport type RunRouteResult =\n\t| { kind: \"completed\"; outcome?: SetupPullOutcome }\n\t| { kind: \"decision-required\"; decision: SyncDecision }\n\t| { kind: \"remote-selection-required\"; decision: RemoteSelectionDecision }\n\t| { kind: \"failed\" };\n\nexport type RunRoute = (\n\troute: string,\n\tsignal?: AbortSignal,\n\tonCommit?: () => void,\n\ttarget?: string,\n) => Promise<RunRouteResult | undefined>;\n\nexport type CancellableOperationResult =\n\t| RunRouteResult\n\t| { kind: \"closed\" }\n\t| { kind: \"cancelled\" };\n\ninterface CancellableOperationOptions {\n\tcommitAware?: boolean;\n\tcancelledMessage?: string | null;\n\ttarget?: string;\n\tsignal?: AbortSignal;\n}\n\nexport async function runCancellableOperation(\n\tctx: ExtensionContext,\n\tmessage: string,\n\troute: string,\n\trunRoute: RunRoute,\n\toptions: CancellableOperationOptions = {},\n): Promise<CancellableOperationResult> {\n\tconst {\n\t\tcommitAware = false,\n\t\tcancelledMessage = \"Check cancelled; no settings or files were changed.\",\n\t\ttarget,\n\t\tsignal,\n\t} = options;\n\tif (ctx.mode !== \"tui\") {\n\t\treturn (await runRoute(route, signal, undefined, target)) ?? { kind: \"failed\" };\n\t}\n\tlet commitStarted = false;\n\tlet routeResult: RunRouteResult | undefined;\n\tconst interaction = await runCustomInteraction<\n\t\t{ cancelled?: boolean; error?: unknown },\n\t\tExtensionContext\n\t>(ctx, {\n\t\tsignal,\n\t\tisCurrent: () => !signal?.aborted,\n\t\tcreate: ({ tui, theme, keybindings, signal: interactionSignal, complete }) => {\n\t\t\tconst loader = new BorderedLoader(tui, theme, message, { cancellable: false });\n\t\t\tconst cancelHint = `${keybindingText(keybindings, \"tui.select.cancel\", \"esc\")} cancel`;\n\t\t\tconst operation = runRoute(\n\t\t\t\troute,\n\t\t\t\tinteractionSignal,\n\t\t\t\tcommitAware ? () => (commitStarted = true) : undefined,\n\t\t\t\ttarget,\n\t\t\t).then(\n\t\t\t\t(result) => {\n\t\t\t\t\trouteResult = result;\n\t\t\t\t\tcomplete({});\n\t\t\t\t},\n\t\t\t\t(error: unknown) => complete({ error }),\n\t\t\t);\n\t\t\treturn {\n\t\t\t\trender(width: number) {\n\t\t\t\t\tconst safeWidth = Math.max(1, width);\n\t\t\t\t\tconst lines = loader.render(safeWidth);\n\t\t\t\t\tconst bottomBorder = lines.at(-1);\n\t\t\t\t\treturn [\n\t\t\t\t\t\t...lines.slice(0, -1),\n\t\t\t\t\t\ttruncateToWidth(theme.fg(\"dim\", cancelHint), safeWidth, \"\"),\n\t\t\t\t\t\t...(bottomBorder === undefined ? [] : [bottomBorder]),\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t\tinvalidate: () => loader.invalidate(),\n\t\t\t\thandleInput(data: string) {\n\t\t\t\t\tif (!matchesKey(data, Key.ctrl(\"c\")) && !keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (commitStarted) {\n\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\"Applying or publishing has started and cannot be cancelled safely.\",\n\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcomplete({ cancelled: true });\n\t\t\t\t},\n\t\t\t\tdispose: () => loader.dispose(),\n\t\t\t\twaitForPending: () => operation,\n\t\t\t};\n\t\t},\n\t});\n\tif (interaction.kind === \"error\") throw interaction.error;\n\tif (interaction.kind !== \"completed\") return { kind: \"closed\" };\n\tif (interaction.value.cancelled) {\n\t\tif (cancelledMessage) ctx.ui.notify(cancelledMessage, \"info\");\n\t\treturn { kind: \"cancelled\" };\n\t}\n\tif (interaction.value.error) throw interaction.value.error;\n\treturn routeResult ?? { kind: \"failed\" };\n}\n\nfunction keybindingText(\n\tkeybindings: Pick<KeybindingsManager, \"getKeys\">,\n\tbinding: Parameters<KeybindingsManager[\"getKeys\"]>[0],\n\tfallback: string,\n) {\n\tconst keys = [...new Set([...keybindings.getKeys(binding), \"ctrl+c\"])]\n\t\t.map(String)\n\t\t.map((key) => {\n\t\t\tif (key === \"return\") return \"enter\";\n\t\t\tif (key === \"escape\") return \"esc\";\n\t\t\treturn safeTerminalText(key);\n\t\t})\n\t\t.filter(Boolean);\n\treturn keys.join(\"/\") || fallback;\n}\n"],
5
+ "mappings": "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",
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": "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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