@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
@@ -9,113 +9,101 @@ import { createStartupCheck } from "./sync/startup-check.js";
9
9
  import { errorMessage } from "./sync/sync-errors.js";
10
10
  import { createSyncLoaders, type SyncDependencies } from "./sync/sync-loaders.js";
11
11
  import { createSyncAttentionController } from "./ui/sync-attention.js";
12
+ import { setSyncStatus } from "./ui/sync-status.js";
12
13
  import { safeTerminalText } from "./ui/terminal-text.js";
13
14
 
14
- const STATUS_KEY = "sync";
15
-
16
15
  export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDependencies> = {}) {
17
- const loaders = createSyncLoaders(dependencies);
18
- const attention = createSyncAttentionController();
19
- const check = createStartupCheck(loaders, attention);
20
- let sessionAbort = new AbortController();
21
- let shutdownAbort: AbortController | undefined;
22
- let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
16
+ const loaders = createSyncLoaders(dependencies);
17
+ const attention = createSyncAttentionController();
18
+ const check = createStartupCheck(loaders, attention);
19
+ let sessionAbort = new AbortController();
20
+ let shutdownAbort: AbortController | undefined;
21
+ let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
23
22
 
24
- pi.registerCommand("sync", {
25
- description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
26
- getArgumentCompletions: completeSyncArguments,
27
- handler: async (args, ctx) => {
28
- if (!ctx.hasUI) {
29
- throw new Error(
30
- "/sync requires TUI or RPC mode so results and safety prompts are observable.",
31
- );
32
- }
33
- const signal = sessionAbort.signal;
34
- const ready = await initialization;
35
- if (signal.aborted) return;
36
- await check.stop();
37
- if (signal.aborted) return;
38
- const command = splitArgs(args)[0];
39
- if (
40
- ready === false &&
41
- command !== "help" &&
42
- command !== "unlock" &&
43
- command !== "migrate-state"
44
- ) {
45
- ctx.ui.notify(
46
- "pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
47
- "error",
48
- );
49
- return;
50
- }
51
- const run = () => handleCommand(args, ctx, signal, loaders, attention);
52
- // Drain background cache/child cleanup before entering any foreground guard.
53
- // Help must remain reachable when the state roots themselves need repair.
54
- if (command === "migrate-state" || command === "help") await run();
55
- else await withStateDirectoryAccess(run);
56
- },
57
- });
23
+ pi.registerCommand("sync", {
24
+ description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
25
+ getArgumentCompletions: completeSyncArguments,
26
+ handler: async (args, ctx) => {
27
+ if (!ctx.hasUI) {
28
+ throw new Error("/sync requires TUI or RPC mode so results and safety prompts are observable.");
29
+ }
30
+ const signal = sessionAbort.signal;
31
+ const ready = await initialization;
32
+ if (signal.aborted) return;
33
+ await check.stop();
34
+ if (signal.aborted) return;
35
+ const command = splitArgs(args)[0];
36
+ if (ready === false && command !== "help" && command !== "unlock" && command !== "migrate-state") {
37
+ ctx.ui.notify(
38
+ "pi-sync recovery required. Repair the startup error, then /reload before syncing. Use /sync help for recovery guidance.",
39
+ "error",
40
+ );
41
+ return;
42
+ }
43
+ const run = () => handleCommand(args, ctx, signal, loaders, attention);
44
+ // Drain background cache/child cleanup before entering any foreground guard.
45
+ // Help must remain reachable when the state roots themselves need repair.
46
+ if (command === "migrate-state" || command === "help") await run();
47
+ else await withStateDirectoryAccess(run);
48
+ },
49
+ });
58
50
 
59
- pi.on("session_start", async (_event, ctx) => {
60
- shutdownAbort?.abort(new DOMException("Session replaced", "AbortError"));
61
- shutdownAbort = undefined;
62
- sessionAbort.abort(new DOMException("Session replaced", "AbortError"));
63
- sessionAbort = new AbortController();
64
- const signal = sessionAbort.signal;
65
- attention.reset(ctx);
66
- const previous = initialization;
67
- initialization = (async () => {
68
- await previous;
69
- if (signal.aborted) return false;
70
- await check.stop();
71
- if (signal.aborted) return false;
72
- try {
73
- const config = await withStateDirectoryAccess(() => startSession(ctx, signal));
74
- return signal.aborted ? false : config;
75
- } catch (error) {
76
- if (!signal.aborted && ctx.hasUI) {
77
- ctx.ui.notify(
78
- `pi-sync recovery required: ${safeTerminalText(errorMessage(error))}`,
79
- "error",
80
- );
81
- }
82
- return false;
83
- }
84
- })();
85
- const ready = await initialization;
86
- if (ready && !signal.aborted) check.start(ctx, signal, ready);
87
- });
51
+ pi.on("session_start", async (_event, ctx) => {
52
+ shutdownAbort?.abort(new DOMException("Session replaced", "AbortError"));
53
+ shutdownAbort = undefined;
54
+ sessionAbort.abort(new DOMException("Session replaced", "AbortError"));
55
+ sessionAbort = new AbortController();
56
+ const signal = sessionAbort.signal;
57
+ attention.reset(ctx);
58
+ const previous = initialization;
59
+ initialization = (async () => {
60
+ await previous;
61
+ if (signal.aborted) return false;
62
+ await check.stop();
63
+ if (signal.aborted) return false;
64
+ try {
65
+ const config = await withStateDirectoryAccess(() => startSession(ctx, signal));
66
+ return signal.aborted ? false : config;
67
+ } catch (error) {
68
+ if (!signal.aborted && ctx.hasUI) {
69
+ ctx.ui.notify(`pi-sync recovery required: ${safeTerminalText(errorMessage(error))}`, "error");
70
+ }
71
+ return false;
72
+ }
73
+ })();
74
+ const ready = await initialization;
75
+ if (ready && !signal.aborted) check.start(ctx, signal, ready);
76
+ });
88
77
 
89
- pi.on("session_shutdown", async (event, ctx) => {
90
- sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
91
- attention.reset(ctx);
92
- shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError"));
93
- const controller = new AbortController();
94
- shutdownAbort = controller;
95
- const signal = combineSignals(controller.signal, AbortSignal.timeout(30_000));
96
- const reason =
97
- typeof event === "object" && event ? (event as { reason?: string }).reason : undefined;
98
- try {
99
- const ready = await initialization;
100
- if (signal.aborted) return;
101
- await check.stop();
102
- if (signal.aborted) return;
103
- if (ready !== false && reason !== "reload") {
104
- await withStateDirectoryAccess(async () => {
105
- if (signal.aborted) return;
106
- await autoPushSessions(ctx, signal, loaders);
107
- });
108
- }
109
- } catch (error) {
110
- if (!signal.aborted) {
111
- ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
112
- }
113
- } finally {
114
- if (shutdownAbort === controller) shutdownAbort = undefined;
115
- }
116
- if (signal.aborted) return;
117
- ctx.ui.setStatus(STATUS_KEY, undefined);
118
- });
78
+ pi.on("session_shutdown", async (event, ctx) => {
79
+ sessionAbort.abort(new DOMException("Session shut down", "AbortError"));
80
+ attention.reset(ctx);
81
+ shutdownAbort?.abort(new DOMException("Session shut down again", "AbortError"));
82
+ const controller = new AbortController();
83
+ shutdownAbort = controller;
84
+ const signal = combineSignals(controller.signal, AbortSignal.timeout(30_000));
85
+ const reason = typeof event === "object" && event ? (event as { reason?: string }).reason : undefined;
86
+ try {
87
+ const ready = await initialization;
88
+ if (signal.aborted) return;
89
+ await check.stop();
90
+ if (signal.aborted) return;
91
+ if (ready !== false && reason !== "reload") {
92
+ await withStateDirectoryAccess(async () => {
93
+ if (signal.aborted) return;
94
+ await autoPushSessions(ctx, signal, loaders);
95
+ });
96
+ }
97
+ } catch (error) {
98
+ if (!signal.aborted) {
99
+ ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
100
+ }
101
+ } finally {
102
+ if (shutdownAbort === controller) shutdownAbort = undefined;
103
+ }
104
+ if (signal.aborted) return;
105
+ setSyncStatus(ctx, undefined);
106
+ });
119
107
  }
