@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
@@ -7,11 +7,8 @@ import { snapshotOptionsForContext } from "../snapshot/session-paths.js";
7
7
  import { createSnapshot, scanSnapshot } from "../snapshot/snapshot.js";
8
8
  import type { SnapshotOptions } from "../snapshot/snapshot-types.js";
9
9
  import { inspectLock, isLockGuardHeld, isStaleLock, withLock } from "../state/lock.js";
10
- import {
11
- formatDiff,
12
- formatSnapshotOnlyDiff,
13
- publicationCapabilityDescription,
14
- } from "../ui/sync-format.js";
10
+ import { formatDiff, formatSnapshotOnlyDiff, publicationCapabilityDescription } from "../ui/sync-format.js";
11
+ import { setSyncStatus } from "../ui/sync-status.js";
15
12
  import { safeTerminalText } from "../ui/terminal-text.js";
16
13
  import { formatRemoteSelectionStatus, readRemoteSnapshot } from "./remote-snapshot.js";
17
14
  import { throwIfAborted } from "./signals.js";
@@ -19,217 +16,201 @@ import { errorMessage } from "./sync-errors.js";
19
16
  import { inspectSync } from "./sync-inspection.js";
20
17
  import { rollback } from "./sync-mutations.js";
21
18
 
22
- const STATUS_KEY = "sync";
23
19
  const DEFAULT_PROFILE = "default";
24
20
 
