@narumitw/pi-sync 0.49.14 → 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 +134 -241
  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-RGJ6SOYR.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-3NDD5QPM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-APZVBSM6.ts → chunk-C7XY5NF5.ts} +214 -365
  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-LJDSHNS3.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-FR6CVXAL.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-4F3W4CSC.ts → chunk-LCRI3ABW.ts} +22 -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-EWX3TPLQ.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-BHJUISB4.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-LONPFFNP.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-5WJNWFTG.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-4UABOP44.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-3QZGO2EF.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-X3W3FALZ.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-WBSV4MQG.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-MPAFVVHA.ts → sync-operations-ONJ27QRL.ts} +381 -299
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-36H7ULG2.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 +211 -0
  84. package/package.json +6 -8
  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} +40 -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} +41 -330
  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 -284
  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} +42 -11
  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-3NDD5QPM.ts.map +0 -7
  164. package/dist/chunks/chunk-4F3W4CSC.ts.map +0 -7
  165. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5WJNWFTG.ts.map +0 -7
  168. package/dist/chunks/chunk-APZVBSM6.ts.map +0 -7
  169. package/dist/chunks/chunk-BHJUISB4.ts.map +0 -7
  170. package/dist/chunks/chunk-EWX3TPLQ.ts.map +0 -7
  171. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  172. package/dist/chunks/chunk-FR6CVXAL.ts.map +0 -7
  173. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  174. package/dist/chunks/chunk-JNOOSPEW.ts +0 -82
  175. package/dist/chunks/chunk-JNOOSPEW.ts.map +0 -7
  176. package/dist/chunks/chunk-LJDSHNS3.ts.map +0 -7
  177. package/dist/chunks/chunk-LONPFFNP.ts.map +0 -7
  178. package/dist/chunks/chunk-RGJ6SOYR.ts.map +0 -7
  179. package/dist/chunks/chunk-W64NEHXT.ts +0 -167
  180. package/dist/chunks/chunk-W64NEHXT.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-4UABOP44.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-RNRQA3PW.ts +0 -18
  185. package/dist/chunks/manager-ui-QO2P4V3Z.ts +0 -2757
  186. package/dist/chunks/manager-ui-QO2P4V3Z.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-LHSAGKGA.ts +0 -17
  188. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +0 -7
  189. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -910
  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-RNRQA3PW.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-LHSAGKGA.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-X3W3FALZ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-WBSV4MQG.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-36H7ULG2.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