120
108
 
121
109
  export type { SyncDependencies } from "./sync/sync-loaders.js";
package/src/sync.ts CHANGED
@@ -1,38 +1,38 @@
1
1
  export { completeSyncArguments, parseOptions, splitArgs } from "./commands/command.js";
2
2
  export { encodeKey, posixJoin, safeJoin, safeName } from "./paths.js";
3
3
  export {
4
- isEnabled,
5
- isExplicitlyEnabled,
6
- loadConfig,
7
- sessionTokenWarnings,
4
+ isEnabled,
5
+ isExplicitlyEnabled,
6
+ loadConfig,
7
+ sessionTokenWarnings,
8
8
  } from "./settings/config.js";
9
9
  export { isCloudflareR2Endpoint } from "./settings/settings-validation.js";
10
10
  export {
11
- canonicalSnapshotPathForConfig,
12
- collectFiles,
13
- filterSnapshotForConfigPolicy,
14
- isConfiguredSnapshotPath,
15
- isDeniedPath,
16
- isSessionPath,
17
- mergeRemotePreservedFiles,
18
- mergeRemoteSessionFiles,
19
- scanSnapshot,
20
- sessionSnapshotPathFromAbsolute,
21
- snapshotWithoutSessions,
11
+ canonicalSnapshotPathForConfig,
12
+ collectFiles,
13
+ filterSnapshotForConfigPolicy,
14
+ isConfiguredSnapshotPath,
15
+ isDeniedPath,
16
+ isSessionPath,
17
+ mergeRemotePreservedFiles,
18
+ mergeRemoteSessionFiles,
19
+ scanSnapshot,
20
+ sessionSnapshotPathFromAbsolute,
21
+ snapshotWithoutSessions,
22
22
  } from "./snapshot/snapshot.js";
