@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
@@ -4,100 +4,95 @@ import type { CommandOptions } from "../commands/command-types.js";
4
4
  import { loadConfig, loadPartialConfig } from "../settings/config.js";
5
5
  import { isMissingConfigError } from "../settings/config-errors.js";
6
6
  import { consumeLocalConfigMigrationNotice } from "../settings/config-file.js";
7
- import { configuredSyncSetupNames } from "../settings/settings-store.js";
7
+ import { readLocalConfigObject } from "../settings/settings-store.js";
8
8
  import { snapshotOptionsForContext } from "../snapshot/session-paths.js";
9
9
  import { recoverSnapshotTransactionsOnStartup } from "../snapshot/snapshot-transaction.js";
10
10
  import { withLock } from "../state/lock.js";
11
11
  import { stateDirectoryMigrationNotice } from "../state/state-directory.js";
12
12
  import { ensureStateDir, readStateForConfig } from "../state/sync-state-store.js";
13
+ import { configureSyncStatus, setSyncStatus } from "../ui/sync-status.js";
13
14
  import { safeTerminalText } from "../ui/terminal-text.js";
14
15
  import { throwIfAborted } from "./signals.js";
15
16
  import { errorMessage } from "./sync-errors.js";
16
17
  import type { SyncLoaders } from "./sync-loaders.js";
17
18
 
18
19
  const AUTO_SYNC_OPTIONS: CommandOptions = {
19
- yes: true,
20
- force: false,
21
- stale: false,
22
- silent: true,
23
- reload: false,
24
- auto: true,
25
- args: [],
20
+ yes: true,
21
+ force: false,
22
+ stale: false,
23
+ silent: true,
24
+ reload: false,
25
+ auto: true,
26
+ args: [],
26
27
  };
27
28
 
28
- const STATUS_KEY = "sync";
29
-
30
29
  export async function startSession(ctx: ExtensionContext, signal: AbortSignal) {
31
- throwIfAborted(signal);
32
- const stateNotice = stateDirectoryMigrationNotice();
33
- if (stateNotice && ctx.hasUI) ctx.ui.notify(stateNotice, "warning");
34
- // Recovery can restore managed files: finish it before Pi accepts user edits.
35
- // Unlike remote inspection, it must never be detached behind startup.
36
- await recoverSnapshotTransactionsOnStartup();
37
- throwIfAborted(signal);
38
- try {
39
- const names = await configuredSyncSetupNames();
40
- throwIfAborted(signal);
41
- setSyncSetupCompletions(names);
42
- } catch {
43
- if (signal.aborted) return;
44
- setSyncSetupCompletions([]);
45
- }
46
- const migrationNotice = consumeLocalConfigMigrationNotice();
47
- if (migrationNotice && ctx.hasUI) ctx.ui.notify(migrationNotice, "warning");
48
- throwIfAborted(signal);
49
- if (!ctx.hasUI) return;
50
- try {
51
- const config = await loadConfig();
52
- throwIfAborted(signal);
53
- return config.automatic ? config : undefined;
54
- } catch (error) {
55
- throwIfAborted(signal);
56
- if (!isMissingConfigError(error)) {
57
- ctx.ui.notify(
58
- `pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}`,
59
- "warning",
60
- );
61
- }
62
- }
30
+ throwIfAborted(signal);
31
+ const stateNotice = stateDirectoryMigrationNotice();
32
+ if (stateNotice && ctx.hasUI) ctx.ui.notify(stateNotice, "warning");
33
+ // Recovery can restore managed files: finish it before Pi accepts user edits.
34
+ // Unlike remote inspection, it must never be detached behind startup.
35
+ await recoverSnapshotTransactionsOnStartup();
36
+ throwIfAborted(signal);
37
+ try {
38
+ const settings = await readLocalConfigObject();
39
+ throwIfAborted(signal);
40
+ const names = settings ? Object.keys(settings.syncSetups).sort((left, right) => left.localeCompare(right)) : [];
41
+ setSyncSetupCompletions(names);
42
+ configureSyncStatus(ctx, settings?.showStatus ?? true);
43
+ } catch {
44
+ if (signal.aborted) return;
45
+ setSyncSetupCompletions([]);
46
+ }
47
+ const migrationNotice = consumeLocalConfigMigrationNotice();
48
+ if (migrationNotice && ctx.hasUI) ctx.ui.notify(migrationNotice, "warning");
49
+ throwIfAborted(signal);
50
+ if (!ctx.hasUI) return;
51
+ try {
52
+ const config = await loadConfig();
53
+ throwIfAborted(signal);
54
+ configureSyncStatus(ctx, config.showStatus);
55
+ return config.automatic ? config : undefined;
56
+ } catch (error) {
57
+ throwIfAborted(signal);
58
+ if (!isMissingConfigError(error)) {
59
+ ctx.ui.notify(`pi-sync startup check skipped: ${safeTerminalText(errorMessage(error))}`, "warning");
60
+ }
61
+ }
63
62
  }