@@ -0,0 +1,330 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ normalizeWebDavPath,
4
+ normalizeWebDavUrl,
5
+ validateWebDavCredentials,
6
+ } from "../../backends/webdav/webdav-config.js";
7
+ import {
8
+ addStorageConnection,
9
+ addSyncSetup,
10
+ saveNewV3Settings,
11
+ updateStorageConnection,
12
+ updateSyncSetup,
13
+ } from "../../settings/settings-management.js";
14
+ import type { PartialConfig } from "../../settings/settings-types.js";
15
+ import { automaticSyncSummary } from "../automatic-sync-summary.js";
16
+ import { promptSecret } from "../secret-input.js";
17
+ import { safeTerminalText as safe } from "../terminal-text.js";
18
+ import { promptAvailableSetupStorage } from "./setup-location-ui.js";
19
+ import {
20
+ chooseAutomaticSync,
21
+ chooseSessions,
22
+ chooseSetupContent,
23
+ includedContentLines,
24
+ promptResourceName,
25
+ readSetupConnection,
26
+ } from "./setup-prompts.js";
27
+ import { saveReviewedDraft } from "./setup-review.js";
28
+ import { promptTextInput } from "./text-input.js";
29
+
30
+ export async function showWebDavSetup(
31
+ ctx: ExtensionCommandContext,
32
+ targetName: string,
33
+ signal?: AbortSignal,
34
+ ) {
35
+ const url = await promptWebDavUrl(ctx, signal);
36
+ if (!url) return false;
37
+ const username = await promptUsername(ctx, signal);
38
+ if (!username) return false;
39
+ const password = await promptSecret(ctx, "WebDAV password", { signal });
40
+ signal?.throwIfAborted();
41
+ if (password === undefined) return false;
42
+ const remotePath = await chooseDestination(ctx, signal);
43
+ if (!remotePath) return false;
44
+ const include = await chooseSetupContent(ctx, signal);
45
+ if (!include) return false;
46
+ const automatic = await chooseAutomaticSync(ctx, signal);
47
+ if (automatic === undefined) return false;
48
+ const sessions = await chooseSessions(ctx, signal);
49
+ if (sessions === undefined) return false;
50
+ const saved = await saveReviewedDraft(
51
+ ctx,
52
+ "Review WebDAV setup",
53
+ [
54
+ `Sync setup: ${safe(targetName)}`,
55
+ `Storage connection: ${safe(targetName)} (WebDAV)`,
56
+ `URL: ${safe(url)}`,
57
+ `Storage location: ${safe(remotePath)}`,
58
+ "Username and password: stored privately (values hidden)",
59
+ ...includedContentLines(include, sessions),
60
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
61
+ "Check setup tests whether this server can safely save sync updates using a temporary probe.",
62
+ "Saving does not contact remote storage or start syncing.",
63
+ ],
64
+ "Save setup",
65
+ (saveSignal) =>
66
+ saveNewV3Settings(
67
+ {
68
+ setupName: targetName,
69
+ connectionName: targetName,
70
+ connection: { type: "webdav", url, credentials: { username, password } },
71
+ setup: {
72
+ storage: { connection: targetName, path: remotePath },
73
+ sync: { include: [...include, ...(sessions ? ["sessions"] : [])], automatic },
74
+ },
75
+ },
76
+ saveSignal,
77
+ ),
78
+ signal,
79
+ );
80
+ if (saved && !signal?.aborted)
81
+ ctx.ui.notify(`Sync setup “${safe(targetName)}” saved. Choose Sync now to start.`, "info");
82
+ return saved;
83
+ }
84
+
85
+ export async function showAddWebDavTarget(
86
+ ctx: ExtensionCommandContext,
87
+ name: string,
88
+ profile: string,
89
+ signal?: AbortSignal,
90
+ ) {
91
+ const remotePath = await chooseDestination(ctx, signal);
92
+ if (!remotePath) return false;
93
+ const storage = await promptAvailableSetupStorage(
94
+ ctx,
95
+ { connection: profile, path: remotePath },
96
+ signal,
97
+ );
98
+ if (!storage) return false;
99
+ const include = await chooseSetupContent(ctx, signal);
100
+ if (!include) return false;
101
+ const automatic = await chooseAutomaticSync(ctx, signal);
102
+ if (automatic === undefined) return false;
103
+ const connection = await readSetupConnection(profile, signal);
104
+ if (connection.type !== "webdav") throw new Error("Storage connection changed; reopen setup.");
105
+ const saved = await saveReviewedDraft(
106
+ ctx,
107
+ "Review WebDAV sync setup",
108
+ [
109
+ `Sync setup: ${safe(name)}`,
110
+ `Storage connection: ${safe(profile)}`,
111
+ `URL: ${safe(connection.url)}`,
112
+ `Storage location: ${safe(storage.path)}`,
113
+ ...includedContentLines(include),
114
+ `Automatic sync: ${automaticSyncSummary(automatic)}`,
115
+ "Adding this setup does not sync or modify remote data.",
116
+ ],
117
+ "Add sync setup",
118
+ (saveSignal) =>
119
+ addSyncSetup(name, { storage, sync: { include, automatic } }, saveSignal, connection),
120
+ signal,
121
+ );
122
+ if (saved && !signal?.aborted) ctx.ui.notify(`Added sync setup “${safe(name)}”.`, "info");
123
+ return saved;
124
+ }
125
+
126
+ export async function showEditWebDavTarget(
127
+ ctx: ExtensionCommandContext,
128
+ partial: PartialConfig,
129
+ signal?: AbortSignal,
130
+ ) {
131
+ const remotePath = await chooseDestination(ctx, signal, partial.storagePath);
132
+ if (!remotePath) return false;
133
+ const connection = await readSetupConnection(partial.connectionName, signal);
134
+ if (connection.type !== "webdav") throw new Error("Storage connection changed; reopen setup.");
135
+ const saved = await saveReviewedDraft(
136
+ ctx,
137
+ "Review sync setup",
138
+ [
139
+ `Sync setup: ${safe(partial.setupName)}`,
140
+ `Storage connection: ${safe(partial.connectionName)}`,
141
+ `URL: ${safe(connection.url)}`,
142
+ `Storage path: ${safe(partial.storagePath)} → ${safe(remotePath)}`,
143
+ "Saving changes the future storage location only; it does not move or delete remote data.",
144
+ ],
145
+ "Save sync setup",
146
+ (saveSignal) =>
147
+ updateSyncSetup(
148
+ partial.setupName,
149
+ (setup) => ({
150
+ ...setup,
151
+ storage: { ...setup.storage, path: remotePath },
152
+ }),
153
+ { expectedStorage: partial, expectedConnection: connection, signal: saveSignal },
154
+ ),
155
+ signal,
156
+ );
157
+ if (saved && !signal?.aborted)
158
+ ctx.ui.notify(`Saved sync setup “${safe(partial.setupName)}”.`, "info");
159
+ return saved;
160
+ }
161
+
162
+ export async function showAddWebDavStorageProfile(
163
+ ctx: ExtensionCommandContext,
164
+ signal?: AbortSignal,
165
+ ) {
166
+ const name = await promptResourceName(ctx, "storage connection", "webdav", signal, true);
167
+ if (!name) return false;
168
+ const url = await promptWebDavUrl(ctx, signal);
169
+ if (!url) return false;
170
+ const username = await promptUsername(ctx, signal);
171
+ if (!username) return false;
172
+ const password = await promptSecret(ctx, "WebDAV password", { signal });
173
+ signal?.throwIfAborted();
174
+ if (password === undefined) return false;
175
+ const saved = await saveReviewedDraft(
176
+ ctx,
177
+ "Review storage connection",
178
+ [
179
+ `Name: ${safe(name)}`,
180
+ "Type: WebDAV",
181
+ `URL: ${safe(url)}`,
182
+ "Username and password: stored privately (values hidden)",
183
+ "Adding a connection does not contact the server or start syncing.",
184
+ ],
185
+ "Add storage connection",
186
+ (saveSignal) =>
187
+ addStorageConnection(
188
+ name,
189
+ { type: "webdav", url, credentials: { username, password } },
190
+ saveSignal,
191
+ ),
192
+ signal,
193
+ );
194
+ if (saved && !signal?.aborted) ctx.ui.notify(`Added storage connection “${safe(name)}”.`, "info");
195
+ return saved;
196
+ }
197
+
198
+ export async function showEditWebDavStorageProfile(
199
+ ctx: ExtensionCommandContext,
200
+ name: string,
201
+ profile: Record<string, unknown>,
202
+ signal?: AbortSignal,
203
+ affectedSetups?: string[],
204
+ ) {
205
+ const url = await promptWebDavUrl(
206
+ ctx,
207
+ signal,
208
+ typeof profile.url === "string" ? profile.url : undefined,
209
+ );
210
+ if (!url) return false;
211
+ const username = await promptUsername(ctx, signal);
212
+ if (!username) return false;
213
+ const hasPassword = typeof profile.password === "string" && profile.password.length > 0;
214
+ const passwordAction = hasPassword
215
+ ? await select(
216
+ ctx,
217
+ "WebDAV password",
218
+ ["Keep current password", "Replace password", "Cancel"],
219
+ signal,
220
+ )
221
+ : "Replace password";
222
+ if (!passwordAction || passwordAction === "Cancel") return false;
223
+ const replacePassword = passwordAction === "Replace password";
224
+ const password = replacePassword
225
+ ? await promptSecret(ctx, "New WebDAV password", { signal })
226
+ : undefined;
227
+ signal?.throwIfAborted();
228
+ if (replacePassword && password === undefined) return false;
229
+ const saved = await saveReviewedDraft(
230
+ ctx,
231
+ "Review storage connection",
232
+ [
233
+ `Storage connection: ${safe(name)}`,
234
+ `URL: ${safe(String(profile.url))} → ${safe(url)}`,
235
+ "Username: stored privately (value hidden)",
236
+ `Password: ${replacePassword ? "will be replaced" : "unchanged"} (value hidden)`,
237
+ `Affected sync setups: ${affectedSetups?.length ? affectedSetups.map(safe).join(", ") : "None"}`,
238
+ "Saving changes future storage access for every affected setup; it does not move remote data.",
239
+ ],
240
+ "Save storage connection",
241
+ (saveSignal) =>
242
+ updateStorageConnection(
243
+ name,
244
+ (current) => {
245
+ if (
246
+ current.type !== "webdav" ||
247
+ current.url !== profile.url ||
248
+ current.credentials.username !== profile.username ||
249
+ current.credentials.password !== profile.password
250
+ )
251
+ throw new Error("Storage connection changed while it was open; reopen it.");
252
+ return {
253
+ ...current,
254
+ url,
255
+ credentials: {
256
+ ...current.credentials,
257
+ username,
258
+ password: password ?? current.credentials.password,
259
+ },
260
+ };
261
+ },
262
+ affectedSetups,
263
+ saveSignal,
264
+ ),
265
+ signal,
266
+ );
267
+ if (saved && !signal?.aborted) ctx.ui.notify(`Saved storage connection “${safe(name)}”.`, "info");
268
+ return saved;
269
+ }
270
+
271
+ async function chooseDestination(
272
+ ctx: ExtensionCommandContext,
273
+ signal?: AbortSignal,
274
+ current = "./",
275
+ ) {
276
+ return promptTextInput(
277
+ ctx,
278
+ "WebDAV storage path\n\nFolder relative to the collection URL, not your local filesystem.\n./ uses the collection root. Use different folders for independent setups.",
279
+ { defaultValue: current, validate: normalizeWebDavPath },
280
+ signal,
281
+ );
282
+ }
283
+
284
+ async function promptUsername(ctx: ExtensionCommandContext, signal?: AbortSignal) {
285
+ return promptTextInput(
286
+ ctx,
287
+ "WebDAV username\n\nEnter the account username; stored values remain hidden.",
288
+ {
289
+ example: "user",
290
+ validate: (value) => {
291
+ validateWebDavCredentials(value);
292
+ return value;
293
+ },
294
+ },
295
+ signal,
296
+ );
297
+ }
298
+
299
+ async function promptWebDavUrl(
300
+ ctx: ExtensionCommandContext,
301
+ signal?: AbortSignal,
302
+ current?: string,
303
+ ) {
304
+ return promptTextInput(
305
+ ctx,
306
+ "WebDAV collection URL\n\nUse the HTTPS WebDAV URL from your provider, not its web login page.",
307
+ {
308
+ ...(current
309
+ ? { defaultValue: current }
310
+ : { example: "https://cloud.example.com/remote.php/dav/files/user" }),
311
+ validate: (value) => {
312
+ const url = normalizeWebDavUrl(value);
313
+ if (!url) throw new Error("WebDAV URL is required.");
314
+ return url;
315
+ },
316
+ },
317
+ signal,
318
+ );
319
+ }
320
+
321
+ async function select(
322
+ ctx: ExtensionCommandContext,
323
+ title: string,
324
+ options: string[],
325
+ signal?: AbortSignal,
326
+ ) {
327
+ const value = await ctx.ui.select(title, options, { signal });
328
+ signal?.throwIfAborted();
329
+ return value;
330
+ }
@@ -1,19 +1,29 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
3
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
- import { isCloudflareR2Endpoint, readLocalConfigObject } from "./config.js";
4
- import { showAddGitStorageProfile, showEditGitStorageProfile } from "./git-ui.js";
5
- import { errorMessage, ownRecord, requiredInput, safeTerminalText } from "./manager-helpers.js";
6
- import {
7
- applyS3CredentialUpdate,
8
- chooseS3Credentials,
9
- chooseS3CredentialUpdate,
10
- } from "./s3-credentials-ui.js";
11
4
  import {
12
5
  addStorageConnection,
13
6
  removeStorageConnection,
14
7
  updateStorageConnection,
15
- } from "./settings-management.js";
16
- import { showAddWebDavStorageProfile, showEditWebDavStorageProfile } from "./webdav-ui.js";
8
+ } from "../settings/settings-management.js";
9
+ import { readLocalConfigObject } from "../settings/settings-store.js";
10
+ import {
11
+ isCloudflareR2Endpoint,
12
+ normalizeS3Endpoint,
13
+ ownRecord,
14
+ } from "../settings/settings-validation.js";
15
+ import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
16
+ import { showAddGitStorageProfile, showEditGitStorageProfile } from "./setup/git-ui.js";
17
+ import {
18
+ applyS3CredentialUpdate,
19
+ chooseS3Credentials,
20
+ chooseS3CredentialUpdate,
21
+ } from "./setup/s3-credentials-ui.js";
22
+ import { promptResourceName } from "./setup/setup-prompts.js";
23
+ import { saveReviewedDraft } from "./setup/setup-review.js";
24
+ import { requiredInput, requiredValueInput } from "./setup/text-input.js";
25
+ import { showAddWebDavStorageProfile, showEditWebDavStorageProfile } from "./setup/webdav-ui.js";
26
+ import { safeTerminalText } from "./terminal-text.js";
17
27
 