23
23
  export {
24
- addTopLevelCaseVariantDeletes,
25
- appliedFileHashMap,
26
- preflightSnapshotApply,
27
- protectSnapshotApplyPlan,
24
+ addTopLevelCaseVariantDeletes,
25
+ appliedFileHashMap,
26
+ preflightSnapshotApply,
27
+ protectSnapshotApplyPlan,
28
28
  } from "./snapshot/snapshot-apply.js";
29
29
  export {
30
- canPullRemoteSessionsOnFirstSync,
31
- canPullRemoteSettingsOnFirstSync,
32
- hasRemoteChanges,
33
- sessionHashMap,
34
- settingsHashesMatchState,
35
- settingsHashMap,
36
- settingsHashMapFromState,
30
+ canPullRemoteSessionsOnFirstSync,
31
+ canPullRemoteSettingsOnFirstSync,
32
+ hasRemoteChanges,
33
+ sessionHashMap,
34
+ settingsHashesMatchState,
35
+ settingsHashMap,
36
+ settingsHashMapFromState,
37
37
  } from "./sync/sync-state.js";
38
38
  export { default, type SyncDependencies } from "./sync-extension.js";
package/src/types.ts CHANGED
@@ -1,47 +1,47 @@
1
1
  // Compatibility type surface; implementation modules import from the owning domain.
2
2
 