64
63
 
65
- export async function autoPushSessions(
66
- ctx: ExtensionContext,
67
- signal: AbortSignal,
68
- loaders: SyncLoaders,
69
- ) {
70
- try {
71
- const partial = await loadPartialConfig();
72
- throwIfAborted(signal);
73
- if (!partial.automatic) return;
74
- if (!partial.include.includes("sessions")) return;
75
- await ensureStateDir();
76
- throwIfAborted(signal);
77
- const config = await loadConfig();
78
- throwIfAborted(signal);
79
- if (!config.include.includes("sessions")) return;
80
- const [operations, snapshotModule, syncStateModule] = await Promise.all([
81
- loaders.operations(),
82
- loaders.snapshot(),
83
- loaders.syncState(),
84
- ]);
85
- throwIfAborted(signal);
86
- await withLock("auto-session-push", async () => {
87
- throwIfAborted(signal);
88
- const state = await readStateForConfig(config);
89
- throwIfAborted(signal);
90
- const local = await snapshotModule.createSnapshot(config.snapshotIdentity, {
91
- ...snapshotOptionsForContext(ctx, config),
92
- signal,
93
- });
94
- throwIfAborted(signal);
95
- if (!syncStateModule.hasLocalChanges(local, state, config)) return;
96
- await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
97
- });
98
- } catch (error) {
99
- if (signal.aborted || isMissingConfigError(error)) return;
100
- ctx.ui.setStatus(STATUS_KEY, undefined);
101
- ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
102
- }
64
+ export async function autoPushSessions(ctx: ExtensionContext, signal: AbortSignal, loaders: SyncLoaders) {
65
+ try {
66
+ const partial = await loadPartialConfig();
67
+ throwIfAborted(signal);
68
+ if (!partial.automatic) return;
69
+ if (!partial.include.includes("sessions")) return;
70
+ await ensureStateDir();
71
+ throwIfAborted(signal);
72
+ const config = await loadConfig();
73
+ throwIfAborted(signal);
74
+ if (!config.include.includes("sessions")) return;
75
+ const [operations, snapshotModule, syncStateModule] = await Promise.all([
76
+ loaders.operations(),
77
+ loaders.snapshot(),
78
+ loaders.syncState(),
79
+ ]);
80
+ throwIfAborted(signal);
81
+ await withLock("auto-session-push", async () => {
82
+ throwIfAborted(signal);
83
+ const state = await readStateForConfig(config);
84
+ throwIfAborted(signal);
85
+ const local = await snapshotModule.createSnapshot(config.snapshotIdentity, {
86
+ ...snapshotOptionsForContext(ctx, config),
87
+ signal,
88
+ });
89
+ throwIfAborted(signal);
90
+ if (!syncStateModule.hasLocalChanges(local, state, config)) return;
91
+ await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
92
+ });
93
+ } catch (error) {
94
+ if (signal.aborted || isMissingConfigError(error)) return;
95
+ setSyncStatus(ctx, undefined);
96
+ ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
97
+ }
103
98
  }
@@ -5,79 +5,71 @@ import { filterSnapshotForConfigPolicy } from "../snapshot/snapshot.js";
5
5
  import type { Snapshot } from "../snapshot/snapshot-types.js";
6
6
  import { safeTerminalText } from "../ui/terminal-text.js";
7
7
  import {
8
- inspectRemoteSelection,
9
- type RemoteSelectionState,
10
- remoteSelectionMismatch,
11
- sameSyncInclude,
12
- snapshotSelectionInclude,
8
+ inspectRemoteSelection,
9
+ type RemoteSelectionState,
10
+ remoteSelectionMismatch,
11
+ sameSyncInclude,
12
+ snapshotSelectionInclude,
13
13
  } from "./sync-policy.js";
14
14
 
15
- export async function readSnapshotForHead(
16
- backend: SyncBackend,
17
- head: RemoteHead,
18
- signal?: AbortSignal,
19
- ) {
20
- const snapshot = await backend.readSnapshot(head.snapshotRef, signal);
21
- if (signal?.aborted) {
22
- throw signal.reason instanceof Error
23
- ? signal.reason
24
- : new DOMException("The operation was aborted", "AbortError");
25
- }
26
- if (snapshot.id !== head.snapshotId) {
27
- throw new Error(
28
- `Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`,
29
- );
30
- }
31
- if (head.selection) {
32
- const snapshotInclude = snapshotSelectionInclude(snapshot);
33
- if (!snapshotInclude || !sameSyncInclude(head.selection.include, snapshotInclude)) {
34
- throw new Error("Remote head selection does not match its immutable snapshot.");
35
- }
36
- }
37
- return snapshot;
15
+ export async function readSnapshotForHead(backend: SyncBackend, head: RemoteHead, signal?: AbortSignal) {
16
+ const snapshot = await backend.readSnapshot(head.snapshotRef, signal);
17
+ if (signal?.aborted) {
18
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
19
+ }
20
+ if (snapshot.id !== head.snapshotId) {
21
+ throw new Error(`Remote head ${head.snapshotId} resolved to unexpected snapshot ${snapshot.id}.`);
22
+ }
23
+ if (head.selection) {
24
+ const snapshotInclude = snapshotSelectionInclude(snapshot);
25
+ if (!snapshotInclude || !sameSyncInclude(head.selection.include, snapshotInclude)) {
26
+ throw new Error("Remote head selection does not match its immutable snapshot.");
27
+ }
28
+ }
29
+ return snapshot;
38
30
  }
39
31
 
40
32
  export function requireCompatibleRemoteSelection(config: AnySyncConfig, snapshot: Snapshot) {
41
- const state = inspectRemoteSelection(config.include, snapshot);
42
- if (state.kind === "different") {
43
- throw remoteSelectionMismatch(config, state.include, syncConfigReviewFingerprint(config));
44
- }
33
+ const state = inspectRemoteSelection(config.include, snapshot);
34
+ if (state.kind === "different") {
35
+ throw remoteSelectionMismatch(config, state.include, syncConfigReviewFingerprint(config));
36
+ }
45
37
  }
46
38
 
47
39
  export function formatRemoteSelectionStatus(state: RemoteSelectionState | undefined) {
48
- if (!state) return "remote included content: unavailable (remote is empty)";
49
- if (state.kind === "same") return "remote included content: matches this setup";
50
- if (state.kind === "legacy") {
51
- return `remote included content: unavailable in legacy snapshot (${state.discovered.length} path${state.discovered.length === 1 ? "" : "s"} discovered, partial)`;
52
- }
53
- return [
54
- "remote included content: differs from this setup",
55
- `remote-only selection: ${safeList(state.remoteOnly)}`,
56
- `local-only selection: ${safeList(state.localOnly)}`,
57
- ].join("\n");
40
+ if (!state) return "remote included content: unavailable (remote is empty)";
41
+ if (state.kind === "same") return "remote included content: matches this setup";
42
+ if (state.kind === "legacy") {
43
+ return `remote included content: unavailable in legacy snapshot (${state.discovered.length} path${state.discovered.length === 1 ? "" : "s"} discovered, partial)`;
44
+ }
45
+ return [
46
+ "remote included content: differs from this setup",
47
+ `remote-only selection: ${safeList(state.remoteOnly)}`,
48
+ `local-only selection: ${safeList(state.localOnly)}`,
49
+ ].join("\n");
58
50
  }
59
51
 
60
52
  function safeList(values: readonly string[]) {
61
- return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
53
+ return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
62
54
  }
63
55
 
64
56
  export async function readRemoteSnapshot(
65
- backend: SyncBackend,
66
- config: AnySyncConfig,
67
- signal?: AbortSignal,
68
- options: { allowSelectionDifference?: boolean } = {},
57
+ backend: SyncBackend,
58
+ config: AnySyncConfig,
59
+ signal?: AbortSignal,
60
+ options: { allowSelectionDifference?: boolean } = {},
69
61
  ) {
70
- const head = await backend.readHead(signal);
71
- if (!head) return { head: undefined, snapshot: undefined, selectionState: undefined };
72
- const snapshot = await readSnapshotForHead(backend, head, signal);
73
- const selectionState = inspectRemoteSelection(config.include, snapshot);
74
- if (!options.allowSelectionDifference && selectionState.kind === "different") {
75
- const configIdentity = syncConfigReviewFingerprint(config);
76
- throw remoteSelectionMismatch(config, selectionState.include, configIdentity);
77
- }
78
- return {
79
- head,
80
- snapshot: filterSnapshotForConfigPolicy(snapshot, config),
81
- selectionState,
82
- };
62
+ const head = await backend.readHead(signal);
63
+ if (!head) return { head: undefined, snapshot: undefined, selectionState: undefined };
64
+ const snapshot = await readSnapshotForHead(backend, head, signal);
65
+ const selectionState = inspectRemoteSelection(config.include, snapshot);
66
+ if (!options.allowSelectionDifference && selectionState.kind === "different") {
67
+ const configIdentity = syncConfigReviewFingerprint(config);
68
+ throw remoteSelectionMismatch(config, selectionState.include, configIdentity);
69
+ }
70
+ return {
71
+ head,
72
+ snapshot: filterSnapshotForConfigPolicy(snapshot, config),
73
+ selectionState,
74
+ };
83
75
  }
@@ -1,10 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { setSyncSetupCompletions } from "../commands/command.js";
3
- import {
4
- loadConfig,
5
- syncConfigReviewIdentity,
6
- syncSetupReviewIdentity,
7
- } from "../settings/config.js";
3
+ import { loadConfig, syncConfigReviewIdentity, syncSetupReviewIdentity } from "../settings/config.js";
8
4
  import { configuredSyncSetupNames, updateLocalConfig } from "../settings/settings-store.js";
9
5
  import type { OnSwitchAction } from "../settings/settings-types.js";
10
6
  import { normalizeOnSwitch } from "../settings/settings-validation.js";
@@ -14,137 +10,126 @@ import { SetupPullRequiresUiError } from "./sync-errors.js";
14
10
  export { SetupPullRequiresUiError } from "./sync-errors.js";
15
11
 
16
12
  export const SETUP_SWITCH_ACTION_OPTIONS: ReadonlyArray<{
17
- label: string;
18
- value: OnSwitchAction;
13
+ label: string;
14
+ value: OnSwitchAction;
19
15
  }> = [
20
- { label: "Ask before pull", value: "ask-before-pull" },
21
- { label: "Start pull", value: "pull-after-switch" },
22
- { label: "Switch only", value: "switch-only" },
16
+ { label: "Ask before pull", value: "ask-before-pull" },
17
+ { label: "Start pull", value: "pull-after-switch" },
18
+ { label: "Switch only", value: "switch-only" },
23
19
  ];
24
20
 
25
21
  export function setupSwitchActionLabel(action: OnSwitchAction) {
26
- return SETUP_SWITCH_ACTION_OPTIONS.find((option) => option.value === action)?.label ?? action;
22
+ return SETUP_SWITCH_ACTION_OPTIONS.find((option) => option.value === action)?.label ?? action;
27
23
  }
28
24
 
29
25
  export function setupSwitchActionFromLabel(label: string): OnSwitchAction | undefined {
30
- return SETUP_SWITCH_ACTION_OPTIONS.find((option) => option.label === label)?.value;
26
+ return SETUP_SWITCH_ACTION_OPTIONS.find((option) => option.label === label)?.value;
31
27
  }
32
28
 
33
29
  export async function saveOnSwitch(action: OnSwitchAction, signal?: AbortSignal) {
34
- await updateLocalConfig((settings) => ({ ...settings, onSwitch: action }), signal);
30
+ await updateLocalConfig((settings) => ({ ...settings, onSwitch: action }), signal);
35
31
  }
36
32
 
37
33
  export type SetupPullOutcome = "applied" | "cancelled";
38
34
 
39
35
  export interface SetupSwitchResult {
40
- pullApplied: boolean;
36
+ pullApplied: boolean;
41
37
  }
42
38
 
43
39
  export async function useSyncSetup(
44
- ctx: ExtensionCommandContext,
45
- name: string,
46
- pullCurrentSetup?: (setup: string) => Promise<SetupPullOutcome | undefined>,
47
- expectedAction?: OnSwitchAction,
48
- signal?: AbortSignal,
49
- expectedSetupIdentity?: string,
40
+ ctx: ExtensionCommandContext,
41
+ name: string,
42
+ pullCurrentSetup?: (setup: string) => Promise<SetupPullOutcome | undefined>,
43
+ expectedAction?: OnSwitchAction,
44
+ signal?: AbortSignal,
45
+ expectedSetupIdentity?: string,
50
46
  ): Promise<SetupSwitchResult> {
51
- const normalized = name.trim();
52
- if (!normalized) throw new Error("Usage: /sync use <setup>");
53
- const loadedConfig = await loadConfig(normalized);
54
- const reviewedSetupIdentity = expectedSetupIdentity ?? syncConfigReviewIdentity(loadedConfig);
55
- throwIfAborted(signal);
56
- const switchResult: { action: OnSwitchAction; switched: boolean } = {
57
- action: "ask-before-pull",
58
- switched: false,
59
- };
60
- await updateLocalConfig((current) => {
61
- throwIfAborted(signal);
62
- const setup = current.syncSetups[normalized];
63
- if (!setup) {
64
- throw new Error(`Sync setup “${safeTerminalText(normalized)}” no longer exists.`);
65
- }
66
- const connectionName = setup.storage.connection;
67
- const connection = current.storageConnections[connectionName];
68
- if (
69
- !connection ||
70
- syncSetupReviewIdentity(normalized, setup, connectionName, connection) !==
71
- reviewedSetupIdentity
72
- ) {
73
- throw new Error(
74
- `Sync setup “${safeTerminalText(normalized)}” changed while the switch preview was open; reopen it and review the current destination.`,
75
- );
76
- }
77
- switchResult.action = normalizeOnSwitch(current.onSwitch);
78
- if (expectedAction !== undefined && switchResult.action !== expectedAction) {
79
- throw new Error(
80
- "Setup-switch behavior changed while the preview was open; reopen it and retry.",
81
- );
82
- }
83
- if (switchResult.action === "pull-after-switch" && !ctx.hasUI) {
84
- throw new SetupPullRequiresUiError(
85
- `Automatic setup pulls require interactive confirmation; sync setup “${safeTerminalText(normalized)}” was not switched. Use TUI or RPC mode.`,
86
- );
87
- }
88
- if (current.activeSyncSetup === normalized) return current;
89
- switchResult.switched = true;
90
- return { ...current, activeSyncSetup: normalized };
91
- }, signal);
92
- throwIfAborted(signal);
93
- if (!switchResult.switched) {
94
- ctx.ui.notify(`Sync setup “${safeTerminalText(normalized)}” is already current.`, "info");
95
- return { pullApplied: false };
96
- }
97
- setSyncSetupCompletions(await configuredSyncSetupNames());
98
- throwIfAborted(signal);
47
+ const normalized = name.trim();
48
+ if (!normalized) throw new Error("Usage: /sync use <setup>");
49
+ const loadedConfig = await loadConfig(normalized);
50
+ const reviewedSetupIdentity = expectedSetupIdentity ?? syncConfigReviewIdentity(loadedConfig);
51
+ throwIfAborted(signal);
52
+ const switchResult: { action: OnSwitchAction; switched: boolean } = {
53
+ action: "ask-before-pull",
54
+ switched: false,
55
+ };
56
+ await updateLocalConfig((current) => {
57
+ throwIfAborted(signal);
58
+ const setup = current.syncSetups[normalized];
59
+ if (!setup) {
60
+ throw new Error(`Sync setup “${safeTerminalText(normalized)}” no longer exists.`);
61
+ }
62
+ const connectionName = setup.storage.connection;
63
+ const connection = current.storageConnections[connectionName];
64
+ if (
65
+ !connection ||
66
+ syncSetupReviewIdentity(normalized, setup, connectionName, connection) !== reviewedSetupIdentity
67
+ ) {
68
+ throw new Error(
69
+ `Sync setup “${safeTerminalText(normalized)}” changed while the switch preview was open; reopen it and review the current destination.`,
70
+ );
71
+ }
72
+ switchResult.action = normalizeOnSwitch(current.onSwitch);
73
+ if (expectedAction !== undefined && switchResult.action !== expectedAction) {
74
+ throw new Error("Setup-switch behavior changed while the preview was open; reopen it and retry.");
75
+ }
76
+ if (switchResult.action === "pull-after-switch" && !ctx.hasUI) {
77
+ throw new SetupPullRequiresUiError(
78
+ `Automatic setup pulls require interactive confirmation; sync setup “${safeTerminalText(normalized)}” was not switched. Use TUI or RPC mode.`,
79
+ );
80
+ }
81
+ if (current.activeSyncSetup === normalized) return current;
82
+ switchResult.switched = true;
83
+ return { ...current, activeSyncSetup: normalized };
84
+ }, signal);
85
+ throwIfAborted(signal);
86
+ if (!switchResult.switched) {
87
+ ctx.ui.notify(`Sync setup “${safeTerminalText(normalized)}” is already current.`, "info");
88
+ return { pullApplied: false };
89
+ }
90
+ setSyncSetupCompletions(await configuredSyncSetupNames());
91
+ throwIfAborted(signal);
99
92
 
100
- if (switchResult.action === "switch-only") {
101
- ctx.ui.notify(`Switched to “${safeTerminalText(normalized)}”. No files were pulled.`, "info");
102
- return { pullApplied: false };
103
- }
104
- if (switchResult.action === "ask-before-pull") {
105
- if (ctx.mode !== "tui") {
106
- ctx.ui.notify(
107
- `Switched to “${safeTerminalText(normalized)}”. No files were pulled because confirmation requires TUI mode; run /sync pull to apply this setup.`,
108
- "info",
109
- );
110
- return { pullApplied: false };
111
- }
112
- const confirmed = await ctx.ui.confirm(
113
- `Review a pull for sync setup “${safeTerminalText(normalized)}” now?`,
114
- "pi-sync will check the remote snapshot and show the exact local writes and deletions before applying anything.",
115
- { signal },
116
- );
117
- throwIfAborted(signal);
118
- if (!confirmed) {
119
- ctx.ui.notify(
120
- `Switched to “${safeTerminalText(normalized)}”; files were not pulled.`,
121
- "info",
122
- );
123
- return { pullApplied: false };
124
- }
125
- }
93
+ if (switchResult.action === "switch-only") {
94
+ ctx.ui.notify(`Switched to “${safeTerminalText(normalized)}”. No files were pulled.`, "info");
95
+ return { pullApplied: false };
96
+ }
97
+ if (switchResult.action === "ask-before-pull") {
98
+ if (ctx.mode !== "tui") {
99
+ ctx.ui.notify(
100
+ `Switched to “${safeTerminalText(normalized)}”. No files were pulled because confirmation requires TUI mode; run /sync pull to apply this setup.`,
101
+ "info",
102
+ );
103
+ return { pullApplied: false };
104
+ }
105
+ const confirmed = await ctx.ui.confirm(
106
+ `Review a pull for sync setup “${safeTerminalText(normalized)}” now?`,
107
+ "pi-sync will check the remote snapshot and show the exact local writes and deletions before applying anything.",
108
+ { signal },
109
+ );
110
+ throwIfAborted(signal);
111
+ if (!confirmed) {
112
+ ctx.ui.notify(`Switched to “${safeTerminalText(normalized)}”; files were not pulled.`, "info");
113
+ return { pullApplied: false };
114
+ }
115
+ }
126
116
 
127
- ctx.ui.notify(
128
- `Switched to “${safeTerminalText(normalized)}”. Checking remote files for a reviewed pull…`,
129
- "info",
130
- );
131
- if (!pullCurrentSetup) {
132
- throw new Error(`Switched to “${safeTerminalText(normalized)}”, but pull is unavailable.`);
133
- }
134
- const pullOutcome = await pullCurrentSetup(normalized);
135
- throwIfAborted(signal);
136
- if (pullOutcome === "cancelled") {
137
- ctx.ui.notify(
138
- `Pull cancelled; sync setup “${safeTerminalText(normalized)}” remains current and synced files were not changed.`,
139
- "info",
140
- );
141
- }
142
- return { pullApplied: pullOutcome === "applied" };
117
+ ctx.ui.notify(`Switched to “${safeTerminalText(normalized)}”. Checking remote files for a reviewed pull…`, "info");
118
+ if (!pullCurrentSetup) {
119
+ throw new Error(`Switched to “${safeTerminalText(normalized)}”, but pull is unavailable.`);
120
+ }
121
+ const pullOutcome = await pullCurrentSetup(normalized);
122
+ throwIfAborted(signal);
123
+ if (pullOutcome === "cancelled") {
124
+ ctx.ui.notify(
125
+ `Pull cancelled; sync setup “${safeTerminalText(normalized)}” remains current and synced files were not changed.`,
126
+ "info",
127
+ );
128
+ }
129
+ return { pullApplied: pullOutcome === "applied" };
143
130
  }
144
131
 
145
132
  function throwIfAborted(signal?: AbortSignal) {
146
- if (!signal?.aborted) return;
147
- throw signal.reason instanceof Error
148
- ? signal.reason
149
- : new DOMException("The operation was aborted", "AbortError");
133
+ if (!signal?.aborted) return;
134
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
150
135
  }
@@ -1,10 +1,8 @@
1
1
  export function throwIfAborted(signal?: AbortSignal) {
2
- if (!signal?.aborted) return;
3
- throw signal.reason instanceof Error
4
- ? signal.reason
5
- : new DOMException("The operation was aborted", "AbortError");
2
+ if (!signal?.aborted) return;
3
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
6
4
  }
7
5
 
8
6
  export function combineSignals(primary: AbortSignal, secondary?: AbortSignal) {
9
- return secondary ? AbortSignal.any([primary, secondary]) : primary;
7
+ return secondary ? AbortSignal.any([primary, secondary]) : primary;
10
8
  }