@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
@@ -5,10 +5,10 @@ import { readLocalConfigObject } from "../settings/settings-store.js";
5
5
  import type { AnySyncConfig } from "../settings/settings-types.js";
6
6
  import { isCloudflareR2Endpoint, ownRecord } from "../settings/settings-validation.js";
7
7
  import {
8
- inspectOperationAvailability,
9
- type OperationAvailability,
10
- operationBlocksChanges,
11
- operationCanRecover,
8
+ inspectOperationAvailability,
9
+ type OperationAvailability,
10
+ operationBlocksChanges,
11
+ operationCanRecover,
12
12
  } from "../state/operation-availability.js";
13
13
  import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
14
14
  import { errorMessage } from "../sync/sync-errors.js";
@@ -16,254 +16,222 @@ import type { StartupObservation } from "../sync/sync-inspection.js";
16
16
  import { compareSyncInclude, syncIncludeSelection } from "../sync/sync-policy.js";
17
17
  import { automaticSyncSummary } from "./automatic-sync-summary.js";
18
18
  import {
19
- observationMatchesConfig,
20
- observationSummary,
21
- type SyncAttentionState,
22
- syncAttentionMatchesConfig,
19
+ observationMatchesConfig,
20
+ observationSummary,
21
+ type SyncAttentionState,
22
+ syncAttentionMatchesConfig,
23
23
  } from "./sync-attention.js";
24
24
  import { countValidSyncSetups } from "./sync-setups-ui.js";
25
25
  import { safeTerminalText } from "./terminal-text.js";
26
26
 
27
27
  export const MAIN_MENU_ACTIONS = [
28
- "Sync now (recommended)",
29
- "Switch sync setup",
30
- "Status & changes",
31
- "Settings",
32
- "More…",
28
+ "Sync now (recommended)",
29
+ "Switch sync setup",
30
+ "Status & changes",
31
+ "Settings",
32
+ "More…",
33
33
  ] as const;
34
34
 
35
35
  export interface ManagerDescription {
36
- title: string;
37
- actions: string[];
38
- operation?: OperationAvailability;
39
- attention?: SyncAttentionState;
40
- observation?: StartupObservation;
41
- attentionBlocksSync?: boolean;
42
- attentionReviewDisabled?: boolean;
36
+ title: string;
37
+ actions: string[];
38
+ operation?: OperationAvailability;
39
+ attention?: SyncAttentionState;
40
+ observation?: StartupObservation;
41
+ attentionBlocksSync?: boolean;
42
+ attentionReviewDisabled?: boolean;
43
43
  }
44
44
 