3
3
  export type {
4
- LatestPointer,
5
- RemoteObject,
6
- ResolvedGitBackend,
7
- ResolvedGitDestination,
8
- ResolvedGitStorageProfile,
9
- ResolvedS3Backend,
10
- ResolvedS3Destination,
11
- ResolvedS3StorageProfile,
12
- ResolvedSyncBackend,
13
- ResolvedWebDavBackend,
14
- ResolvedWebDavDestination,
15
- ResolvedWebDavStorageProfile,
4
+ LatestPointer,
5
+ RemoteObject,
6
+ ResolvedGitBackend,
7
+ ResolvedGitDestination,
8
+ ResolvedGitStorageProfile,
9
+ ResolvedS3Backend,
10
+ ResolvedS3Destination,
11
+ ResolvedS3StorageProfile,
12
+ ResolvedSyncBackend,
13
+ ResolvedWebDavBackend,
14
+ ResolvedWebDavDestination,
15
+ ResolvedWebDavStorageProfile,
16
16
  } from "./backends/backend-types.js";
17
17
  export type { CommandArgumentCompletion, CommandOptions } from "./commands/command-types.js";
18
18
  export type {
19
- AnySyncConfig,
20
- CommonSyncConfig,
21
- CommonSyncSetupStorageSettings,
22
- GitStorageConnectionSettings,
23
- GitSyncSetupStorageSettings,
24
- OnSwitchAction,
25
- PartialConfig,
26
- PiSyncSettingsV3,
27
- S3CredentialsSettings,
28
- S3StorageConnectionSettings,
29
- S3SyncSetupStorageSettings,
30
- StorageConnectionSettings,
31
- StorageConnectionType,
32
- SyncConfig,
33
- SyncPolicySettings,
34
- SyncSetupSettings,
35
- SyncSetupStorageSettings,
36
- WebDavCredentialsSettings,
37
- WebDavStorageConnectionSettings,
38
- WebDavSyncSetupStorageSettings,
19
+ AnySyncConfig,
20
+ CommonSyncConfig,
21
+ CommonSyncSetupStorageSettings,
22
+ GitStorageConnectionSettings,
23
+ GitSyncSetupStorageSettings,
24
+ OnSwitchAction,
25
+ PartialConfig,
26
+ PiSyncSettingsV3,
27
+ S3CredentialsSettings,
28
+ S3StorageConnectionSettings,
29
+ S3SyncSetupStorageSettings,
30
+ StorageConnectionSettings,
31
+ StorageConnectionType,
32
+ SyncConfig,
33
+ SyncPolicySettings,
34
+ SyncSetupSettings,
35
+ SyncSetupStorageSettings,
36
+ WebDavCredentialsSettings,
37
+ WebDavStorageConnectionSettings,
38
+ WebDavSyncSetupStorageSettings,
39
39
  } from "./settings/settings-types.js";
40
40
  export type {
41
- Snapshot,
42
- SnapshotApplyPlan,
43
- SnapshotFile,
44
- SnapshotOptions,
45
- SnapshotSelection,
41
+ Snapshot,
42
+ SnapshotApplyPlan,
43
+ SnapshotFile,
44
+ SnapshotOptions,
45
+ SnapshotSelection,
46
46
  } from "./snapshot/snapshot-types.js";
47
47
  export type { LockFile, SyncState } from "./state/state-types.js";
@@ -1,6 +1,4 @@
1
1
  /** Keep the setting's automatic transfer scope visible in every setup summary. */
2
2
  export function automaticSyncSummary(enabled: boolean) {
3
- return enabled
4
- ? "On (startup check; shutdown pushes selected content if sessions included)"
5
- : "Off";
3
+ return enabled ? "On (startup check; shutdown pushes selected content if sessions included)" : "Off";
6
4
  }
@@ -6,118 +6,106 @@ import type { SyncDecision } from "../sync/sync-decision.js";
6
6
  import type { RemoteSelectionDecision } from "../sync/sync-policy.js";
7
7
 
8
8
  export type RunRouteResult =
