@narumitw/pi-sync 0.50.3 → 0.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +4 -1
  2. package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
  3. package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
  4. package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
  5. package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
  6. package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
  7. package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
  8. package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
  9. package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
  10. package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
  11. package/dist/chunks/chunk-6546TS54.ts.map +7 -0
  12. package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
  13. package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
  14. package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
  15. package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
  16. package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
  17. package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
  18. package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
  19. package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
  20. package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
  21. package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
  22. package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
  23. package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
  24. package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
  25. package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
  26. package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
  27. package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
  28. package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
  29. package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
  30. package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
  31. package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
  32. package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
  33. package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
  34. package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
  35. package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
  36. package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
  37. package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
  38. package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
  39. package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
  40. package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
  41. package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
  42. package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
  43. package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
  44. package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
  45. package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
  46. package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
  47. package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
  48. package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
  49. package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
  50. package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
  51. package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
  52. package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
  53. package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
  54. package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
  55. package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
  56. package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
  57. package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
  58. package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
  59. package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
  60. package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
  61. package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
  62. package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
  63. package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
  64. package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
  65. package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
  66. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
  67. package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
  68. package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
  69. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
  70. package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
  71. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
  72. package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
  73. package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
  74. package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
  75. package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
  76. package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
  77. package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
  78. package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
  79. package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
  80. package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
  81. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
  82. package/dist/index.ts +49 -74
  83. package/dist/index.ts.map +3 -3
  84. package/docs/settings.md +9 -0
  85. package/package.json +61 -62
  86. package/src/backends/backend-factory.ts +15 -15
  87. package/src/backends/backend-identity.ts +31 -31
  88. package/src/backends/backend-types.ts +42 -42
  89. package/src/backends/git/git-backend.ts +714 -788
  90. package/src/backends/git/git-config.ts +104 -111
  91. package/src/backends/git/git-runner.ts +230 -247
  92. package/src/backends/git/git-storage.ts +228 -245
  93. package/src/backends/s3/s3-backend.ts +408 -441
  94. package/src/backends/s3/s3-client.ts +288 -333
  95. package/src/backends/sync-backend.ts +65 -65
  96. package/src/backends/webdav/webdav-backend.ts +542 -589
  97. package/src/backends/webdav/webdav-client.ts +292 -316
  98. package/src/backends/webdav/webdav-config.ts +57 -68
  99. package/src/commands/command-execution.ts +270 -289
  100. package/src/commands/command-handler.ts +160 -163
  101. package/src/commands/command-types.ts +13 -13
  102. package/src/commands/command.ts +190 -196
  103. package/src/commands/setup-completions.ts +1 -1
  104. package/src/paths.ts +47 -47
  105. package/src/settings/config-errors.ts +4 -5
  106. package/src/settings/config-file.ts +473 -506
  107. package/src/settings/config.ts +171 -174
  108. package/src/settings/settings-management.ts +191 -216
  109. package/src/settings/settings-store.ts +63 -77
  110. package/src/settings/settings-types.ts +78 -75
  111. package/src/settings/settings-validation.ts +328 -364
  112. package/src/snapshot/session-paths.ts +41 -55
  113. package/src/snapshot/snapshot-apply.ts +197 -229
  114. package/src/snapshot/snapshot-codec.ts +29 -31
  115. package/src/snapshot/snapshot-paths.ts +4 -6
  116. package/src/snapshot/snapshot-transaction.ts +161 -170
  117. package/src/snapshot/snapshot-types.ts +24 -24
  118. package/src/snapshot/snapshot.ts +310 -343
  119. package/src/state/json-file.ts +11 -11
  120. package/src/state/lock.ts +226 -242
  121. package/src/state/lockfile-fs.ts +20 -20
  122. package/src/state/operation-availability.ts +36 -41
  123. package/src/state/state-directory.ts +171 -173
  124. package/src/state/state-types.ts +15 -15
  125. package/src/state/sync-state-store.ts +23 -23
  126. package/src/sync/automatic-sync.ts +75 -80
  127. package/src/sync/remote-snapshot.ts +52 -60
  128. package/src/sync/setup-switch.ts +99 -114
  129. package/src/sync/signals.ts +3 -5
  130. package/src/sync/startup-check.ts +97 -111
  131. package/src/sync/sync-decision.ts +76 -80
  132. package/src/sync/sync-error-guidance.ts +41 -48
  133. package/src/sync/sync-errors.ts +10 -10
  134. package/src/sync/sync-inspection.ts +29 -33
  135. package/src/sync/sync-loaders.ts +28 -36
  136. package/src/sync/sync-mutations.ts +510 -622
  137. package/src/sync/sync-operations.ts +7 -7
  138. package/src/sync/sync-policy.ts +296 -326
  139. package/src/sync/sync-queries.ts +175 -194
  140. package/src/sync/sync-state.ts +74 -100
  141. package/src/sync-extension.ts +89 -101
  142. package/src/sync.ts +26 -26
  143. package/src/types.ts +37 -37
  144. package/src/ui/automatic-sync-summary.ts +1 -3
  145. package/src/ui/cancellable-operation.ts +89 -101
  146. package/src/ui/file-selection.ts +250 -274
  147. package/src/ui/manager-attention.ts +51 -63
  148. package/src/ui/manager-recovery.ts +48 -57
  149. package/src/ui/manager-result-dispatcher.ts +62 -76
  150. package/src/ui/manager-state.ts +200 -232
  151. package/src/ui/manager-ui.ts +264 -362
  152. package/src/ui/remote-selection-ui.ts +547 -588
  153. package/src/ui/secret-input.ts +197 -205
  154. package/src/ui/settings-ui.ts +175 -172
  155. package/src/ui/setup/git-ui.ts +232 -261
  156. package/src/ui/setup/s3-credentials-ui.ts +58 -65
  157. package/src/ui/setup/s3-ui.ts +171 -195
  158. package/src/ui/setup/setup-actions.ts +91 -101
  159. package/src/ui/setup/setup-location-ui.ts +121 -137
  160. package/src/ui/setup/setup-name-ui.ts +27 -27
  161. package/src/ui/setup/setup-prompts.ts +65 -65
  162. package/src/ui/setup/setup-review.ts +58 -59
  163. package/src/ui/setup/setup-switcher.ts +156 -160
  164. package/src/ui/setup/setup-wizard.ts +28 -28
  165. package/src/ui/setup/text-input.ts +54 -58
  166. package/src/ui/setup/webdav-ui.ts +246 -293
  167. package/src/ui/storage-connections-ui.ts +317 -369
  168. package/src/ui/sync-attention.ts +148 -164
  169. package/src/ui/sync-format.ts +110 -121
  170. package/src/ui/sync-resolution-ui.ts +147 -153
  171. package/src/ui/sync-setups-ui.ts +218 -238
  172. package/src/ui/sync-status.ts +17 -0
  173. package/src/ui/terminal-text.ts +2 -2
  174. package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
  175. package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
  176. package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
  177. package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
  178. package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
  179. package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
  180. package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
  181. package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
  182. package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
  183. package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
  184. package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
  185. package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
  186. package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
  187. package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
  188. package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
  189. package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
  190. package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
  191. package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
  192. package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
  193. package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
  194. package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
  195. package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
  196. package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
  197. package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
  198. package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
  199. package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
  200. package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
  201. package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
  202. package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
  203. package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
  204. package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
  205. package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
  206. package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
  207. package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
  208. package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
  209. package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
  210. package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
  211. package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
  212. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
  213. /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
  214. /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
  215. /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
  216. /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
  217. /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
  218. /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