25
21
  export async function status(
26
- ctx: ExtensionCommandContext,
27
- options: CommandOptions,
28
- factory: SyncBackendFactory = createSyncBackend,
22
+ ctx: ExtensionCommandContext,
23
+ options: CommandOptions,
24
+ factory: SyncBackendFactory = createSyncBackend,
29
25
  ) {
30
- const config = await loadConfig(options.setup);
31
- throwIfAborted(options.signal);
32
- ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
33
- const { head, selectionState, localChanged, remoteChanged, localFiles, destination, capability } =
34
- await inspectSync(config, snapshotOptionsForContext(ctx, config), options.signal, factory);
35
- throwIfAborted(options.signal);
26
+ const config = await loadConfig(options.setup);
27
+ throwIfAborted(options.signal);
28
+ setSyncStatus(ctx, `checking ${config.setupName}`);
29
+ const { head, selectionState, localChanged, remoteChanged, localFiles, destination, capability } = await inspectSync(
30
+ config,
31
+ snapshotOptionsForContext(ctx, config),
32
+ options.signal,
33
+ factory,
34
+ );
35
+ throwIfAborted(options.signal);
36
36
 
37
- const remoteText = head
38
- ? `remote: ${head.snapshotId} from ${head.machine} at ${head.createdAt}`
39
- : "remote: empty";
40
- const warnings = syncSessionsWarnings(config);
41
- ctx.ui.setStatus(STATUS_KEY, undefined);
42
- ctx.ui.notify(
43
- [
44
- `sync setup: ${config.setupName}`,
45
- `storage connection: ${config.connectionName}`,
46
- `storage location: ${safeTerminalText(destination)}`,
47
- `publication safety: ${publicationCapabilityDescription(capability)}`,
48
- `included content: ${config.include.join(", ") || "none"}`,
49
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
50
- remoteText,
51
- formatRemoteSelectionStatus(selectionState),
52
- `local files: ${localFiles}`,
53
- `local changed since last sync: ${localChanged ? "yes" : "no"}`,
54
- `remote changed since last sync: ${remoteChanged ? "yes" : "no"}`,
55
- ...warnings,
56
- ].join("\n"),
57
- localChanged || remoteChanged || selectionState?.kind === "different" || warnings.length > 0
58
- ? "warning"
59
- : "info",
60
- );
37
+ const remoteText = head ? `remote: ${head.snapshotId} from ${head.machine} at ${head.createdAt}` : "remote: empty";
38
+ const warnings = syncSessionsWarnings(config);
39
+ setSyncStatus(ctx, undefined);
40
+ ctx.ui.notify(
41
+ [
42
+ `sync setup: ${config.setupName}`,
43
+ `storage connection: ${config.connectionName}`,
44
+ `storage location: ${safeTerminalText(destination)}`,
45
+ `publication safety: ${publicationCapabilityDescription(capability)}`,
46
+ `included content: ${config.include.join(", ") || "none"}`,
47
+ `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
48
+ remoteText,
49
+ formatRemoteSelectionStatus(selectionState),
50
+ `local files: ${localFiles}`,
51
+ `local changed since last sync: ${localChanged ? "yes" : "no"}`,
52
+ `remote changed since last sync: ${remoteChanged ? "yes" : "no"}`,
53
+ ...warnings,
54
+ ].join("\n"),
55
+ localChanged || remoteChanged || selectionState?.kind === "different" || warnings.length > 0 ? "warning" : "info",
56
+ );
61
57
  }
62
58
 
63
59
  export async function diff(
64
- ctx: ExtensionCommandContext,
65
- options: CommandOptions,
66
- factory: SyncBackendFactory = createSyncBackend,
60
+ ctx: ExtensionCommandContext,
61
+ options: CommandOptions,
62
+ factory: SyncBackendFactory = createSyncBackend,
67
63
  ) {
68
- const config = await loadConfig(options.setup);
69
- throwIfAborted(options.signal);
70
- ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
71
- const backend = await factory(config);
72
- const local = await createSnapshot(
73
- config.snapshotIdentity,
74
- snapshotOptionsForContext(ctx, config),
75
- );
76
- throwIfAborted(options.signal);
77
- const { snapshot: remote, selectionState } = await readRemoteSnapshot(
78
- backend,
79
- config,
80
- options.signal,
81
- { allowSelectionDifference: true },
82
- );
83
- throwIfAborted(options.signal);
84
- ctx.ui.setStatus(STATUS_KEY, undefined);
64
+ const config = await loadConfig(options.setup);
65
+ throwIfAborted(options.signal);
66
+ setSyncStatus(ctx, `checking ${config.setupName}`);
67
+ const backend = await factory(config);
68
+ const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
69
+ throwIfAborted(options.signal);
70
+ const { snapshot: remote, selectionState } = await readRemoteSnapshot(backend, config, options.signal, {
71
+ allowSelectionDifference: true,
72
+ });
73
+ throwIfAborted(options.signal);
74
+ setSyncStatus(ctx, undefined);
85
75
 
86
- const warnings = syncSessionsWarnings(config);
87
- const header = [
88
- `sync setup: ${config.setupName}`,
89
- `storage connection: ${config.connectionName}`,
90
- `storage location: ${safeTerminalText(backend.destination)}`,
91
- `included content: ${config.include.join(", ") || "none"}`,
92
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
93
- formatRemoteSelectionStatus(selectionState),
94
- ...warnings,
95
- ].join("\n");
96
- const level = warnings.length > 0 || selectionState?.kind === "different" ? "warning" : "info";
97
- if (!remote) {
98
- ctx.ui.notify(
99
- `${header}\n\n${formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local)}`,
100
- level,
101
- );
102
- return;
103
- }
76
+ const warnings = syncSessionsWarnings(config);
77
+ const header = [
78
+ `sync setup: ${config.setupName}`,
79
+ `storage connection: ${config.connectionName}`,
80
+ `storage location: ${safeTerminalText(backend.destination)}`,
81
+ `included content: ${config.include.join(", ") || "none"}`,
82
+ `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
83
+ formatRemoteSelectionStatus(selectionState),
84
+ ...warnings,
85
+ ].join("\n");
86
+ const level = warnings.length > 0 || selectionState?.kind === "different" ? "warning" : "info";
87
+ if (!remote) {
88
+ ctx.ui.notify(`${header}\n\n${formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local)}`, level);
89
+ return;
90
+ }
104
91
 
105
- ctx.ui.notify(`${header}\n\n${formatDiff(local, remote)}`, level);
92
+ ctx.ui.notify(`${header}\n\n${formatDiff(local, remote)}`, level);
106
93
  }
107
94
 