9
- | { kind: "completed"; outcome?: SetupPullOutcome }
10
- | { kind: "decision-required"; decision: SyncDecision }
11
- | { kind: "remote-selection-required"; decision: RemoteSelectionDecision }
12
- | { kind: "failed" };
9
+ | { kind: "completed"; outcome?: SetupPullOutcome }
10
+ | { kind: "decision-required"; decision: SyncDecision }
11
+ | { kind: "remote-selection-required"; decision: RemoteSelectionDecision }
12
+ | { kind: "failed" };
13
13
 
14
14
  export type RunRoute = (
15
- route: string,
16
- signal?: AbortSignal,
17
- onCommit?: () => void,
18
- target?: string,
15
+ route: string,
16
+ signal?: AbortSignal,
17
+ onCommit?: () => void,
18
+ target?: string,
19
19
  ) => Promise<RunRouteResult | undefined>;
20
20
 
21
- export type CancellableOperationResult =
22
- | RunRouteResult
23
- | { kind: "closed" }
24
- | { kind: "cancelled" };
21
+ export type CancellableOperationResult = RunRouteResult | { kind: "closed" } | { kind: "cancelled" };
25
22
 
26
23
  interface CancellableOperationOptions {
27
- commitAware?: boolean;
28
- cancelledMessage?: string | null;
29
- target?: string;
30
- signal?: AbortSignal;
24
+ commitAware?: boolean;
25
+ cancelledMessage?: string | null;
26
+ target?: string;
27
+ signal?: AbortSignal;
31
28
  }
32
29
 