@@ -14,114 +14,104 @@ import { promptResourceName } from "./setup-prompts.js";
14
14
  import { showSetupSwitcher } from "./setup-switcher.js";
15
15
  import { showAddWebDavTarget, showEditWebDavTarget } from "./webdav-ui.js";
16
16
 
17
- export async function showSyncSetupManager(
18
- ctx: ExtensionCommandContext,
19
- runRoute: RunRoute,
20
- signal?: AbortSignal,
21
- ) {
22
- return showSyncSetups(
23
- ctx,
24
- {
25
- add: async (setupSignal) => {
26
- await showAddTarget(ctx, setupSignal);
27
- },
28
- edit: async (name, setupSignal) => {
29
- await showEditTarget(ctx, name, setupSignal);
30
- },
31
- makeCurrent: async (name, setupSignal) => {
32
- const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
33
- return result === "pull-attempted" || result === "closed" ? "exit" : undefined;
34
- },
35
- remove: async (name, setupSignal) => {
36
- await showRemoveTarget(ctx, name, setupSignal);
37
- },
38
- },
39
- signal,
40
- );
17
+ export async function showSyncSetupManager(ctx: ExtensionCommandContext, runRoute: RunRoute, signal?: AbortSignal) {
18
+ return showSyncSetups(
19
+ ctx,
20
+ {
21
+ add: async (setupSignal) => {
22
+ await showAddTarget(ctx, setupSignal);
23
+ },
24
+ edit: async (name, setupSignal) => {
25
+ await showEditTarget(ctx, name, setupSignal);
26
+ },
27
+ makeCurrent: async (name, setupSignal) => {
28
+ const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
29
+ return result === "pull-attempted" || result === "closed" ? "exit" : undefined;
30
+ },
31
+ remove: async (name, setupSignal) => {
32
+ await showRemoveTarget(ctx, name, setupSignal);
33
+ },
34
+ },
35
+ signal,
36
+ );
41
37
  }
42
38
 
43
39
  async function showAddTarget(ctx: ExtensionCommandContext, signal?: AbortSignal) {
44
- let raw = await readLocalConfigObject();
45
- if (signal?.aborted) return;
46
- if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
47
- if (raw.version !== 3) {
48
- ctx.ui.notify(
49
- "Version 1 and version 2 settings are unsupported and are never migrated.",
50
- "error",
51
- );
52
- return;
53
- }
54
- let profiles = ownRecord(raw.storageConnections) ?? {};
55
- const name = await promptResourceName(ctx, "sync setup", "work", signal);
56
- if (!name) return;
57
- const createConnection = "Add a new storage connection…";
58
- let profile = await ctx.ui.select(
59
- "Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
60
- [...Object.keys(profiles).sort(), createConnection, "Cancel"],
61
- { signal },
62
- );
63
- if (signal?.aborted || !profile || profile === "Cancel") return;
64
- if (profile === createConnection) {
65
- const previousNames = new Set(Object.keys(profiles));
66
- if (!(await showAddStorageConnection(ctx, signal))) return;
67
- if (signal?.aborted) return;
68
- raw = (await readLocalConfigObject()) ?? raw;
69
- if (signal?.aborted) return;
70
- profiles = ownRecord(raw.storageConnections) ?? {};
71
- profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
72
- if (!profile) return;
73
- ctx.ui.notify(
74
- `Connection “${safeTerminalText(profile)}” saved. Cancelling setup will keep this connection.`,
75
- "info",
76
- );
77
- }
78
- const storageKind = ownRecord(profiles[profile])?.type;
79
- if (storageKind === "webdav") {
80
- const saved = await showAddWebDavTarget(ctx, name, profile, signal);
81
- if (signal?.aborted) return;
82
- if (saved) await refreshTargetCompletions();
83
- return;
84
- }
85
- if (storageKind === "git") {
86
- const saved = await showAddGitTarget(ctx, name, profile, signal);
87
- if (signal?.aborted) return;
88
- if (saved) await refreshTargetCompletions();
89
- return;
90
- }
91
- await showAddS3Target(ctx, raw, profile, name, signal);
40
+ let raw = await readLocalConfigObject();
41
+ if (signal?.aborted) return;
42
+ if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
43
+ if (raw.version !== 3) {
44
+ ctx.ui.notify("Version 1 and version 2 settings are unsupported and are never migrated.", "error");
45
+ return;
46
+ }
47
+ let profiles = ownRecord(raw.storageConnections) ?? {};
48
+ const name = await promptResourceName(ctx, "sync setup", "work", signal);
49
+ if (!name) return;
50
+ const createConnection = "Add a new storage connection…";
51
+ let profile = await ctx.ui.select(
52
+ "Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
53
+ [...Object.keys(profiles).sort(), createConnection, "Cancel"],
54
+ { signal },
55
+ );
56
+ if (signal?.aborted || !profile || profile === "Cancel") return;
57
+ if (profile === createConnection) {
58
+ const previousNames = new Set(Object.keys(profiles));
59
+ if (!(await showAddStorageConnection(ctx, signal))) return;
60
+ if (signal?.aborted) return;
61
+ raw = (await readLocalConfigObject()) ?? raw;
62
+ if (signal?.aborted) return;
63
+ profiles = ownRecord(raw.storageConnections) ?? {};
64
+ profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
65
+ if (!profile) return;
66
+ ctx.ui.notify(
67
+ `Connection “${safeTerminalText(profile)}” saved. Cancelling setup will keep this connection.`,
68
+ "info",
69
+ );
70
+ }
71
+ const storageKind = ownRecord(profiles[profile])?.type;
72
+ if (storageKind === "webdav") {
73
+ const saved = await showAddWebDavTarget(ctx, name, profile, signal);
74
+ if (signal?.aborted) return;
75
+ if (saved) await refreshTargetCompletions();
76
+ return;
77
+ }
78
+ if (storageKind === "git") {
79
+ const saved = await showAddGitTarget(ctx, name, profile, signal);
80
+ if (signal?.aborted) return;
81
+ if (saved) await refreshTargetCompletions();
82
+ return;
83
+ }
84
+ await showAddS3Target(ctx, raw, profile, name, signal);
92
85
  }
93
86
 
94
87
  async function showEditTarget(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
95
- const partial = await loadPartialConfig(name);
96
- if (signal?.aborted) return;
97
- if (!partial.setupName) {
98
- ctx.ui.notify("Create version 3 settings before editing a named sync setup.", "info");
99
- return;
100
- }
101
- if (partial.storageKind === "webdav") {
102
- await showEditWebDavTarget(ctx, partial, signal);
103
- return;
104
- }
105
- if (partial.storageKind === "git") {
106
- await showEditGitTarget(ctx, partial, signal);
107
- return;
108
- }
109
- await showEditS3Target(ctx, partial, signal);
88
+ const partial = await loadPartialConfig(name);
89
+ if (signal?.aborted) return;
90
+ if (!partial.setupName) {
91
+ ctx.ui.notify("Create version 3 settings before editing a named sync setup.", "info");
92
+ return;
93
+ }
94
+ if (partial.storageKind === "webdav") {
95
+ await showEditWebDavTarget(ctx, partial, signal);
96
+ return;
97
+ }
98
+ if (partial.storageKind === "git") {
99
+ await showEditGitTarget(ctx, partial, signal);
100
+ return;
101
+ }
102
+ await showEditS3Target(ctx, partial, signal);
110
103
  }
111
104
 
112
105
  async function showRemoveTarget(ctx: ExtensionCommandContext, name: string, signal?: AbortSignal) {
113
- const confirmed = await ctx.ui.confirm(
114
- "Remove sync setup?",
115
- `Remove local sync setup “${safeTerminalText(name)}”? Remote data and history are not deleted.`,
116
- { signal },
117
- );
118
- if (signal?.aborted || !confirmed) return;
119
- await removeSyncSetup(name, signal);
120
- if (signal?.aborted) return;
121
- await refreshTargetCompletions();
122
- if (signal?.aborted) return;
123
- ctx.ui.notify(
124
- `Removed sync setup “${safeTerminalText(name)}”; remote data was not deleted.`,
125
- "info",
126
- );
106
+ const confirmed = await ctx.ui.confirm(
107
+ "Remove sync setup?",
108
+ `Remove local sync setup “${safeTerminalText(name)}”? Remote data and history are not deleted.`,
109
+ { signal },
110
+ );
111
+ if (signal?.aborted || !confirmed) return;
112
+ await removeSyncSetup(name, signal);
113
+ if (signal?.aborted) return;
114
+ await refreshTargetCompletions();
115
+ if (signal?.aborted) return;
116
+ ctx.ui.notify(`Removed sync setup “${safeTerminalText(name)}”; remote data was not deleted.`, "info");
127
117
  }
@@ -1,167 +1,151 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { normalizeGitBranch } from "../../backends/git/git-config.js";
3
3
  import { readLocalConfigObject } from "../../settings/settings-store.js";
4
- import type {
5
- StorageConnectionSettings,
6
- SyncSetupSettings,
7
- } from "../../settings/settings-types.js";
4
+ import type { StorageConnectionSettings, SyncSetupSettings } from "../../settings/settings-types.js";
8
5
  import {
9
- effectiveSyncSetupRemoteIdentity,
10
- normalizeStoragePath,
11
- ownRecord,
6
+ effectiveSyncSetupRemoteIdentity,
7
+ normalizeStoragePath,
8
+ ownRecord,
12
9
  } from "../../settings/settings-validation.js";
13
10
  import { errorMessage } from "../../sync/sync-errors.js";
14
11
  import { safeTerminalText } from "../terminal-text.js";
15
- import {
16
- promptTextInput,
17
- requiredExistingBucket,
18
- requiredInput,
19
- requiredValueInput,
20
- } from "./text-input.js";
12
+ import { promptTextInput, requiredExistingBucket, requiredInput, requiredValueInput } from "./text-input.js";
21
13
 
22
14
  interface ChosenRemoteLocation {
23
- connectionName: string;
24
- bucket: string;
25
- path: string;
15
+ connectionName: string;
16
+ bucket: string;
17
+ path: string;
26
18
  }
27
19
 
28
20
  export async function chooseInitialRemoteLocation(
29
- ctx: ExtensionCommandContext,
30
- _preset: string,
31
- setupName: string,
32
- signal?: AbortSignal,
21
+ ctx: ExtensionCommandContext,
22
+ _preset: string,
23
+ setupName: string,
24
+ signal?: AbortSignal,
33
25
  ): Promise<ChosenRemoteLocation | undefined> {
34
- const choice = await ctx.ui.select(
35
- "Choose storage location\n\nUse an existing bucket. ./ stores snapshots at the bucket root; a custom path keeps them in a separate folder.",
36
- ["Use an existing bucket at ./", "Customize remote location", "Cancel"],
37
- { signal },
38
- );
39
- if (signal?.aborted || !choice || choice === "Cancel") return undefined;
40
- if (choice === "Customize remote location")
41
- return chooseCustomRemoteLocation(ctx, setupName, signal);
42
- const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
43
- return bucket ? { connectionName: setupName, bucket, path: "./" } : undefined;
26
+ const choice = await ctx.ui.select(
27
+ "Choose storage location\n\nUse an existing bucket. ./ stores snapshots at the bucket root; a custom path keeps them in a separate folder.",
28
+ ["Use an existing bucket at ./", "Customize remote location", "Cancel"],
29
+ { signal },
30
+ );
31
+ if (signal?.aborted || !choice || choice === "Cancel") return undefined;
32
+ if (choice === "Customize remote location") return chooseCustomRemoteLocation(ctx, setupName, signal);
33
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
34
+ return bucket ? { connectionName: setupName, bucket, path: "./" } : undefined;
44
35
  }
45
36
 
46
37
  export async function chooseAdditionalRemoteLocation(
47
- ctx: ExtensionCommandContext,
48
- settings: Record<string, unknown>,
49
- connectionName: string,
50
- setupName: string,
51
- signal?: AbortSignal,
38
+ ctx: ExtensionCommandContext,
39
+ settings: Record<string, unknown>,
40
+ connectionName: string,
41
+ setupName: string,
42
+ signal?: AbortSignal,
52
43
  ): Promise<Omit<ChosenRemoteLocation, "connectionName"> | undefined> {
53
- const setups = ownRecord(settings.syncSetups) ?? {};
54
- const currentSetup =
55
- typeof settings.activeSyncSetup === "string" ? settings.activeSyncSetup : undefined;
56
- const candidates = Object.entries(setups)
57
- .map(([name, value]) => ({ name, storage: ownRecord(ownRecord(value)?.storage) }))
58
- .filter(
59
- (item): item is { name: string; storage: Record<string, unknown> } =>
60
- item.storage?.connection === connectionName && typeof item.storage.bucket === "string",
61
- );
62
- const source =
63
- candidates.find((item) => item.name === currentSetup) ??
64
- candidates.sort((left, right) => left.name.localeCompare(right.name))[0];
65
- if (source) {
66
- const suggestedPath = "./";
67
- const sameBucketLabel = `Same bucket as “${safeTerminalText(source.name)}”`;
68
- const choice = await ctx.ui.select(
69
- [
70
- `Storage location for “${safeTerminalText(setupName)}”`,
71
- "",
72
- `Existing bucket: ${safeTerminalText(String(source.storage.bucket))}`,
73
- `Remote path: ${safeTerminalText(suggestedPath)}`,
74
- "./ uses the bucket root. Use a different path or bucket for independent setups.",
75
- ].join("\n"),
76
- [sameBucketLabel, "Use a different bucket", "Customize remote location", "Cancel"],
77
- { signal },
78
- );
79
- if (signal?.aborted || !choice || choice === "Cancel") return undefined;
80
- if (choice === sameBucketLabel) {
81
- return { bucket: String(source.storage.bucket), path: suggestedPath };
82
- }
83
- if (choice === "Use a different bucket") {
84
- const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
85
- return bucket ? { bucket, path: "./" } : undefined;
86
- }
87
- const custom = await chooseCustomRemoteLocation(ctx, connectionName, signal);
88
- return custom ? { bucket: custom.bucket, path: custom.path } : undefined;
89
- }
44
+ const setups = ownRecord(settings.syncSetups) ?? {};
45
+ const currentSetup = typeof settings.activeSyncSetup === "string" ? settings.activeSyncSetup : undefined;
46
+ const candidates = Object.entries(setups)
47
+ .map(([name, value]) => ({ name, storage: ownRecord(ownRecord(value)?.storage) }))
48
+ .filter(
49
+ (item): item is { name: string; storage: Record<string, unknown> } =>
50
+ item.storage?.connection === connectionName && typeof item.storage.bucket === "string",
51
+ );
52
+ const source =
53
+ candidates.find((item) => item.name === currentSetup) ??
54
+ candidates.sort((left, right) => left.name.localeCompare(right.name))[0];
55
+ if (source) {
56
+ const suggestedPath = "./";
57
+ const sameBucketLabel = `Same bucket as “${safeTerminalText(source.name)}”`;
58
+ const choice = await ctx.ui.select(
59
+ [
60
+ `Storage location for “${safeTerminalText(setupName)}”`,
61
+ "",
62
+ `Existing bucket: ${safeTerminalText(String(source.storage.bucket))}`,
63
+ `Remote path: ${safeTerminalText(suggestedPath)}`,
64
+ "./ uses the bucket root. Use a different path or bucket for independent setups.",
65
+ ].join("\n"),
66
+ [sameBucketLabel, "Use a different bucket", "Customize remote location", "Cancel"],
67
+ { signal },
68
+ );
69
+ if (signal?.aborted || !choice || choice === "Cancel") return undefined;
70
+ if (choice === sameBucketLabel) {
71
+ return { bucket: String(source.storage.bucket), path: suggestedPath };
72
+ }
73
+ if (choice === "Use a different bucket") {
74
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
75
+ return bucket ? { bucket, path: "./" } : undefined;
76
+ }
77
+ const custom = await chooseCustomRemoteLocation(ctx, connectionName, signal);
78
+ return custom ? { bucket: custom.bucket, path: custom.path } : undefined;
79
+ }
90
80
 
91
- const location = await chooseInitialRemoteLocation(ctx, "S3", setupName, signal);
92
- return location ? { bucket: location.bucket, path: location.path } : undefined;
81
+ const location = await chooseInitialRemoteLocation(ctx, "S3", setupName, signal);
82
+ return location ? { bucket: location.bucket, path: location.path } : undefined;
93
83
  }
94
84
 
95
85
  async function chooseCustomRemoteLocation(
96
- ctx: ExtensionCommandContext,
97
- connectionName: string,
98
- signal?: AbortSignal,
86
+ ctx: ExtensionCommandContext,
87
+ connectionName: string,
88
+ signal?: AbortSignal,
99
89
  ): Promise<ChosenRemoteLocation | undefined> {
100
- const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
101
- if (!bucket) return undefined;
102
- const storagePath = await requiredInput(
103
- ctx,
104
- "Storage path\n\nPath inside the bucket, not your local filesystem.\n./ uses the bucket root. Use different paths for independent setups.",
105
- "./",
106
- signal,
107
- normalizeStoragePath,
108
- );
109
- if (!storagePath) return undefined;
110
- return { connectionName, bucket, path: normalizeStoragePath(storagePath) };
90
+ const bucket = await requiredExistingBucket(ctx, "pi-sync", signal);
91
+ if (!bucket) return undefined;
92
+ const storagePath = await requiredInput(
93
+ ctx,
94
+ "Storage path\n\nPath inside the bucket, not your local filesystem.\n./ uses the bucket root. Use different paths for independent setups.",
95
+ "./",
96
+ signal,
97
+ normalizeStoragePath,
98
+ );
99
+ if (!storagePath) return undefined;
100
+ return { connectionName, bucket, path: normalizeStoragePath(storagePath) };
111
101
  }
112
102
 
113
103
  /** Early UI check only; the settings writer still validates uniqueness under its lock. */
114
104
  export async function promptAvailableSetupStorage<T extends SyncSetupSettings["storage"]>(
115
- ctx: ExtensionCommandContext,
116
- initial: T,
117
- signal?: AbortSignal,
105
+ ctx: ExtensionCommandContext,
106
+ initial: T,
107
+ signal?: AbortSignal,
118
108
  ): Promise<T | undefined> {
119
- let storage = initial;
120
- while (!signal?.aborted) {
121
- const settings = await readLocalConfigObject();
122
- if (signal?.aborted) return undefined;
123
- const connection = settings?.storageConnections[storage.connection];
124
- if (!settings || !connection) throw new Error("Storage connection changed; reopen setup.");
125
- const identity = setupPublicationIdentity(storage, connection);
126
- const occupied = Object.entries(settings.syncSetups).find(
127
- ([, setup]) =>
128
- setupPublicationIdentity(
129
- setup.storage,
130
- settings.storageConnections[setup.storage.connection],
131
- ) === identity,
132
- );
133
- if (!occupied) return storage;
134
- const git = connection.type === "git";
135
- const message = `${git ? "Git branch" : "Storage location"} is already used by “${safeTerminalText(occupied[0])}”. ${git ? "Use a different branch; pi-sync owns the entire branch." : "Enter a different storage path, or cancel to choose another connection or bucket."}`;
136
- ctx.ui.notify(message, "warning");
137
- const title = `${git ? "Git branch for the new setup" : "Storage path for the new setup"}\n\n${message}`;
138
- const example = git ? "pi-sync/work" : "backups/work";
139
- const value =
140
- connection.type === "webdav"
141
- ? await promptTextInput(ctx, title, { example }, signal)
142
- : await requiredValueInput(ctx, title, example, signal);
143
- if (signal?.aborted || value === undefined) return undefined;
144
- try {
145
- storage = git
146
- ? { ...storage, branch: normalizeGitBranch(value) }
147
- : { ...storage, path: normalizeStoragePath(value) };
148
- } catch (error) {
149
- ctx.ui.notify(safeTerminalText(errorMessage(error)), "warning");
150
- }
151
- }
152
- return undefined;
109
+ let storage = initial;
110
+ while (!signal?.aborted) {
111
+ const settings = await readLocalConfigObject();
112
+ if (signal?.aborted) return undefined;
113
+ const connection = settings?.storageConnections[storage.connection];
114
+ if (!settings || !connection) throw new Error("Storage connection changed; reopen setup.");
115
+ const identity = setupPublicationIdentity(storage, connection);
116
+ const occupied = Object.entries(settings.syncSetups).find(
117
+ ([, setup]) =>
118
+ setupPublicationIdentity(setup.storage, settings.storageConnections[setup.storage.connection]) === identity,
119
+ );
120
+ if (!occupied) return storage;
121
+ const git = connection.type === "git";
122
+ const message = `${git ? "Git branch" : "Storage location"} is already used by “${safeTerminalText(occupied[0])}”. ${git ? "Use a different branch; pi-sync owns the entire branch." : "Enter a different storage path, or cancel to choose another connection or bucket."}`;
123
+ ctx.ui.notify(message, "warning");
124
+ const title = `${git ? "Git branch for the new setup" : "Storage path for the new setup"}\n\n${message}`;
125
+ const example = git ? "pi-sync/work" : "backups/work";
126
+ const value =
127
+ connection.type === "webdav"
128
+ ? await promptTextInput(ctx, title, { example }, signal)
129
+ : await requiredValueInput(ctx, title, example, signal);
130
+ if (signal?.aborted || value === undefined) return undefined;
131
+ try {
132
+ storage = git
133
+ ? { ...storage, branch: normalizeGitBranch(value) }
134
+ : { ...storage, path: normalizeStoragePath(value) };
135
+ } catch (error) {
136
+ ctx.ui.notify(safeTerminalText(errorMessage(error)), "warning");
137
+ }
138
+ }
139
+ return undefined;
153
140
  }
154
141
 
155
- function setupPublicationIdentity(
156
- storage: SyncSetupSettings["storage"],
157
- connection: StorageConnectionSettings,
158
- ) {
159
- // Git publications own a complete branch tree; changing only its directory is not isolation.
160
- return effectiveSyncSetupRemoteIdentity(
161
- {
162
- storage: connection.type === "git" ? { ...storage, path: "./" } : storage,
163
- sync: { include: [], automatic: false },
164
- },
165
- connection,
166
- );
142
+ function setupPublicationIdentity(storage: SyncSetupSettings["storage"], connection: StorageConnectionSettings) {
143
+ // Git publications own a complete branch tree; changing only its directory is not isolation.
144
+ return effectiveSyncSetupRemoteIdentity(
145
+ {
146
+ storage: connection.type === "git" ? { ...storage, path: "./" } : storage,
147
+ sync: { include: [], automatic: false },
148
+ },
149
+ connection,
150
+ );
167
151
  }
@@ -4,31 +4,31 @@ import { errorMessage } from "../../sync/sync-errors.js";
4
4
  import { safeTerminalText } from "../terminal-text.js";
5
5
 
6
6
  export async function promptInitialSetupName(ctx: ExtensionCommandContext, signal?: AbortSignal) {
7
- while (!signal?.aborted) {
8
- const hint = "For example: home or work. Leave blank for default.";
9
- // Pi styles the whole input title as accent; give only the guidance a muted role.
10
- const guidance = ctx.mode === "tui" ? ctx.ui.theme.fg("muted", hint) : hint;
11
- // This compact prompt owns its default hint; the general helper would repeat it.
12
- const value = await ctx.ui.input(`Sync setup name\n${guidance}`, undefined, { signal });
13
- if (signal?.aborted) {
14
- throw signal.reason instanceof Error
15
- ? signal.reason
16
- : new DOMException("The operation was aborted", "AbortError");
17
- }
18
- if (value === undefined) return undefined;
19
- const name = value.trim() || "default";
20
- try {
21
- if (name.includes("<") || name.includes(">")) {
22
- throw new Error("Replace example placeholders with your own name.");
23
- }
24
- validateConfigName(name, "sync setup");
25
- return name;
26
- } catch (error) {
27
- ctx.ui.notify(
28
- `This name cannot be used for the sync setup. ${safeTerminalText(errorMessage(error))} Enter another name (for example, default).`,
29
- "warning",
30
- );
31
- }
32
- }
33
- return undefined;
7
+ while (!signal?.aborted) {
8
+ const hint = "For example: home or work. Leave blank for default.";
9
+ // Pi styles the whole input title as accent; give only the guidance a muted role.
10
+ const guidance = ctx.mode === "tui" ? ctx.ui.theme.fg("muted", hint) : hint;
11
+ // This compact prompt owns its default hint; the general helper would repeat it.
12
+ const value = await ctx.ui.input(`Sync setup name\n${guidance}`, undefined, { signal });
13
+ if (signal?.aborted) {
14
+ throw signal.reason instanceof Error
15
+ ? signal.reason
16
+ : new DOMException("The operation was aborted", "AbortError");
17
+ }
18
+ if (value === undefined) return undefined;
19
+ const name = value.trim() || "default";
20
+ try {
21
+ if (name.includes("<") || name.includes(">")) {
22
+ throw new Error("Replace example placeholders with your own name.");
23
+ }
24
+ validateConfigName(name, "sync setup");
25
+ return name;
26
+ } catch (error) {
27
+ ctx.ui.notify(
28
+ `This name cannot be used for the sync setup. ${safeTerminalText(errorMessage(error))} Enter another name (for example, default).`,
29
+ "warning",
30
+ );
31
+ }
32
+ }
33
+ return undefined;
34
34
  }