45
45
  export async function describeManagerState(
46
- signal?: AbortSignal,
47
- attention?: SyncAttentionState,
48
- inspectOperation: () => Promise<OperationAvailability> = inspectOperationAvailability,
49
- observation?: StartupObservation,
46
+ signal?: AbortSignal,
47
+ attention?: SyncAttentionState,
48
+ inspectOperation: () => Promise<OperationAvailability> = inspectOperationAvailability,
49
+ observation?: StartupObservation,
50
50
  ): Promise<ManagerDescription> {
51
- let raw: Record<string, unknown> | undefined;
52
- try {
53
- raw = await readLocalConfigObject();
54
- } catch (error) {
55
- return {
56
- title: [
57
- "Manage sync",
58
- "",
59
- "Settings file needs repair. Automatic sync and settings writes are paused.",
60
- `Error: ${safeTerminalText(errorMessage(error))}`,
61
- `File: ${safeTerminalText(await activeLocalConfigPath())}`,
62
- "",
63
- "Repair the JSON file, then reopen /sync.",
64
- ].join("\n"),
65
- actions: ["Help"],
66
- };
67
- }
68
- if (!raw) {
69
- return {
70
- title: ["Manage sync", "", "Not set up.", "", "What do you want to do?"].join("\n"),
71
- actions: ["Set up sync", "Help"],
72
- };
73
- }
74
- const configuredTargets = ownRecord(raw.syncSetups);
75
- if (raw.version === 3 && configuredTargets && Object.keys(configuredTargets).length === 0) {
76
- return {
77
- title: [
78
- "Manage sync",
79
- "",
80
- "No sync setups are configured.",
81
- "Add a sync setup using an existing storage connection.",
82
- "",
83
- "What do you want to do?",
84
- ].join("\n"),
85
- actions: ["Sync setups…", "Storage connections…", "Help"],
86
- };
87
- }
88
- try {
89
- const config = await loadConfig();
90
- const operation = await inspectOperation();
91
- const changesBlocked = operationBlocksChanges(operation);
92
- const selection = syncIncludeSelection(config.include);
93
- let currentAttention: SyncAttentionState | undefined;
94
- if (attention) {
95
- try {
96
- const attentionConfig =
97
- attention.decision.setupName === config.setupName
98
- ? config
99
- : await loadConfig(attention.decision.setupName);
100
- if (syncAttentionMatchesConfig(attention, attentionConfig)) currentAttention = attention;
101
- } catch {
102
- currentAttention = undefined;
103
- }
104
- if (signal?.aborted) throw signal.reason;
105
- }
106
- const attentionComparison = currentAttention
107
- ? compareSyncInclude(
108
- currentAttention.decision.localInclude,
109
- currentAttention.decision.remoteInclude,
110
- )
111
- : undefined;
112
- const noSyncedContent = config.include.length === 0;
113
- const syncState = changesBlocked
114
- ? undefined
115
- : await readStateForConfig(config).catch(() => undefined);
116
- const currentObservation =
117
- observation &&
118
- observationMatchesConfig(observation, config) &&
119
- syncState &&
120
- syncStateFingerprint(syncState) === observation.inspection.stateIdentity
121
- ? observation
122
- : undefined;
123
- const lastAppliedSnapshot = changesBlocked
124
- ? "Unavailable while operations are locked"
125
- : syncState?.lastAppliedSnapshot
126
- ? safeTerminalText(syncState.lastAppliedSnapshot)
127
- : syncState
128
- ? "Never synced"
129
- : "Unavailable";
130
- const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
131
- const mainActions = MAIN_MENU_ACTIONS.filter(
132
- (action) => action !== "Switch sync setup" || canSwitch,
133
- );
134
- const ordinaryTitle = [
135
- "Manage sync",
136
- "",
137
- `Current sync setup: ${safeTerminalText(config.setupName)}`,
138
- `Storage: ${backendStorageDescription(config)}`,
139
- `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
140
- `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
141
- `Last applied: ${lastAppliedSnapshot}`,
142
- ...(currentAttention
143
- ? [
144
- currentAttention.decision.setupName === config.setupName
145
- ? "Sync status: Review needed"
146
- : `Sync status: Review needed for setup ${safeTerminalText(currentAttention.decision.setupName)}`,
147
- attentionComparison?.remoteOnly.length === 0 &&
148
- attentionComparison.localOnly.length === 0
149
- ? "Only the synced-content order differs."
150
- : `Remote-only paths: ${attentionComparison?.remoteOnly.length ?? 0} · Device-only paths: ${attentionComparison?.localOnly.length ?? 0}`,
151
- "Nothing has been changed.",
152
- ]
153
- : currentObservation
154
- ? [
155
- `Startup check (${safeTerminalText(currentObservation.checkedAt)}): ${observationSummary(currentObservation)}`,
156
- "Check-time observation only; synchronization rechecks current content.",
157
- ]
158
- : ["Remote status: Not checked"]),
159
- ...(noSyncedContent
160
- ? [
161
- "",
162
- "No included content is selected. Choose included content in Settings before syncing.",
163
- ]
164
- : []),
165
- "",
166
- "What do you want to do?",
167
- ];
168
- return {
169
- title: (changesBlocked
170
- ? ["Manage sync", ...operationStatusLines(operation)]
171
- : ordinaryTitle
172
- ).join("\n"),
173
- actions: operationActions(operation, noSyncedContent, canSwitch, mainActions),
174
- operation,
175
- ...(currentObservation ? { observation: currentObservation } : {}),
176
- ...(currentAttention
177
- ? {
178
- attention: currentAttention,
179
- attentionBlocksSync: currentAttention.decision.setupName === config.setupName,
180
- attentionReviewDisabled: changesBlocked,
181
- }
182
- : {}),
183
- };
184
- } catch (error) {
185
- if (signal?.aborted) throw error;
186
- return {
187
- title: [
188
- "Manage sync",
189
- "",
190
- "Settings need attention. Automatic sync is paused.",
191
- `Current sync setup: ${safeTerminalText(typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : "none")}`,
192
- `Error: ${safeTerminalText(errorMessage(error))}`,
193
- `File: ${safeTerminalText(await activeLocalConfigPath())}`,
194
- "",
195
- "What do you want to do?",
196
- ].join("\n"),
197
- actions: ["Sync setups…", "Storage connections…", "History & recovery…", "Help"],
198
- };
199
- }
51
+ let raw: Record<string, unknown> | undefined;
52
+ try {
53
+ raw = await readLocalConfigObject();
54
+ } catch (error) {
55
+ return {
56
+ title: [
57
+ "Manage sync",
58
+ "",
59
+ "Settings file needs repair. Automatic sync and settings writes are paused.",
60
+ `Error: ${safeTerminalText(errorMessage(error))}`,
61
+ `File: ${safeTerminalText(await activeLocalConfigPath())}`,
62
+ "",
63
+ "Repair the JSON file, then reopen /sync.",
64
+ ].join("\n"),
65
+ actions: ["Help"],
66
+ };
67
+ }
68
+ if (!raw) {
69
+ return {
70
+ title: ["Manage sync", "", "Not set up.", "", "What do you want to do?"].join("\n"),
71
+ actions: ["Set up sync", "Help"],
72
+ };
73
+ }
74
+ const configuredTargets = ownRecord(raw.syncSetups);
75
+ if (raw.version === 3 && configuredTargets && Object.keys(configuredTargets).length === 0) {
76
+ return {
77
+ title: [
78
+ "Manage sync",
79
+ "",
80
+ "No sync setups are configured.",
81
+ "Add a sync setup using an existing storage connection.",
82
+ "",
83
+ "What do you want to do?",
84
+ ].join("\n"),
85
+ actions: ["Sync setups…", "Storage connections…", "Help"],
86
+ };
87
+ }
88
+ try {
89
+ const config = await loadConfig();
90
+ const operation = await inspectOperation();
91
+ const changesBlocked = operationBlocksChanges(operation);
92
+ const selection = syncIncludeSelection(config.include);
93
+ let currentAttention: SyncAttentionState | undefined;
94
+ if (attention) {
95
+ try {
96
+ const attentionConfig =
97
+ attention.decision.setupName === config.setupName ? config : await loadConfig(attention.decision.setupName);
98
+ if (syncAttentionMatchesConfig(attention, attentionConfig)) currentAttention = attention;
99
+ } catch {
100
+ currentAttention = undefined;
101
+ }
102
+ if (signal?.aborted) throw signal.reason;
103
+ }
104
+ const attentionComparison = currentAttention
105
+ ? compareSyncInclude(currentAttention.decision.localInclude, currentAttention.decision.remoteInclude)
106
+ : undefined;
107
+ const noSyncedContent = config.include.length === 0;
108
+ const syncState = changesBlocked ? undefined : await readStateForConfig(config).catch(() => undefined);
109
+ const currentObservation =
110
+ observation &&
111
+ observationMatchesConfig(observation, config) &&
112
+ syncState &&
113
+ syncStateFingerprint(syncState) === observation.inspection.stateIdentity
114
+ ? observation
115
+ : undefined;
116
+ const lastAppliedSnapshot = changesBlocked
117
+ ? "Unavailable while operations are locked"
118
+ : syncState?.lastAppliedSnapshot
119
+ ? safeTerminalText(syncState.lastAppliedSnapshot)
120
+ : syncState
121
+ ? "Never synced"
122
+ : "Unavailable";
123
+ const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
124
+ const mainActions = MAIN_MENU_ACTIONS.filter((action) => action !== "Switch sync setup" || canSwitch);
125
+ const ordinaryTitle = [
126
+ "Manage sync",
127
+ "",
128
+ `Current sync setup: ${safeTerminalText(config.setupName)}`,
129
+ `Storage: ${backendStorageDescription(config)}`,
130
+ `Included: ${selection.builtIns.length} built-in group${selection.builtIns.length === 1 ? "" : "s"} · ${selection.custom.length} extra path${selection.custom.length === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
131
+ `Automatic sync: ${automaticSyncSummary(config.automatic)}`,
132
+ `Last applied: ${lastAppliedSnapshot}`,
133
+ ...(currentAttention
134
+ ? [
135
+ currentAttention.decision.setupName === config.setupName
136
+ ? "Sync status: Review needed"
137
+ : `Sync status: Review needed for setup ${safeTerminalText(currentAttention.decision.setupName)}`,
138
+ attentionComparison?.remoteOnly.length === 0 && attentionComparison.localOnly.length === 0
139
+ ? "Only the synced-content order differs."
140
+ : `Remote-only paths: ${attentionComparison?.remoteOnly.length ?? 0} · Device-only paths: ${attentionComparison?.localOnly.length ?? 0}`,
141
+ "Nothing has been changed.",
142
+ ]
143
+ : currentObservation
144
+ ? [
145
+ `Startup check (${safeTerminalText(currentObservation.checkedAt)}): ${observationSummary(currentObservation)}`,
146
+ "Check-time observation only; synchronization rechecks current content.",
147
+ ]
148
+ : ["Remote status: Not checked"]),
149
+ ...(noSyncedContent
150
+ ? ["", "No included content is selected. Choose included content in Settings before syncing."]
151
+ : []),
152
+ "",
153
+ "What do you want to do?",
154
+ ];
155
+ return {
156
+ title: (changesBlocked ? ["Manage sync", ...operationStatusLines(operation)] : ordinaryTitle).join("\n"),
157
+ actions: operationActions(operation, noSyncedContent, canSwitch, mainActions),
158
+ operation,
159
+ ...(currentObservation ? { observation: currentObservation } : {}),
160
+ ...(currentAttention
161
+ ? {
162
+ attention: currentAttention,
163
+ attentionBlocksSync: currentAttention.decision.setupName === config.setupName,
164
+ attentionReviewDisabled: changesBlocked,
165
+ }
166
+ : {}),
167
+ };
168
+ } catch (error) {
169
+ if (signal?.aborted) throw error;
170
+ return {
171
+ title: [
172
+ "Manage sync",
173
+ "",
174
+ "Settings need attention. Automatic sync is paused.",
175
+ `Current sync setup: ${safeTerminalText(typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : "none")}`,
176
+ `Error: ${safeTerminalText(errorMessage(error))}`,
177
+ `File: ${safeTerminalText(await activeLocalConfigPath())}`,
178
+ "",
179
+ "What do you want to do?",
180
+ ].join("\n"),
181
+ actions: ["Sync setups…", "Storage connections…", "History & recovery…", "Help"],
182
+ };
183
+ }
200
184
  }
201
185
 
202
186
  function operationActions(
203
- operation: OperationAvailability,
204
- noSyncedContent: boolean,
205
- canSwitch: boolean,
206
- mainActions: string[],
187
+ operation: OperationAvailability,
188
+ noSyncedContent: boolean,
189
+ canSwitch: boolean,
190
+ mainActions: string[],
207
191
  ) {
208
- if (operationCanRecover(operation)) {
209
- return [
210
- "Restore sync access… (recommended)",
211
- "Status & changes",
212
- "History & recovery…",
213
- "Help",
214
- ];
215
- }
216
- if (operation.kind !== "free") {
217
- return ["Refresh operation status", "Status & changes", "History & recovery…", "Help"];
218
- }
219
- return noSyncedContent
220
- ? ["Settings", ...(canSwitch ? ["Switch sync setup"] : []), "Status & changes", "More…"]
221
- : mainActions;
192
+ if (operationCanRecover(operation)) {
193
+ return ["Restore sync access… (recommended)", "Status & changes", "History & recovery…", "Help"];
194
+ }
195
+ if (operation.kind !== "free") {
196
+ return ["Refresh operation status", "Status & changes", "History & recovery…", "Help"];
197
+ }
198
+ return noSyncedContent
199
+ ? ["Settings", ...(canSwitch ? ["Switch sync setup"] : []), "Status & changes", "More…"]
200
+ : mainActions;
222
201
  }
223
202
 
224
203
  function operationStatusLines(operation: OperationAvailability): string[] {
225
- switch (operation.kind) {
226
- case "free":
227
- return [];
228
- case "live": {
229
- const command = truncateToWidth(safeTerminalText(operation.lock.command), 16, "…");
230
- return [
231
- `Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Settings and More return.`,
232
- ];
233
- }
234
- case "busy":
235
- return [
236
- "Pi-sync may be starting or finishing. Wait, then refresh; Settings and More remain unavailable.",
237
- ];
238
- case "recoverable-stale":
239
- return [
240
- "Sync paused: old lock remains. Close other Pi sessions then restore; Settings and More return.",
241
- ];
242
- case "recoverable-unreadable":
243
- return [
244
- "Sync paused: owner unknown. Close other Pi sessions then restore; Settings and More return.",
245
- ];
246
- case "inspection-error":
247
- return [
248
- "Lock check failed. Fix path access, then refresh; Settings and More remain unavailable.",
249
- ];
250
- }
204
+ switch (operation.kind) {
205
+ case "free":
206
+ return [];
207
+ case "live": {
208
+ const command = truncateToWidth(safeTerminalText(operation.lock.command), 16, "…");
209
+ return [`Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Settings and More return.`];
210
+ }
211
+ case "busy":
212
+ return ["Pi-sync may be starting or finishing. Wait, then refresh; Settings and More remain unavailable."];
213
+ case "recoverable-stale":
214
+ return ["Sync paused: old lock remains. Close other Pi sessions then restore; Settings and More return."];
215
+ case "recoverable-unreadable":
216
+ return ["Sync paused: owner unknown. Close other Pi sessions then restore; Settings and More return."];
217
+ case "inspection-error":
218
+ return ["Lock check failed. Fix path access, then refresh; Settings and More remain unavailable."];
219
+ }
251
220
  }
252
221
 
253
222
  export function backendStorageDescription(config: AnySyncConfig) {
254
- const connection = safeTerminalText(config.connectionName);
255
- switch (config.backend.type) {
256
- case "s3": {
257
- const type =
258
- config.backend.profile.kind === "r2" ||
259
- isCloudflareR2Endpoint(config.backend.profile.endpoint)
260
- ? "Cloudflare R2"
261
- : "S3-compatible";
262
- return `${type} · ${connection} · ${safeTerminalText(config.backend.destination.bucket)}`;
263
- }
264
- case "webdav":
265
- return `WebDAV · ${connection} · ${safeTerminalText(config.backend.destination.path)}`;
266
- case "git":
267
- return `Git · ${connection} · ${safeTerminalText(config.backend.destination.branch)}`;
268
- }
223
+ const connection = safeTerminalText(config.connectionName);
224
+ switch (config.backend.type) {
225
+ case "s3": {
226
+ const type =
227
+ config.backend.profile.kind === "r2" || isCloudflareR2Endpoint(config.backend.profile.endpoint)
228
+ ? "Cloudflare R2"
229
+ : "S3-compatible";
230
+ return `${type} · ${connection} · ${safeTerminalText(config.backend.destination.bucket)}`;
231
+ }
232
+ case "webdav":
233
+ return `WebDAV · ${connection} · ${safeTerminalText(config.backend.destination.path)}`;
234
+ case "git":
235
+ return `Git · ${connection} · ${safeTerminalText(config.backend.destination.branch)}`;
236
+ }
269
237
  }