33
30
  export async function runCancellableOperation(
34
- ctx: ExtensionContext,
35
- message: string,
36
- route: string,
37
- runRoute: RunRoute,
38
- options: CancellableOperationOptions = {},
31
+ ctx: ExtensionContext,
32
+ message: string,
33
+ route: string,
34
+ runRoute: RunRoute,
35
+ options: CancellableOperationOptions = {},
39
36
  ): Promise<CancellableOperationResult> {
40
- const {
41
- commitAware = false,
42
- cancelledMessage = "Check cancelled; no settings or files were changed.",
43
- target,
44
- signal,
45
- } = options;
46
- if (ctx.mode !== "tui") {
47
- return (await runRoute(route, signal, undefined, target)) ?? { kind: "failed" };
48
- }
49
- let commitStarted = false;
50
- let routeResult: RunRouteResult | undefined;
51
- const interaction = await runCustomInteraction<
52
- { cancelled?: boolean; error?: unknown },
53
- ExtensionContext
54
- >(ctx, {
55
- signal,
56
- isCurrent: () => !signal?.aborted,
57
- create: ({ tui, theme, keybindings, signal: interactionSignal, complete }) => {
58
- const loader = new BorderedLoader(tui, theme, message, { cancellable: false });
59
- const cancelHint = formatInteractionHints(keybindings, [
60
- {
61
- keys: [
62
- ...keybindings.getKeys("tui.select.cancel").filter((key) => !hasControlCharacter(key)),
63
- "ctrl+c",
64
- ],
65
- label: "cancel",
66
- },
67
- ]);
68
- const operation = runRoute(
69
- route,
70
- interactionSignal,
71
- commitAware ? () => (commitStarted = true) : undefined,
72
- target,
73
- ).then(
74
- (result) => {
75
- routeResult = result;
76
- complete({});
77
- },
78
- (error: unknown) => complete({ error }),
79
- );
80
- return {
81
- render(width: number) {
82
- const safeWidth = Math.max(1, width);
83
- const lines = loader.render(safeWidth);
84
- const bottomBorder = lines.at(-1);
85
- return [
86
- ...lines.slice(0, -1),
87
- truncateToWidth(theme.fg("dim", cancelHint), safeWidth, ""),
88
- ...(bottomBorder === undefined ? [] : [bottomBorder]),
89
- ];
90
- },
91
- invalidate: () => loader.invalidate(),
92
- handleInput(data: string) {
93
- if (!matchesKey(data, Key.ctrl("c")) && !keybindings.matches(data, "tui.select.cancel")) {
94
- return;
95
- }
96
- if (commitStarted) {
97
- ctx.ui.notify(
98
- "Applying or publishing has started and cannot be cancelled safely.",
99
- "warning",
100
- );
101
- return;
102
- }
103
- complete({ cancelled: true });
104
- },
105
- dispose: () => loader.dispose(),
106
- waitForPending: () => operation,
107
- };
108
- },
109
- });
110
- if (interaction.kind === "error") throw interaction.error;
111
- if (interaction.kind !== "completed") return { kind: "closed" };
112
- if (interaction.value.cancelled) {
113
- if (cancelledMessage) ctx.ui.notify(cancelledMessage, "info");
114
- return { kind: "cancelled" };
115
- }
116
- if (interaction.value.error) throw interaction.value.error;
117
- return routeResult ?? { kind: "failed" };
37
+ const {
38
+ commitAware = false,
39
+ cancelledMessage = "Check cancelled; no settings or files were changed.",
40
+ target,
41
+ signal,
42
+ } = options;
43
+ if (ctx.mode !== "tui") {
44
+ return (await runRoute(route, signal, undefined, target)) ?? { kind: "failed" };
45
+ }
46
+ let commitStarted = false;
47
+ let routeResult: RunRouteResult | undefined;
48
+ const interaction = await runCustomInteraction<{ cancelled?: boolean; error?: unknown }, ExtensionContext>(ctx, {
49
+ signal,
50
+ isCurrent: () => !signal?.aborted,
51
+ create: ({ tui, theme, keybindings, signal: interactionSignal, complete }) => {
52
+ const loader = new BorderedLoader(tui, theme, message, { cancellable: false });
53
+ const cancelHint = formatInteractionHints(keybindings, [
54
+ {
55
+ keys: [...keybindings.getKeys("tui.select.cancel").filter((key) => !hasControlCharacter(key)), "ctrl+c"],
56
+ label: "cancel",
57
+ },
58
+ ]);
59
+ const operation = runRoute(
60
+ route,
61
+ interactionSignal,
62
+ commitAware ? () => (commitStarted = true) : undefined,
63
+ target,
64
+ ).then(
65
+ (result) => {
66
+ routeResult = result;
67
+ complete({});
68
+ },
69
+ (error: unknown) => complete({ error }),
70
+ );
71
+ return {
72
+ render(width: number) {
73
+ const safeWidth = Math.max(1, width);
74
+ const lines = loader.render(safeWidth);
75
+ const bottomBorder = lines.at(-1);
76
+ return [
77
+ ...lines.slice(0, -1),
78
+ truncateToWidth(theme.fg("dim", cancelHint), safeWidth, ""),
79
+ ...(bottomBorder === undefined ? [] : [bottomBorder]),
80
+ ];
81
+ },
82
+ invalidate: () => loader.invalidate(),
83
+ handleInput(data: string) {
84
+ if (!matchesKey(data, Key.ctrl("c")) && !keybindings.matches(data, "tui.select.cancel")) {
85
+ return;
86
+ }
87
+ if (commitStarted) {
88
+ ctx.ui.notify("Applying or publishing has started and cannot be cancelled safely.", "warning");
89
+ return;
90
+ }
91
+ complete({ cancelled: true });
92
+ },
93
+ dispose: () => loader.dispose(),
94
+ waitForPending: () => operation,
95
+ };
96
+ },
97
+ });
98
+ if (interaction.kind === "error") throw interaction.error;
99
+ if (interaction.kind !== "completed") return { kind: "closed" };
100
+ if (interaction.value.cancelled) {
101
+ if (cancelledMessage) ctx.ui.notify(cancelledMessage, "info");
102
+ return { kind: "cancelled" };
103
+ }
104
+ if (interaction.value.error) throw interaction.value.error;
105
+ return routeResult ?? { kind: "failed" };
118
106
  }
119
107
 
120
108
  function hasControlCharacter(value: string) {
121
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Unsafe key labels must be omitted.
122
- return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
109
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Unsafe key labels must be omitted.
110
+ return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
123
111
  }