18
28
  export async function showStorageConnections(ctx: ExtensionCommandContext, signal?: AbortSignal) {
19
29
  let selectedName: string | undefined;
@@ -35,7 +45,7 @@ export async function showStorageConnections(ctx: ExtensionCommandContext, signa
35
45
  kind: "actions",
36
46
  title: "Storage connections",
37
47
  lines: state.version3
38
- ? []
48
+ ? ["Server addresses and sign-in details, reusable across sync setups."]
39
49
  : ["Create version 3 settings before managing storage connections."],
40
50
  items: state.version3
41
51
  ? [
@@ -46,6 +56,7 @@ export async function showStorageConnections(ctx: ExtensionCommandContext, signa
46
56
  return {
47
57
  id,
48
58
  label: safeTerminalText(name),
59
+ description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
49
60
  action: "select" as const,
50
61
  };
51
62
  }),
@@ -84,10 +95,7 @@ export async function showStorageConnections(ctx: ExtensionCommandContext, signa
84
95
  await showAddStorageConnection(ctx, signal);
85
96
  } catch (error) {
86
97
  if (!signal?.aborted) {
87
- ctx.ui.notify(
88
- `Storage connection was not added: ${safeTerminalText(errorMessage(error))} Retry from Add storage connection.`,
89
- "error",
90
- );
98
+ ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
91
99
  }
92
100
  }
93
101
  return { kind: "stay" };
@@ -165,7 +173,9 @@ async function loadStorageMenuState(selectedName: string | undefined, signal?: A
165
173
  `Credentials: ${credentialSource(profile)}`,
166
174
  `Used by: ${usedBy.length > 0 ? usedBy.map(safeTerminalText).join(", ") : "No sync setups"}`,
167
175
  ...(usedBy.length > 0
168
- ? ["Remove unavailable: edit or remove the listed sync setups first."]
176
+ ? [
177
+ "Remove unavailable: remove the listed sync setups first. Switch away from a current setup before removing it if other setups remain.",
178
+ ]
169
179
  : []),
170
180
  ],
171
181
  },
@@ -180,7 +190,7 @@ function notifyConnectionError(
180
190
  ) {
181
191
  if (signal?.aborted) return;
182
192
  ctx.ui.notify(
183
- `Storage connection “${safeTerminalText(name)}” was not changed: ${safeTerminalText(errorMessage(error))} Reopen it and retry.`,
193
+ `Storage connection “${safeTerminalText(name)}” was not changed: ${syncErrorGuidance(error)}`,
184
194
  "error",
185
195
  );
186
196
  }
@@ -218,9 +228,15 @@ async function editStorageConnection(
218
228
  "Endpoint",
219
229
  String(profile.endpoint ?? "https://s3.example.com"),
220
230
  signal,
231
+ normalizeS3Endpoint,
221
232
  );
222
233
  if (!endpoint || signal?.aborted) return;
223
- const region = await requiredInput(ctx, "Region", String(profile.region ?? "auto"), signal);
234
+ const region = await requiredInput(
235
+ ctx,
236
+ "Region\n\nUse your bucket's region; Cloudflare R2 uses auto.",
237
+ String(profile.region ?? "auto"),
238
+ signal,
239
+ );
224
240
  if (!region || signal?.aborted) return;
225
241
  const storedCredentials = ownRecord(profile.credentials) ?? {};
226
242
  const credentials = await chooseS3CredentialUpdate(
@@ -229,41 +245,41 @@ async function editStorageConnection(
229
245
  signal,
230
246
  );
231
247
  if (!credentials || signal?.aborted) return;
232
- const save = await ctx.ui.select(
248
+ const saved = await saveReviewedDraft(
249
+ ctx,
250
+ "Review storage connection",
233
251
  [
234
- "Review storage connection",
235
- "",
236
252
  `Storage connection: ${safeTerminalText(name)}`,
237
253
  `Endpoint: ${safeTerminalText(String(profile.endpoint ?? "missing"))} → ${safeTerminalText(endpoint)}`,
238
254
  `Region: ${safeTerminalText(String(profile.region ?? "auto"))} → ${safeTerminalText(region)}`,
239
255
  `Credentials: ${safeTerminalText(credentials.summary)}`,
240
256
  `Affected sync setups: ${usedBy.length > 0 ? usedBy.map(safeTerminalText).join(", ") : "None"}`,
241
257
  "Saving changes future storage access for every affected setup; it does not move remote data.",
242
- ].join("\n"),
243
- ["Save storage connection", "Cancel"],
244
- { signal },
245
- );
246
- if (save !== "Save storage connection" || signal?.aborted) return;
247
- await updateStorageConnection(
248
- name,
249
- (current) => {
250
- if (current.type !== "s3") {
251
- throw new Error("Storage connection type changed; reopen it.");
252
- }
253
- return {
254
- ...current,
255
- endpoint,
256
- region,
257
- credentials: applyS3CredentialUpdate(
258
- current.credentials,
259
- credentials,
260
- ) as typeof current.credentials,
261
- };
262
- },
263
- usedBy,
258
+ ],
259
+ "Save storage connection",
260
+ (saveSignal) =>
261
+ updateStorageConnection(
262
+ name,
263
+ (current) => {
264
+ if (current.type !== "s3" || !isDeepStrictEqual(current, profile)) {
265
+ throw new Error("Storage connection changed while it was open; reopen it.");
266
+ }
267
+ return {
268
+ ...current,
269
+ endpoint,
270
+ region,
271
+ credentials: applyS3CredentialUpdate(
272
+ current.credentials,
273
+ credentials,
274
+ ) as typeof current.credentials,
275
+ };
276
+ },
277
+ usedBy,
278
+ saveSignal,
279
+ ),
264
280
  signal,
265
281
  );
266
- if (signal?.aborted) return;
282
+ if (!saved || signal?.aborted) return;
267
283
  ctx.ui.notify(`Saved storage connection “${safeTerminalText(name)}”.`, "info");
268
284
  }
269
285
 
@@ -283,55 +299,64 @@ export async function showAddStorageConnection(ctx: ExtensionCommandContext, sig
283
299
  if (signal?.aborted || !preset || preset === "Cancel") return false;
284
300
  if (preset === "WebDAV") return showAddWebDavStorageProfile(ctx, signal);
285
301
  if (preset === "Git") return showAddGitStorageProfile(ctx, signal);
286
- const name = await requiredInput(
302
+ const name = await promptResourceName(
287
303
  ctx,
288
- "Name this storage connection",
304
+ "storage connection",
289
305
  preset === "Cloudflare R2" ? "r2" : "s3",
290
306
  signal,
291
307
  );
292
308
  if (!name || signal?.aborted) return false;
293
- const endpoint = await requiredInput(
309
+ const endpoint = await requiredValueInput(
294
310
  ctx,
295
- "Endpoint",
311
+ "S3 API endpoint\n\nUse the API URL from your storage provider, not its web console.",
296
312
  preset === "Cloudflare R2"
297
313
  ? "https://<account-id>.r2.cloudflarestorage.com"
298
314
  : "https://s3.example.com",
299
315
  signal,
316
+ normalizeS3Endpoint,
300
317
  );
301
318
  if (!endpoint || signal?.aborted) return false;
302
319
  const region =
303
- preset === "Cloudflare R2" ? "auto" : await requiredInput(ctx, "Region", "us-east-1", signal);
320
+ preset === "Cloudflare R2"
321
+ ? "auto"
322
+ : await requiredInput(
323
+ ctx,
324
+ "Region\n\nUse the region assigned to your bucket by the provider.",
325
+ "us-east-1",
326
+ signal,
327
+ );
304
328
  if (!region || signal?.aborted) return false;
305
329
  const credentials = await chooseS3Credentials(ctx, signal);
306
330
  if (!credentials || signal?.aborted) return false;
307
- const save = await ctx.ui.select(
331
+ const saved = await saveReviewedDraft(
332
+ ctx,
333
+ "Review storage connection",
308
334
  [
309
- "Review storage connection",
310
- "",
311
335
  `Name: ${safeTerminalText(name)}`,
312
336
  `Type: ${preset}`,
313
337
  `Endpoint: ${safeTerminalText(endpoint)}`,
314
338
  `Region: ${safeTerminalText(region)}`,
315
339
  `Credentials: ${safeTerminalText(credentials.summary)}`,
316
340
  "Adding a connection does not contact remote storage or start syncing.",
317
- ].join("\n"),
318
- ["Add storage connection", "Cancel"],
319
- { signal },
320
- );
321
- if (save !== "Add storage connection" || signal?.aborted) return false;
322
- await addStorageConnection(
323
- name,
324
- {
325
- type: "s3",
326
- endpoint,
327
- region,
328
- credentials: {
329
- accessKeyId: credentials.profileFields.accessKeyId ?? "",
330
- secretAccessKey: credentials.profileFields.secretAccessKey ?? "",
331
- },
332
- },
341
+ ],
342
+ "Add storage connection",
343
+ (saveSignal) =>
344
+ addStorageConnection(
345
+ name,
346
+ {
347
+ type: "s3",
348
+ endpoint,
349
+ region,
350
+ credentials: {
351
+ accessKeyId: credentials.profileFields.accessKeyId ?? "",
352
+ secretAccessKey: credentials.profileFields.secretAccessKey ?? "",
353
+ },
354
+ },
355
+ saveSignal,
356
+ ),
333
357
  signal,
334
358
  );
359
+ if (!saved) return false;
335
360
  if (signal?.aborted) return true;
336
361
  ctx.ui.notify(`Added storage connection “${safeTerminalText(name)}”.`, "info");
337
362
  return true;
@@ -344,6 +369,10 @@ function referencingSetups(raw: Record<string, unknown> | undefined, connection:
344
369
  .sort((left, right) => left.localeCompare(right));
345
370
  }
346
371
 
372
+ export function connectionSummary(profile: Record<string, unknown>) {
373
+ return `${connectionType(profile)} · ${connectionEndpoint(profile)}`;
374
+ }
375
+
347
376
  function connectionType(profile: Record<string, unknown>) {
348
377
  if (profile.type === "git") return "Git";
349
378
  if (profile.type === "webdav") return "WebDAV";
@@ -367,7 +396,8 @@ function connectionEndpoint(profile: Record<string, unknown>) {
367
396
  if (typeof value !== "string" || value.length === 0) return "Missing";
368
397
  if (profile.type === "git") return safeTerminalText(value);
369
398
  try {
370
- return safeTerminalText(new URL(value).host);
399
+ const url = new URL(value);
400
+ return safeTerminalText(`${url.origin}${url.pathname}`);
371
401
  } catch {
372
402
  return "Invalid";
373
403
  }