@narumitw/pi-sync 0.50.3 → 0.51.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 (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
@@ -3,262 +3,237 @@ import { normalizeSyncInclude } from "../sync/sync-policy.js";
3
3
  import { type SyncSetupStorageReview, syncSetupStorageReview } from "./config.js";
4
4
  import { localConfigPath } from "./config-file.js";
5
5
  import { updateLocalConfig } from "./settings-store.js";
6
- import type {
7
- PiSyncSettingsV3,
8
- StorageConnectionSettings,
9
- SyncSetupSettings,
10
- } from "./settings-types.js";
6
+ import type { PiSyncSettingsV3, StorageConnectionSettings, SyncSetupSettings } from "./settings-types.js";
11
7
  import { effectiveSyncSetupRemoteIdentity, validateConfigName } from "./settings-validation.js";
12
8
 
13
9
  export class SyncSetupReviewChangedError extends Error {
14
- constructor(message: string) {
15
- super(message);
16
- this.name = "SyncSetupReviewChangedError";
17
- }
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = "SyncSetupReviewChangedError";
13
+ }
18
14
  }
19
15
 
20
16
  export async function saveNewV3Settings(
21
- input: {
22
- setupName: string;
23
- connectionName: string;
24
- connection: StorageConnectionSettings;
25
- setup: SyncSetupSettings;
26
- },
27
- signal?: AbortSignal,
17
+ input: {
18
+ setupName: string;
19
+ connectionName: string;
20
+ connection: StorageConnectionSettings;
21
+ setup: SyncSetupSettings;
22
+ },
23
+ signal?: AbortSignal,
28
24
  ) {
29
- validateConfigName(input.setupName, "sync setup");
30
- validateConfigName(input.connectionName, "storage connection");
31
- const settings: PiSyncSettingsV3 = {
32
- version: 3,
33
- activeSyncSetup: input.setupName,
34
- onSwitch: "ask-before-pull",
35
- skipSecretScan: false,
36
- storageConnections: { [input.connectionName]: structuredClone(input.connection) },
37
- syncSetups: {
38
- [input.setupName]: {
39
- ...structuredClone(input.setup),
40
- storage: { ...input.setup.storage, connection: input.connectionName },
41
- },
42
- },
43
- };
44
- return updateLocalConfig((current) => {
45
- if (Object.keys(current.storageConnections).length || Object.keys(current.syncSetups).length) {
46
- throw new Error(`Settings already exist: ${localConfigPath()}`);
47
- }
48
- return { ...current, ...settings };
49
- }, signal);
25
+ validateConfigName(input.setupName, "sync setup");
26
+ validateConfigName(input.connectionName, "storage connection");
27
+ const settings: PiSyncSettingsV3 = {
28
+ version: 3,
29
+ activeSyncSetup: input.setupName,
30
+ onSwitch: "ask-before-pull",
31
+ skipSecretScan: false,
32
+ storageConnections: { [input.connectionName]: structuredClone(input.connection) },
33
+ syncSetups: {
34
+ [input.setupName]: {
35
+ ...structuredClone(input.setup),
36
+ storage: { ...input.setup.storage, connection: input.connectionName },
37
+ },
38
+ },
39
+ };
40
+ return updateLocalConfig((current) => {
41
+ if (Object.keys(current.storageConnections).length || Object.keys(current.syncSetups).length) {
42
+ throw new Error(`Settings already exist: ${localConfigPath()}`);
43
+ }
44
+ return { ...current, ...settings };
45
+ }, signal);
50
46
  }
51
47
 