108
95
  export async function doctor(
109
- ctx: ExtensionCommandContext,
110
- options: CommandOptions,
111
- factory: SyncBackendFactory = createSyncBackend,
96
+ ctx: ExtensionCommandContext,
97
+ options: CommandOptions,
98
+ factory: SyncBackendFactory = createSyncBackend,
112
99
  ) {
113
- const messages: string[] = [];
114
- let level: "info" | "warning" = "info";
115
- let snapshotOptions: SnapshotOptions = {};
116
- let profile = DEFAULT_PROFILE;
117
- let backend: SyncBackend | undefined;
118
- let backendSummary: string[] = [];
100
+ const messages: string[] = [];
101
+ let level: "info" | "warning" = "info";
102
+ let snapshotOptions: SnapshotOptions = {};
103
+ let profile = DEFAULT_PROFILE;
104
+ let backend: SyncBackend | undefined;
105
+ let backendSummary: string[] = [];
119
106
 
120
- try {
121
- const config = await loadConfig(options.setup);
122
- throwIfAborted(options.signal);
123
- backend = await factory(config);
124
- profile = config.snapshotIdentity;
125
- snapshotOptions = snapshotOptionsForContext(ctx, config);
126
- messages.push(
127
- `config: ok (sync setup ${config.setupName})`,
128
- `included content: ${config.include.join(", ") || "none"}`,
129
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
130
- );
131
- backendSummary = [
132
- `storage location: ${safeTerminalText(backend.destination)}`,
133
- `publication safety: ${publicationCapabilityDescription(backend.capability)}`,
134
- ];
135
- const warnings = syncSessionsWarnings(config);
136
- if (warnings.length > 0) {
137
- level = "warning";
138
- messages.push(...warnings);
139
- }
140
- } catch (error) {
141
- throwIfAborted(options.signal);
142
- level = "warning";
143
- messages.push(`config: ${errorMessage(error)}`);
144
- }
107
+ try {
108
+ const config = await loadConfig(options.setup);
109
+ throwIfAborted(options.signal);
110
+ backend = await factory(config);
111
+ profile = config.snapshotIdentity;
112
+ snapshotOptions = snapshotOptionsForContext(ctx, config);
113
+ messages.push(
114
+ `config: ok (sync setup ${config.setupName})`,
115
+ `included content: ${config.include.join(", ") || "none"}`,
116
+ `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
117
+ );
118
+ backendSummary = [
119
+ `storage location: ${safeTerminalText(backend.destination)}`,
120
+ `publication safety: ${publicationCapabilityDescription(backend.capability)}`,
121
+ ];
122
+ const warnings = syncSessionsWarnings(config);
123
+ if (warnings.length > 0) {
124
+ level = "warning";
125
+ messages.push(...warnings);
126
+ }
127
+ } catch (error) {
128
+ throwIfAborted(options.signal);
129
+ level = "warning";
130
+ messages.push(`config: ${errorMessage(error)}`);
131
+ }
145
132
 
146
- const local = await createSnapshot(profile, snapshotOptions);
147
- throwIfAborted(options.signal);
148
- const secrets = scanSnapshot(local);
149
- if (secrets.length > 0) {
150
- level = "warning";
151
- messages.push("secret scan: possible secrets found:");
152
- messages.push(...secrets.map((secret) => `- ${secret}`));
153
- } else {
154
- messages.push(`secret scan: ok (${local.files.length} files checked)`);
155
- }
133
+ const local = await createSnapshot(profile, snapshotOptions);
134
+ throwIfAborted(options.signal);
135
+ const secrets = scanSnapshot(local);
136
+ if (secrets.length > 0) {
137
+ level = "warning";
138
+ messages.push("secret scan: possible secrets found:");
139
+ messages.push(...secrets.map((secret) => `- ${secret}`));
140
+ } else {
141
+ messages.push(`secret scan: ok (${local.files.length} files checked)`);
142
+ }
156
143
 
157
- const lock = await inspectLock();
158
- throwIfAborted(options.signal);
159
- if (lock.status === "valid" && isStaleLock(lock.lock)) {
160
- level = "warning";
161
- messages.push(
162
- `lock: stale (pid ${lock.lock.pid}); run /sync unlock --stale after verifying no sync is running`,
163
- );
164
- } else if (lock.status === "valid") {
165
- messages.push(`lock: held by pid ${lock.lock.pid} since ${lock.lock.startedAt}`);
166
- } else if (lock.status === "unreadable") {
167
- level = "warning";
168
- messages.push(
169
- "lock: unreadable; use /sync unlock --stale only after verifying no sync is running",
170
- );
171
- } else if (await isLockGuardHeld()) {
172
- throwIfAborted(options.signal);
173
- level = "warning";
174
- messages.push("lock: guard active while metadata is missing or still being initialized");
175
- } else {
176
- messages.push("lock: free");
177
- }
144
+ const lock = await inspectLock();
145
+ throwIfAborted(options.signal);
146
+ if (lock.status === "valid" && isStaleLock(lock.lock)) {
147
+ level = "warning";
148
+ messages.push(`lock: stale (pid ${lock.lock.pid}); run /sync unlock --stale after verifying no sync is running`);
149
+ } else if (lock.status === "valid") {
150
+ messages.push(`lock: held by pid ${lock.lock.pid} since ${lock.lock.startedAt}`);
151
+ } else if (lock.status === "unreadable") {
152
+ level = "warning";
153
+ messages.push("lock: unreadable; use /sync unlock --stale only after verifying no sync is running");
154
+ } else if (await isLockGuardHeld()) {
155
+ throwIfAborted(options.signal);
156
+ level = "warning";
157
+ messages.push("lock: guard active while metadata is missing or still being initialized");
158
+ } else {
159
+ messages.push("lock: free");
160
+ }
178
161
 
179
- if (backend) {
180
- messages.push(...backendSummary);
181
- const diagnostics = await backend.diagnose(options.signal);
182
- throwIfAborted(options.signal);
183
- for (const diagnostic of diagnostics) {
184
- messages.push(diagnostic.message);
185
- if (diagnostic.level !== "info") level = "warning";
186
- }
187
- }
188
- ctx.ui.notify(messages.join("\n"), level);
162
+ if (backend) {
163
+ messages.push(...backendSummary);
164
+ const diagnostics = await backend.diagnose(options.signal);
165
+ throwIfAborted(options.signal);
166
+ for (const diagnostic of diagnostics) {
167
+ messages.push(diagnostic.message);
168
+ if (diagnostic.level !== "info") level = "warning";
169
+ }
170
+ }
171
+ ctx.ui.notify(messages.join("\n"), level);
189
172
  }
190
173
 
191
174
  export async function history(
192
- ctx: ExtensionCommandContext,
193
- options: CommandOptions,
194
- factory: SyncBackendFactory = createSyncBackend,
175
+ ctx: ExtensionCommandContext,
176
+ options: CommandOptions,
177
+ factory: SyncBackendFactory = createSyncBackend,
195
178
  ) {
196
- const config = await loadConfig(options.setup);
197
- throwIfAborted(options.signal);
198
- const backend = await factory(config);
199
- const snapshots = (await backend.listHistory(options.signal)).slice(-20).reverse();
200
- throwIfAborted(options.signal);
201
- if (snapshots.length === 0) {
202
- ctx.ui.notify("No remote pi-sync history found.", "info");
203
- return;
204
- }
179
+ const config = await loadConfig(options.setup);
180
+ throwIfAborted(options.signal);
181
+ const backend = await factory(config);
182
+ const snapshots = (await backend.listHistory(options.signal)).slice(-20).reverse();
183
+ throwIfAborted(options.signal);
184
+ if (snapshots.length === 0) {
185
+ ctx.ui.notify("No remote pi-sync history found.", "info");
186
+ return;
187
+ }
205
188
 
206
- const currentSnapshot = snapshots[0]?.snapshotId;
207
- if (ctx.mode === "tui") {
208
- const labels = snapshots.map(
209
- (item, index) =>
210
- `${index + 1}. ${item.createdAt} · ${safeTerminalText(item.machine)} · ${item.snapshotId}${item.snapshotId === currentSnapshot ? " (current)" : ""}${item.syncSessions ? " · sessions" : ""}`,
211
- );
212
- const selected = await ctx.ui.select(
213
- `History for sync setup “${safeTerminalText(config.setupName)}”\n\nChoose a snapshot to preview rollback.`,
214
- [...labels, "Back"],
215
- );
216
- if (!selected || selected === "Back") return;
217
- throwIfAborted(options.signal);
218
- const index = labels.indexOf(selected);
219
- const snapshot = snapshots[index];
220
- if (!snapshot) return;
221
- await withLock("rollback", () =>
222
- rollback(ctx, { ...options, args: [snapshot.snapshotRef], yes: false }, factory, {
223
- backendIdentity: backend.identity,
224
- setup: config.setupName,
225
- }),
226
- );
227
- return;
228
- }
229
- ctx.ui.notify(
230
- snapshots
231
- .map((item) => `${item.snapshotRef} ${item.createdAt} ${safeTerminalText(item.machine)}`)
232
- .join("\n"),
233
- "info",
234
- );
189
+ const currentSnapshot = snapshots[0]?.snapshotId;
190
+ if (ctx.mode === "tui") {
191
+ const labels = snapshots.map(
192
+ (item, index) =>
193
+ `${index + 1}. ${item.createdAt} · ${safeTerminalText(item.machine)} · ${item.snapshotId}${item.snapshotId === currentSnapshot ? " (current)" : ""}${item.syncSessions ? " · sessions" : ""}`,
194
+ );
195
+ const selected = await ctx.ui.select(
196
+ `History for sync setup “${safeTerminalText(config.setupName)}”\n\nChoose a snapshot to preview rollback.`,
197
+ [...labels, "Back"],
198
+ );
199
+ if (!selected || selected === "Back") return;
200
+ throwIfAborted(options.signal);
201
+ const index = labels.indexOf(selected);
202
+ const snapshot = snapshots[index];
203
+ if (!snapshot) return;
204
+ await withLock("rollback", () =>
205
+ rollback(ctx, { ...options, args: [snapshot.snapshotRef], yes: false }, factory, {
206
+ backendIdentity: backend.identity,
207
+ setup: config.setupName,
208
+ }),
209
+ );
210
+ return;
211
+ }
212
+ ctx.ui.notify(
213
+ snapshots.map((item) => `${item.snapshotRef} ${item.createdAt} ${safeTerminalText(item.machine)}`).join("\n"),
214
+ "info",
215
+ );
235
216
  }
@@ -1,168 +1,142 @@
1
1
  import type { RemoteHead } from "../backends/sync-backend.js";
2
2
  import { toPosix } from "../paths.js";
3
3
  import {
4
- canonicalSnapshotPathForConfig,
5
- filterSnapshotForConfigPolicy,
6
- isConfiguredSnapshotPath,
7
- isSessionPath,
4
+ canonicalSnapshotPathForConfig,
5
+ filterSnapshotForConfigPolicy,
6
+ isConfiguredSnapshotPath,
7
+ isSessionPath,
8
8
  } from "../snapshot/snapshot.js";
9
9
  import type { Snapshot } from "../snapshot/snapshot-types.js";
10
10
  import type { SyncState } from "../state/state-types.js";
11
11
  import {
12
- customIncludePathsByLower,
13
- includeFromSelectionConfig,
14
- normalizeSyncInclude,
15
- type SyncSelectionConfig,
12
+ customIncludePathsByLower,
13
+ includeFromSelectionConfig,
14
+ normalizeSyncInclude,
15
+ type SyncSelectionConfig,
16
16
  } from "./sync-policy.js";
17
17
 
18
18
  type SyncPolicyConfig = SyncSelectionConfig;
19
19
 
20
20
  export function hasLocalChanges(local: Snapshot, state: SyncState, config: SyncPolicyConfig) {
21
- return !sameHashes(fileHashMap(local), stateHashMapForConfig(state, config));
21
+ return !sameHashes(fileHashMap(local), stateHashMapForConfig(state, config));
22
22
  }
23
23
 
24
24
  export function remoteChangedSinceState(
25
- head: RemoteHead | undefined,
26
- state: SyncState,
27
- config: SyncPolicyConfig,
28
- sameRevision: (left: string, right: string) => boolean,
25
+ head: RemoteHead | undefined,
26
+ state: SyncState,
27
+ config: SyncPolicyConfig,
28
+ sameRevision: (left: string, right: string) => boolean,
29
29
  ) {
30
- if (!head) return Boolean(state.lastAppliedSnapshot);
31
- if (head.snapshotId !== state.lastAppliedSnapshot) return true;
32
- if (state.lastRemoteRevision && !sameRevision(head.revision, state.lastRemoteRevision))
33
- return true;
34
- if (syncIncludeChanged(state, config)) return true;
35
- return (
36
- includeFromSelectionConfig(config).includes("sessions") &&
37
- !state.include?.includes("sessions") &&
38
- head.syncSessions
39
- );
30
+ if (!head) return Boolean(state.lastAppliedSnapshot);
31
+ if (head.snapshotId !== state.lastAppliedSnapshot) return true;
32
+ if (state.lastRemoteRevision && !sameRevision(head.revision, state.lastRemoteRevision)) return true;
33
+ if (syncIncludeChanged(state, config)) return true;
34
+ return (
35
+ includeFromSelectionConfig(config).includes("sessions") && !state.include?.includes("sessions") && head.syncSessions
36
+ );
40
37
  }
41
38
 
42
39
  export function hasRemoteChanges(
43
- remote: Snapshot,
44
- state: SyncState,
45
- config: SyncPolicyConfig,
46
- ignoredPaths = new Set<string>(),
40
+ remote: Snapshot,
41
+ state: SyncState,
42
+ config: SyncPolicyConfig,
43
+ ignoredPaths = new Set<string>(),
47
44
  ) {
48
- if (remote.id === state.lastAppliedSnapshot && !syncPolicyChanged(state, config)) return false;
49
- return !snapshotHashesMatchState(
50
- filterSnapshotForConfigPolicy(remote, config),
51
- state,
52
- config,
53
- ignoredPaths,
54
- );
45
+ if (remote.id === state.lastAppliedSnapshot && !syncPolicyChanged(state, config)) return false;
46
+ return !snapshotHashesMatchState(filterSnapshotForConfigPolicy(remote, config), state, config, ignoredPaths);
55
47
  }
56
48
 
57
49
  export function sameHashes(left: Record<string, string>, right: Record<string, string>) {
58
- const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
59
- for (const key of keys) if (left[key] !== right[key]) return false;
60
- return true;
50
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
51
+ for (const key of keys) if (left[key] !== right[key]) return false;
52
+ return true;
61
53
  }
62
54
 
63
55
  export function fileHashMap(snapshot: Snapshot) {
64
- return Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));
56
+ return Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));
65
57
  }
66
58
 
67
59
  function stateHashMapForConfig(state: SyncState, config: SyncPolicyConfig) {
68
- const includePaths = customIncludePathsByLower(includeFromSelectionConfig(config));
69
- return Object.fromEntries(
70
- Object.entries(state.lastFileHashes)
71
- .filter(([filePath]) => isConfiguredSnapshotPath(filePath, config))
72
- .map(([filePath, hash]) => [canonicalSnapshotPathForConfig(filePath, includePaths), hash]),
73
- );
60
+ const includePaths = customIncludePathsByLower(includeFromSelectionConfig(config));
61
+ return Object.fromEntries(
62
+ Object.entries(state.lastFileHashes)
63
+ .filter(([filePath]) => isConfiguredSnapshotPath(filePath, config))
64
+ .map(([filePath, hash]) => [canonicalSnapshotPathForConfig(filePath, includePaths), hash]),
65
+ );
74
66
  }
75
67
 
76
68
  export function snapshotHashesMatchState(
77
- snapshot: Snapshot,
78
- state: SyncState,
79
- config: SyncPolicyConfig,
80
- ignoredPaths = new Set<string>(),
69
+ snapshot: Snapshot,
70
+ state: SyncState,
71
+ config: SyncPolicyConfig,
72
+ ignoredPaths = new Set<string>(),
81
73
  ) {
82
- return sameHashes(
83
- withoutHashPaths(fileHashMap(snapshot), ignoredPaths),
84
- withoutHashPaths(stateHashMapForConfig(state, config), ignoredPaths),
85
- );
74
+ return sameHashes(
75
+ withoutHashPaths(fileHashMap(snapshot), ignoredPaths),
76
+ withoutHashPaths(stateHashMapForConfig(state, config), ignoredPaths),
77
+ );
86
78
  }
87
79
 
88
80
  export function snapshotsMatch(left: Snapshot, right: Snapshot) {
89
- return (
90
- left.syncSessions === right.syncSessions && sameHashes(fileHashMap(left), fileHashMap(right))
91
- );
81
+ return left.syncSessions === right.syncSessions && sameHashes(fileHashMap(left), fileHashMap(right));
92
82
  }
93
83
 
94
84
  function withoutHashPaths(hashes: Record<string, string>, ignoredPaths: Set<string>) {
95
- if (ignoredPaths.size === 0) return hashes;
96
- return Object.fromEntries(
97
- Object.entries(hashes).filter(([filePath]) => !ignoredPaths.has(toPosix(filePath))),
98
- );
85
+ if (ignoredPaths.size === 0) return hashes;
86
+ return Object.fromEntries(Object.entries(hashes).filter(([filePath]) => !ignoredPaths.has(toPosix(filePath))));
99
87
  }
100
88
 
101
89
  export function syncPolicyChanged(state: SyncState, config: SyncPolicyConfig) {
102
- return syncIncludeChanged(state, config);
90
+ return syncIncludeChanged(state, config);
103
91
  }
104
92
 
105
93
  export function shouldRefreshSyncedState(
106
- remote: Snapshot,
107
- head: RemoteHead | undefined,
108
- state: SyncState,
109
- config: SyncPolicyConfig,
110
- sameRevision: (left: string, right: string) => boolean,
94
+ remote: Snapshot,
95
+ head: RemoteHead | undefined,
96
+ state: SyncState,
97
+ config: SyncPolicyConfig,
98
+ sameRevision: (left: string, right: string) => boolean,
111
99
  ) {
112
- return (
113
- remote.id !== state.lastAppliedSnapshot ||
114
- Boolean(
115
- head && (!state.lastRemoteRevision || !sameRevision(head.revision, state.lastRemoteRevision)),
116
- ) ||
117
- syncPolicyChanged(state, config)
118
- );
100
+ return (
101
+ remote.id !== state.lastAppliedSnapshot ||
102
+ Boolean(head && (!state.lastRemoteRevision || !sameRevision(head.revision, state.lastRemoteRevision))) ||
103
+ syncPolicyChanged(state, config)
104
+ );
119
105
  }
120
106
 
121
107
  function syncIncludeChanged(state: SyncState, config: SyncPolicyConfig) {
122
- const stored = state.include
123
- ? normalizeSyncInclude(state.include)
124
- : includeFromSelectionConfig(state);
125
- const current = includeFromSelectionConfig(config);
126
- return stored.length !== current.length || stored.some((item, index) => item !== current[index]);
108
+ const stored = state.include ? normalizeSyncInclude(state.include) : includeFromSelectionConfig(state);
109
+ const current = includeFromSelectionConfig(config);
110
+ return stored.length !== current.length || stored.some((item, index) => item !== current[index]);
127
111
  }
128
112
 
129
113
  export function settingsHashMap(snapshot: Snapshot) {
130
- return Object.fromEntries(
131
- snapshot.files
132
- .filter((file) => !isSessionPath(file.path))
133
- .map((file) => [file.path, file.sha256]),
134
- );
114
+ return Object.fromEntries(
115
+ snapshot.files.filter((file) => !isSessionPath(file.path)).map((file) => [file.path, file.sha256]),
116
+ );
135
117
  }
136
118
 
137
119
  export function sessionHashMap(snapshot: Snapshot) {
138
- return Object.fromEntries(
139
- snapshot.files
140
- .filter((file) => isSessionPath(file.path))
141
- .map((file) => [file.path, file.sha256]),
142
- );
120
+ return Object.fromEntries(
121
+ snapshot.files.filter((file) => isSessionPath(file.path)).map((file) => [file.path, file.sha256]),
122
+ );
143
123
  }
144
124
 
145
125
  export function settingsHashMapFromState(state: SyncState) {
146
- return Object.fromEntries(
147
- Object.entries(state.lastFileHashes).filter(([filePath]) => !isSessionPath(filePath)),
148
- );
126
+ return Object.fromEntries(Object.entries(state.lastFileHashes).filter(([filePath]) => !isSessionPath(filePath)));
149
127
  }
150
128
 
151
129
  export function settingsHashesMatchState(remote: Snapshot, state: SyncState) {
152
- return sameHashes(settingsHashMap(remote), settingsHashMapFromState(state));
130
+ return sameHashes(settingsHashMap(remote), settingsHashMapFromState(state));
153
131
  }
154
132
 
155
133
  export function canPullRemoteSettingsOnFirstSync(local: Snapshot, remote: Snapshot) {
156
- const remoteSettings = settingsHashMap(remote);
157
- return Object.entries(settingsHashMap(local)).every(
158
- ([filePath, hash]) => remoteSettings[filePath] === hash,
159
- );
134
+ const remoteSettings = settingsHashMap(remote);
135
+ return Object.entries(settingsHashMap(local)).every(([filePath, hash]) => remoteSettings[filePath] === hash);
160
136
  }
161
137
 
162
138
  export function canPullRemoteSessionsOnFirstSync(local: Snapshot, remote: Snapshot) {
163
- const localSessions = sessionHashMap(local);
164
- const remoteSessions = sessionHashMap(remote);
165
- return Object.entries(localSessions).every(
166
- ([filePath, hash]) => remoteSessions[filePath] === hash,
167
- );
139
+ const localSessions = sessionHashMap(local);
140
+ const remoteSessions = sessionHashMap(remote);
141
+ return Object.entries(localSessions).every(([filePath, hash]) => remoteSessions[filePath] === hash);
168
142
  }