52
- export async function addStorageConnection(
53
- name: string,
54
- connection: StorageConnectionSettings,
55
- signal?: AbortSignal,
56
- ) {
57
- validateConfigName(name, "storage connection");
58
- await updateSettings((settings) => {
59
- if (Object.hasOwn(settings.storageConnections, name)) {
60
- throw new Error(`Storage connection already exists: ${name}`);
61
- }
62
- return {
63
- ...settings,
64
- storageConnections: {
65
- ...settings.storageConnections,
66
- [name]: structuredClone(connection),
67
- },
68
- };
69
- }, signal);
48
+ export async function addStorageConnection(name: string, connection: StorageConnectionSettings, signal?: AbortSignal) {
49
+ validateConfigName(name, "storage connection");
50
+ await updateSettings((settings) => {
51
+ if (Object.hasOwn(settings.storageConnections, name)) {
52
+ throw new Error(`Storage connection already exists: ${name}`);
53
+ }
54
+ return {
55
+ ...settings,
56
+ storageConnections: {
57
+ ...settings.storageConnections,
58
+ [name]: structuredClone(connection),
59
+ },
60
+ };
61
+ }, signal);
70
62
  }
71
63
 
72
64
  export async function updateStorageConnection(
73
- name: string,
74
- update: (connection: StorageConnectionSettings) => StorageConnectionSettings,
75
- expectedSetups?: readonly string[],
76
- signal?: AbortSignal,
65
+ name: string,
66
+ update: (connection: StorageConnectionSettings) => StorageConnectionSettings,
67
+ expectedSetups?: readonly string[],
68
+ signal?: AbortSignal,
77
69
  ) {
78
- validateConfigName(name, "storage connection");
79
- await updateSettings((settings) => {
80
- const connection = settings.storageConnections[name];
81
- if (!connection) throw new Error(`Storage connection not found: ${name}`);
82
- const currentSetups = referencingSetupNames(settings.syncSetups, name);
83
- if (expectedSetups && !sameNames(currentSetups, expectedSetups)) {
84
- throw new Error(
85
- `Storage connection “${name}” usage changed while it was open; reopen it and review the affected sync setups.`,
86
- );
87
- }
88
- const nextConnection = update(structuredClone(connection));
89
- const nextConnections = { ...settings.storageConnections, [name]: nextConnection };
90
- assertUniqueLocations(settings.syncSetups, nextConnections);
91
- return { ...settings, storageConnections: nextConnections };
92
- }, signal);
70
+ validateConfigName(name, "storage connection");
71
+ await updateSettings((settings) => {
72
+ const connection = settings.storageConnections[name];
73
+ if (!connection) throw new Error(`Storage connection not found: ${name}`);
74
+ const currentSetups = referencingSetupNames(settings.syncSetups, name);
75
+ if (expectedSetups && !sameNames(currentSetups, expectedSetups)) {
76
+ throw new Error(
77
+ `Storage connection “${name}” usage changed while it was open; reopen it and review the affected sync setups.`,
78
+ );
79
+ }
80
+ const nextConnection = update(structuredClone(connection));
81
+ const nextConnections = { ...settings.storageConnections, [name]: nextConnection };
82
+ assertUniqueLocations(settings.syncSetups, nextConnections);
83
+ return { ...settings, storageConnections: nextConnections };
84
+ }, signal);
93
85
  }
94
86
 
95
87
  export async function addSyncSetup(
96
- name: string,
97
- setup: SyncSetupSettings,
98
- signal?: AbortSignal,
99
- expectedConnection?: StorageConnectionSettings,
88
+ name: string,
89
+ setup: SyncSetupSettings,
90
+ signal?: AbortSignal,
91
+ expectedConnection?: StorageConnectionSettings,
100
92
  ) {
101
- validateConfigName(name, "sync setup");
102
- await updateSettings((settings) => {
103
- if (Object.hasOwn(settings.syncSetups, name))
104
- throw new Error(`Sync setup already exists: ${name}`);
105
- if (!Object.hasOwn(settings.storageConnections, setup.storage.connection)) {
106
- throw new Error(`Storage connection not found: ${setup.storage.connection}`);
107
- }
108
- if (
109
- expectedConnection &&
110
- !isDeepStrictEqual(settings.storageConnections[setup.storage.connection], expectedConnection)
111
- ) {
112
- throw new SyncSetupReviewChangedError(
113
- "Storage connection changed while the setup review was open; reopen it.",
114
- );
115
- }
116
- const nextSetups = { ...settings.syncSetups, [name]: structuredClone(setup) };
117
- assertUniqueLocations(nextSetups, settings.storageConnections);
118
- return {
119
- ...settings,
120
- syncSetups: nextSetups,
121
- ...(settings.activeSyncSetup ? {} : { activeSyncSetup: name }),
122
- };
123
- }, signal);
93
+ validateConfigName(name, "sync setup");
94
+ await updateSettings((settings) => {
95
+ if (Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup already exists: ${name}`);
96
+ if (!Object.hasOwn(settings.storageConnections, setup.storage.connection)) {
97
+ throw new Error(`Storage connection not found: ${setup.storage.connection}`);
98
+ }
99
+ if (
100
+ expectedConnection &&
101
+ !isDeepStrictEqual(settings.storageConnections[setup.storage.connection], expectedConnection)
102
+ ) {
103
+ throw new SyncSetupReviewChangedError("Storage connection changed while the setup review was open; reopen it.");
104
+ }
105
+ const nextSetups = { ...settings.syncSetups, [name]: structuredClone(setup) };
106
+ assertUniqueLocations(nextSetups, settings.storageConnections);
107
+ return {
108
+ ...settings,
109
+ syncSetups: nextSetups,
110
+ ...(settings.activeSyncSetup ? {} : { activeSyncSetup: name }),
111
+ };
112
+ }, signal);
124
113
  }
125
114
 
126
115
  export async function updateSyncSetup(
127
- name: string,
128
- update: (setup: SyncSetupSettings) => SyncSetupSettings,
129
- options: {
130
- expectedStorage?: SyncSetupStorageReview;
131
- expectedInclude?: readonly string[];
132
- expectedConnection?: StorageConnectionSettings;
133
- signal?: AbortSignal;
134
- } = {},
116
+ name: string,
117
+ update: (setup: SyncSetupSettings) => SyncSetupSettings,
118
+ options: {
119
+ expectedStorage?: SyncSetupStorageReview;
120
+ expectedInclude?: readonly string[];
121
+ expectedConnection?: StorageConnectionSettings;
122
+ signal?: AbortSignal;
123
+ } = {},
135
124
  ) {
136
- validateConfigName(name, "sync setup");
137
- await updateSettings((settings) => {
138
- const setup = settings.syncSetups[name];
139
- if (!setup) throw new Error(`Sync setup not found: ${name}`);
140
- if (
141
- options.expectedInclude &&
142
- !sameNames(normalizeSyncInclude(setup.sync.include), options.expectedInclude)
143
- ) {
144
- throw new SyncSetupReviewChangedError(
145
- `Sync setup “${name}” included content changed while it was open; reopen it and review the current selection.`,
146
- );
147
- }
148
- if (options.expectedStorage) {
149
- const connectionName = setup.storage.connection;
150
- const connection = settings.storageConnections[connectionName];
151
- if (
152
- !connection ||
153
- !sameStorageReview(
154
- syncSetupStorageReview(name, setup, connectionName, connection),
155
- options.expectedStorage,
156
- )
157
- ) {
158
- throw new SyncSetupReviewChangedError(
159
- `Sync setup “${name}” storage changed while it was open; reopen it and review the current storage location.`,
160
- );
161
- }
162
- }
163
- if (
164
- options.expectedConnection &&
165
- !isDeepStrictEqual(
166
- settings.storageConnections[setup.storage.connection],
167
- options.expectedConnection,
168
- )
169
- ) {
170
- throw new SyncSetupReviewChangedError(
171
- "Storage connection changed while the setup review was open; reopen it.",
172
- );
173
- }
174
- const nextSetup = update(structuredClone(setup));
175
- if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
176
- throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
177
- }
178
- const nextSetups = { ...settings.syncSetups, [name]: nextSetup };
179
- assertUniqueLocations(nextSetups, settings.storageConnections);
180
- return { ...settings, syncSetups: nextSetups };
181
- }, options.signal);
125
+ validateConfigName(name, "sync setup");
126
+ await updateSettings((settings) => {
127
+ const setup = settings.syncSetups[name];
128
+ if (!setup) throw new Error(`Sync setup not found: ${name}`);
129
+ if (options.expectedInclude && !sameNames(normalizeSyncInclude(setup.sync.include), options.expectedInclude)) {
130
+ throw new SyncSetupReviewChangedError(
131
+ `Sync setup “${name}” included content changed while it was open; reopen it and review the current selection.`,
132
+ );
133
+ }
134
+ if (options.expectedStorage) {
135
+ const connectionName = setup.storage.connection;
136
+ const connection = settings.storageConnections[connectionName];
137
+ if (
138
+ !connection ||
139
+ !sameStorageReview(syncSetupStorageReview(name, setup, connectionName, connection), options.expectedStorage)
140
+ ) {
141
+ throw new SyncSetupReviewChangedError(
142
+ `Sync setup “${name}” storage changed while it was open; reopen it and review the current storage location.`,
143
+ );
144
+ }
145
+ }
146
+ if (
147
+ options.expectedConnection &&
148
+ !isDeepStrictEqual(settings.storageConnections[setup.storage.connection], options.expectedConnection)
149
+ ) {
150
+ throw new SyncSetupReviewChangedError("Storage connection changed while the setup review was open; reopen it.");
151
+ }
152
+ const nextSetup = update(structuredClone(setup));
153
+ if (!Object.hasOwn(settings.storageConnections, nextSetup.storage.connection)) {
154
+ throw new Error(`Storage connection not found: ${nextSetup.storage.connection}`);
155
+ }
156
+ const nextSetups = { ...settings.syncSetups, [name]: nextSetup };
157
+ assertUniqueLocations(nextSetups, settings.storageConnections);
158
+ return { ...settings, syncSetups: nextSetups };
159
+ }, options.signal);
182
160
  }
183
161
 
184
162
  export async function removeSyncSetup(name: string, signal?: AbortSignal) {
185
- validateConfigName(name, "sync setup");
186
- await updateSettings((settings) => {
187
- if (!Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup not found: ${name}`);
188
- const isCurrent = settings.activeSyncSetup === name;
189
- if (isCurrent && Object.keys(settings.syncSetups).length > 1) {
190
- throw new Error("Switch to another sync setup before removing the current setup.");
191
- }
192
- const syncSetups = { ...settings.syncSetups };
193
- delete syncSetups[name];
194
- const next = { ...settings, syncSetups };
195
- if (isCurrent) delete next.activeSyncSetup;
196
- return next;
197
- }, signal);
163
+ validateConfigName(name, "sync setup");
164
+ await updateSettings((settings) => {
165
+ if (!Object.hasOwn(settings.syncSetups, name)) throw new Error(`Sync setup not found: ${name}`);
166
+ const isCurrent = settings.activeSyncSetup === name;
167
+ if (isCurrent && Object.keys(settings.syncSetups).length > 1) {
168
+ throw new Error("Switch to another sync setup before removing the current setup.");
169
+ }
170
+ const syncSetups = { ...settings.syncSetups };
171
+ delete syncSetups[name];
172
+ const next = { ...settings, syncSetups };
173
+ if (isCurrent) delete next.activeSyncSetup;
174
+ return next;
175
+ }, signal);
198
176
  }
199
177
 
200
178
  export async function removeStorageConnection(name: string, signal?: AbortSignal) {
201
- validateConfigName(name, "storage connection");
202
- await updateSettings((settings) => {
203
- const referenced = referencingSetupNames(settings.syncSetups, name)[0];
204
- if (referenced) {
205
- throw new Error(`Storage connection “${name}” is used by sync setup “${referenced}”.`);
206
- }
207
- if (!Object.hasOwn(settings.storageConnections, name)) {
208
- throw new Error(`Storage connection not found: ${name}`);
209
- }
210
- const storageConnections = { ...settings.storageConnections };
211
- delete storageConnections[name];
212
- return { ...settings, storageConnections };
213
- }, signal);
179
+ validateConfigName(name, "storage connection");
180
+ await updateSettings((settings) => {
181
+ const referenced = referencingSetupNames(settings.syncSetups, name)[0];
182
+ if (referenced) {
183
+ throw new Error(`Storage connection “${name}” is used by sync setup “${referenced}”.`);
184
+ }
185
+ if (!Object.hasOwn(settings.storageConnections, name)) {
186
+ throw new Error(`Storage connection not found: ${name}`);
187
+ }
188
+ const storageConnections = { ...settings.storageConnections };
189
+ delete storageConnections[name];
190
+ return { ...settings, storageConnections };
191
+ }, signal);
214
192
  }
215
193
 
216
- async function updateSettings(
217
- update: (settings: PiSyncSettingsV3) => PiSyncSettingsV3,
218
- signal?: AbortSignal,
219
- ) {
220
- return updateLocalConfig((settings) => {
221
- if (settings.version !== 3) {
222
- throw new Error("Storage connections and sync setups require version 3 pi-sync settings.");
223
- }
224
- return update(settings);
225
- }, signal);
194
+ async function updateSettings(update: (settings: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
195
+ return updateLocalConfig((settings) => {
196
+ if (settings.version !== 3) {
197
+ throw new Error("Storage connections and sync setups require version 3 pi-sync settings.");
198
+ }
199
+ return update(settings);
200
+ }, signal);
226
201
  }
227
202
 
228
203
  function referencingSetupNames(setups: Record<string, SyncSetupSettings>, connection: string) {
229
- return Object.entries(setups)
230
- .filter(([, setup]) => setup.storage.connection === connection)
231
- .map(([name]) => name)
232
- .sort((left, right) => left.localeCompare(right));
204
+ return Object.entries(setups)
205
+ .filter(([, setup]) => setup.storage.connection === connection)
206
+ .map(([name]) => name)
207
+ .sort((left, right) => left.localeCompare(right));
233
208
  }
234
209
 
235
210
  function assertUniqueLocations(
236
- setups: Record<string, SyncSetupSettings>,
237
- connections: Record<string, StorageConnectionSettings>,
211
+ setups: Record<string, SyncSetupSettings>,
212
+ connections: Record<string, StorageConnectionSettings>,
238
213
  ) {
239
- const identities = new Map<string, string>();
240
- for (const [name, setup] of Object.entries(setups)) {
241
- const connection = connections[setup.storage.connection];
242
- if (!connection) throw new Error(`Storage connection not found: ${setup.storage.connection}`);
243
- const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
244
- const previous = identities.get(identity);
245
- if (previous) {
246
- throw new Error(`Sync setup “${name}” duplicates the storage location of “${previous}”.`);
247
- }
248
- identities.set(identity, name);
249
- }
214
+ const identities = new Map<string, string>();
215
+ for (const [name, setup] of Object.entries(setups)) {
216
+ const connection = connections[setup.storage.connection];
217
+ if (!connection) throw new Error(`Storage connection not found: ${setup.storage.connection}`);
218
+ const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
219
+ const previous = identities.get(identity);
220
+ if (previous) {
221
+ throw new Error(`Sync setup “${name}” duplicates the storage location of “${previous}”.`);
222
+ }
223
+ identities.set(identity, name);
224
+ }
250
225
  }
251
226
 
252
227
  function sameNames(left: readonly string[], right: readonly string[]) {
253
- return left.length === right.length && left.every((name, index) => name === right[index]);
228
+ return left.length === right.length && left.every((name, index) => name === right[index]);
254
229
  }
255
230
 
256
231
  function sameStorageReview(left: SyncSetupStorageReview, right: SyncSetupStorageReview) {
257
- return (
258
- left.connectionName === right.connectionName &&
259
- left.storageKind === right.storageKind &&
260
- left.storagePath === right.storagePath &&
261
- left.bucket === right.bucket &&
262
- left.branch === right.branch
263
- );
232
+ return (
233
+ left.connectionName === right.connectionName &&
234
+ left.storageKind === right.storageKind &&
235
+ left.storagePath === right.storagePath &&
236
+ left.bucket === right.bucket &&
237
+ left.branch === right.branch
238
+ );
264
239
  }
@@ -1,109 +1,95 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import {
5
- localConfigPath,
6
- readMigratingLocalConfigDocument,
7
- updateLocalConfigDocument,
8
- } from "./config-file.js";
4
+ import { localConfigPath, readMigratingLocalConfigDocument, updateLocalConfigDocument } from "./config-file.js";
9
5
  import type { OnSwitchAction, PiSyncSettingsV3 } from "./settings-types.js";
10
6
  import { DEFAULT_ON_SWITCH, validateSettingsDocument } from "./settings-validation.js";
11
7
 
12
8
  let configUpdateQueue: Promise<void> = Promise.resolve();
13
9
 
14
10
  export async function requireSettings() {
15
- const settings = await readLocalConfigObject();
16
- if (!settings) {
17
- throw new Error(
18
- `Missing pi-sync settings. Open /sync and choose Set up sync, or create ${localConfigPath()}.`,
19
- );
20
- }
21
- return settings;
11
+ const settings = await readLocalConfigObject();
12
+ if (!settings) {
13
+ throw new Error(`Missing pi-sync settings. Open /sync and choose Set up sync, or create ${localConfigPath()}.`);
14
+ }
15
+ return settings;
22
16
  }
23
17
 
24
18
  export async function configuredSyncSetupNames() {
25
- const settings = await readLocalConfigObject();
26
- return settings
27
- ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right))
28
- : [];
19
+ const settings = await readLocalConfigObject();
20
+ return settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
29
21
  }
30
22
 
31
23
  export async function loadOnSwitch(): Promise<OnSwitchAction> {
32
- return (await requireSettings()).onSwitch;
24
+ return (await requireSettings()).onSwitch;
33
25
  }
34
26
 
35
27
  export function localConfigTemplate(): PiSyncSettingsV3 {
36
- return {
37
- version: 3,
38
- onSwitch: DEFAULT_ON_SWITCH,
39
- skipSecretScan: false,
40
- storageConnections: {},
41
- syncSetups: {},
42
- };
28
+ return {
29
+ version: 3,
30
+ onSwitch: DEFAULT_ON_SWITCH,
31
+ skipSecretScan: false,
32
+ showStatus: true,
33
+ storageConnections: {},
34
+ syncSetups: {},
35
+ };
43
36
  }
44
37
 
45
38
  export async function readLocalConfigDocument() {
46
- const document = await readMigratingLocalConfigDocument((settings) => {
47
- validateSettingsDocument(settings);
48
- });
49
- if (document) validateSettingsDocument(document.parsed);
50
- return document;
39
+ const document = await readMigratingLocalConfigDocument((settings) => {
40
+ validateSettingsDocument(settings);
41
+ });
42
+ if (document) validateSettingsDocument(document.parsed);
43
+ return document;
51
44
  }
52
45
 
53
46
  export async function readLocalConfigObject(): Promise<PiSyncSettingsV3 | undefined> {
54
- return (await readLocalConfigDocument())?.parsed as PiSyncSettingsV3 | undefined;
47
+ return (await readLocalConfigDocument())?.parsed as PiSyncSettingsV3 | undefined;
55
48
  }
56
49
 
57
- export function updateLocalConfig(
58
- update: (current: PiSyncSettingsV3) => PiSyncSettingsV3,
59
- signal?: AbortSignal,
60
- ) {
61
- const operation = configUpdateQueue.then(() => {
62
- signal?.throwIfAborted();
63
- return performLocalConfigUpdate(update, signal);
64
- });
65
- configUpdateQueue = operation.then(
66
- () => undefined,
67
- () => undefined,
68
- );
69
- return operation;
50
+ export function updateLocalConfig(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
51
+ const operation = configUpdateQueue.then(() => {
52
+ signal?.throwIfAborted();
53
+ return performLocalConfigUpdate(update, signal);
54
+ });
55
+ configUpdateQueue = operation.then(
56
+ () => undefined,
57
+ () => undefined,
58
+ );
59
+ return operation;
70
60
  }
71
61
 
72
- async function performLocalConfigUpdate(
73
- update: (current: PiSyncSettingsV3) => PiSyncSettingsV3,
74
- signal?: AbortSignal,
75
- ) {
76
- return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
62
+ async function performLocalConfigUpdate(update: (current: PiSyncSettingsV3) => PiSyncSettingsV3, signal?: AbortSignal) {
63
+ return updateLocalConfigDocument(localConfigTemplate(), update, validateSettingsDocument, signal);
77
64
  }
78
65
 
79
66
  export async function writeLocalConfigObject(value: PiSyncSettingsV3 | Record<string, unknown>) {
80
- validateSettingsDocument(value as Record<string, unknown>);
81
- const configPath = localConfigPath();
82
- await fs.mkdir(path.dirname(configPath), { recursive: true });
83
- try {
84
- const stat = await fs.lstat(configPath);
85
- if (stat.isSymbolicLink())
86
- throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
87
- if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
88
- } catch (error) {
89
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
90
- }
91
- const temporaryPath = path.join(
92
- path.dirname(configPath),
93
- `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
94
- );
95
- let handle: fs.FileHandle | undefined;
96
- try {
97
- handle = await fs.open(temporaryPath, "wx", 0o600);
98
- await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
99
- if (process.platform !== "win32") await handle.chmod(0o600);
100
- await handle.sync();
101
- await handle.close();
102
- handle = undefined;
103
- await fs.rename(temporaryPath, configPath);
104
- } catch (error) {
105
- await handle?.close().catch(() => undefined);
106
- await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
107
- throw error;
108
- }
67
+ validateSettingsDocument(value as Record<string, unknown>);
68
+ const configPath = localConfigPath();
69
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
70
+ try {
71
+ const stat = await fs.lstat(configPath);
72
+ if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlinked pi-sync settings: ${configPath}`);
73
+ if (!stat.isFile()) throw new Error(`pi-sync settings are not a regular file: ${configPath}`);
74
+ } catch (error) {
75
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
76
+ }
77
+ const temporaryPath = path.join(
78
+ path.dirname(configPath),
79
+ `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`,
80
+ );
81
+ let handle: fs.FileHandle | undefined;
82
+ try {
83
+ handle = await fs.open(temporaryPath, "wx", 0o600);
84
+ await handle.writeFile(`${JSON.stringify(value, null, "\t")}\n`, "utf8");
85
+ if (process.platform !== "win32") await handle.chmod(0o600);
86
+ await handle.sync();
87
+ await handle.close();
88
+ handle = undefined;
89
+ await fs.rename(temporaryPath, configPath);
90
+ } catch (error) {
91
+ await handle?.close().catch(() => undefined);
92
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
93
+ throw error;
94
+ }
109
95
  }