@narumitw/pi-sync 0.49.15 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +49 -16
  2. package/dist/chunks/chunk-2B2ELUF5.ts +29 -0
  3. package/dist/chunks/chunk-2B2ELUF5.ts.map +7 -0
  4. package/dist/chunks/chunk-6LP3MCR2.ts +115 -0
  5. package/dist/chunks/chunk-6LP3MCR2.ts.map +7 -0
  6. package/dist/chunks/chunk-6UHKGALE.ts +134 -0
  7. package/dist/chunks/chunk-6UHKGALE.ts.map +7 -0
  8. package/dist/chunks/{chunk-JF6MZCT4.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-X7SQ2EQM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-5T442OP3.ts → chunk-C7XY5NF5.ts} +210 -373
  13. package/dist/chunks/chunk-C7XY5NF5.ts.map +7 -0
  14. package/dist/chunks/{chunk-4U6AFLJB.ts → chunk-CHH26D4F.ts} +3 -2
  15. package/dist/chunks/chunk-CHH26D4F.ts.map +7 -0
  16. package/dist/chunks/chunk-CPEHGXJV.ts +50 -0
  17. package/dist/chunks/chunk-CPEHGXJV.ts.map +7 -0
  18. package/dist/chunks/{chunk-5MQ76BK5.ts → chunk-D5X27LCA.ts} +2 -2
  19. package/dist/chunks/chunk-D5X27LCA.ts.map +7 -0
  20. package/dist/chunks/{chunk-WSOBYHIZ.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-5LZALPCD.ts → chunk-HUDQATCL.ts} +25 -25
  23. package/dist/chunks/chunk-HUDQATCL.ts.map +7 -0
  24. package/dist/chunks/{chunk-YQ6UW7IF.ts → chunk-JAAKLL6M.ts} +2 -2
  25. package/dist/chunks/chunk-JAAKLL6M.ts.map +7 -0
  26. package/dist/chunks/chunk-JSYMGJQQ.ts +51 -0
  27. package/dist/chunks/chunk-JSYMGJQQ.ts.map +7 -0
  28. package/dist/chunks/chunk-L7TBO32M.ts +25 -0
  29. package/dist/chunks/chunk-L7TBO32M.ts.map +7 -0
  30. package/dist/chunks/{chunk-DFXBKHGN.ts → chunk-LCRI3ABW.ts} +21 -8
  31. package/dist/chunks/chunk-LCRI3ABW.ts.map +7 -0
  32. package/dist/chunks/chunk-LTBWGSBV.ts +12 -0
  33. package/dist/chunks/chunk-LTBWGSBV.ts.map +7 -0
  34. package/dist/chunks/chunk-NJFUIVYL.ts +17 -0
  35. package/dist/chunks/chunk-NJFUIVYL.ts.map +7 -0
  36. package/dist/chunks/{chunk-HVUSHMVL.ts → chunk-OEX4ABY6.ts} +39 -37
  37. package/dist/chunks/chunk-OEX4ABY6.ts.map +7 -0
  38. package/dist/chunks/chunk-OUHGBFYF.ts +1255 -0
  39. package/dist/chunks/chunk-OUHGBFYF.ts.map +7 -0
  40. package/dist/chunks/{chunk-HGV24P4T.ts → chunk-Q2TUM5X6.ts} +3 -3
  41. package/dist/chunks/chunk-Q2TUM5X6.ts.map +7 -0
  42. package/dist/chunks/{chunk-GLQLN5GV.ts → chunk-SL64RLXR.ts} +8 -6
  43. package/dist/chunks/chunk-SL64RLXR.ts.map +7 -0
  44. package/dist/chunks/chunk-T3HH3UEJ.ts +96 -0
  45. package/dist/chunks/chunk-T3HH3UEJ.ts.map +7 -0
  46. package/dist/chunks/chunk-TEN2ZBJV.ts +101 -0
  47. package/dist/chunks/chunk-TEN2ZBJV.ts.map +7 -0
  48. package/dist/chunks/chunk-THMF4BVL.ts +18 -0
  49. package/dist/chunks/chunk-THMF4BVL.ts.map +7 -0
  50. package/dist/chunks/{chunk-44LPGQTI.ts → chunk-X5OM6GXT.ts} +9 -7
  51. package/dist/chunks/chunk-X5OM6GXT.ts.map +7 -0
  52. package/dist/chunks/chunk-XBX4HBLG.ts +57 -0
  53. package/dist/chunks/chunk-XBX4HBLG.ts.map +7 -0
  54. package/dist/chunks/{chunk-ARN2672F.ts → chunk-YKJHPTGO.ts} +23 -55
  55. package/dist/chunks/chunk-YKJHPTGO.ts.map +7 -0
  56. package/dist/chunks/chunk-Z4DQF6L3.ts +12 -0
  57. package/dist/chunks/chunk-Z4DQF6L3.ts.map +7 -0
  58. package/dist/chunks/{chunk-F5QL2GPY.ts → chunk-ZI5MXMEX.ts} +5 -99
  59. package/dist/chunks/chunk-ZI5MXMEX.ts.map +7 -0
  60. package/dist/chunks/{file-selection-RKEHALNQ.ts → file-selection-FTTKAOYX.ts} +10 -7
  61. package/dist/chunks/file-selection-FTTKAOYX.ts.map +7 -0
  62. package/dist/chunks/{git-backend-E3UOUUOM.ts → git-backend-OQW46G2X.ts} +42 -17
  63. package/dist/chunks/git-backend-OQW46G2X.ts.map +7 -0
  64. package/dist/chunks/manager-result-dispatcher-U23ODD32.ts +22 -0
  65. package/dist/chunks/manager-ui-ABATHC36.ts +1680 -0
  66. package/dist/chunks/manager-ui-ABATHC36.ts.map +7 -0
  67. package/dist/chunks/remote-selection-ui-ZE3TY67S.ts +21 -0
  68. package/dist/chunks/{s3-backend-552LXHE4.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-5YDG5YGJ.ts → setup-switch-IS2PEZ3J.ts} +9 -9
  71. package/dist/chunks/setup-wizard-VB3VDOV5.ts +89 -0
  72. package/dist/chunks/setup-wizard-VB3VDOV5.ts.map +7 -0
  73. package/dist/chunks/{snapshot-2RYBUDRA.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-3LLIHWZO.ts → sync-operations-ONJ27QRL.ts} +374 -294
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-SRN6YRBK.ts → sync-state-Q2FTXTW6.ts} +6 -6
  78. package/dist/chunks/sync-state-Q2FTXTW6.ts.map +7 -0
  79. package/dist/chunks/{webdav-backend-ZD5RHRIU.ts → webdav-backend-ECIKD7SO.ts} +26 -13
  80. package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +7 -0
  81. package/dist/index.ts +477 -342
  82. package/dist/index.ts.map +4 -4
  83. package/docs/settings.md +30 -6
  84. package/package.json +1 -1
  85. package/src/{backend-factory.ts → backends/backend-factory.ts} +4 -4
  86. package/src/backends/backend-identity.ts +40 -0
  87. package/src/backends/backend-types.ts +81 -0
  88. package/src/{git-backend.ts → backends/git/git-backend.ts} +43 -19
  89. package/src/{git-config.ts → backends/git/git-config.ts} +1 -0
  90. package/src/{git-storage.ts → backends/git/git-storage.ts} +2 -2
  91. package/src/{s3-backend.ts → backends/s3/s3-backend.ts} +45 -9
  92. package/src/{s3-client.ts → backends/s3/s3-client.ts} +56 -8
  93. package/src/{sync-backend.ts → backends/sync-backend.ts} +1 -1
  94. package/src/{webdav-backend.ts → backends/webdav/webdav-backend.ts} +22 -10
  95. package/src/{webdav-client.ts → backends/webdav/webdav-client.ts} +1 -1
  96. package/src/{webdav-config.ts → backends/webdav/webdav-config.ts} +1 -0
  97. package/src/commands/command-execution.ts +330 -0
  98. package/src/commands/command-handler.ts +201 -0
  99. package/src/commands/command-types.ts +18 -0
  100. package/src/{command.ts → commands/command.ts} +2 -1
  101. package/src/commands/setup-completions.ts +6 -0
  102. package/src/settings/config.ts +238 -0
  103. package/src/{settings-management.ts → settings/settings-management.ts} +39 -14
  104. package/src/settings/settings-store.ts +109 -0
  105. package/src/settings/settings-types.ts +128 -0
  106. package/src/settings/settings-validation.ts +443 -0
  107. package/src/snapshot/session-paths.ts +84 -0
  108. package/src/{snapshot-apply.ts → snapshot/snapshot-apply.ts} +3 -3
  109. package/src/{snapshot-codec.ts → snapshot/snapshot-codec.ts} +2 -2
  110. package/src/{snapshot-transaction.ts → snapshot/snapshot-transaction.ts} +5 -4
  111. package/src/snapshot/snapshot-types.ts +38 -0
  112. package/src/{snapshot.ts → snapshot/snapshot.ts} +39 -20
  113. package/src/state/json-file.ts +20 -0
  114. package/src/{lock.ts → state/lock.ts} +3 -2
  115. package/src/{operation-availability.ts → state/operation-availability.ts} +2 -2
  116. package/src/state/state-types.ts +20 -0
  117. package/src/state/sync-state-store.ts +62 -0
  118. package/src/sync/automatic-sync.ts +103 -0
  119. package/src/{remote-snapshot.ts → sync/remote-snapshot.ts} +27 -4
  120. package/src/{setup-switch.ts → sync/setup-switch.ts} +6 -7
  121. package/src/sync/signals.ts +10 -0
  122. package/src/sync/startup-check.ts +128 -0
  123. package/src/{sync-decision.ts → sync/sync-decision.ts} +6 -3
  124. package/src/sync/sync-error-guidance.ts +53 -0
  125. package/src/sync/sync-inspection.ts +51 -0
  126. package/src/sync/sync-loaders.ts +54 -0
  127. package/src/{sync-operations.ts → sync/sync-mutations.ts} +34 -325
  128. package/src/sync/sync-operations.ts +12 -0
  129. package/src/{sync-policy.ts → sync/sync-policy.ts} +2 -2
  130. package/src/sync/sync-queries.ts +235 -0
  131. package/src/{sync-state.ts → sync/sync-state.ts} +5 -4
  132. package/src/sync-extension.ts +64 -708
  133. package/src/sync.ts +8 -8
  134. package/src/types.ts +47 -286
  135. package/src/ui/automatic-sync-summary.ts +6 -0
  136. package/src/{cancellable-operation.ts → ui/cancellable-operation.ts} +4 -4
  137. package/src/{file-selection.ts → ui/file-selection.ts} +5 -3
  138. package/src/{manager-attention.ts → ui/manager-attention.ts} +5 -1
  139. package/src/{manager-recovery.ts → ui/manager-recovery.ts} +6 -2
  140. package/src/{manager-state.ts → ui/manager-state.ts} +35 -14
  141. package/src/ui/manager-ui.ts +386 -0
  142. package/src/{remote-selection-ui.ts → ui/remote-selection-ui.ts} +21 -13
  143. package/src/{secret-input.ts → ui/secret-input.ts} +22 -4
  144. package/src/{settings-ui.ts → ui/settings-ui.ts} +17 -13
  145. package/src/ui/setup/git-ui.ts +293 -0
  146. package/src/{s3-credentials-ui.ts → ui/setup/s3-credentials-ui.ts} +13 -21
  147. package/src/ui/setup/s3-ui.ts +217 -0
  148. package/src/ui/setup/setup-actions.ts +127 -0
  149. package/src/ui/setup/setup-location-ui.ts +167 -0
  150. package/src/ui/setup/setup-name-ui.ts +34 -0
  151. package/src/ui/setup/setup-prompts.ts +92 -0
  152. package/src/ui/setup/setup-review.ts +70 -0
  153. package/src/ui/setup/setup-switcher.ts +181 -0
  154. package/src/ui/setup/setup-wizard.ts +39 -0
  155. package/src/ui/setup/text-input.ts +78 -0
  156. package/src/ui/setup/webdav-ui.ts +330 -0
  157. package/src/{storage-connections-ui.ts → ui/storage-connections-ui.ts} +99 -69
  158. package/src/{sync-attention.ts → ui/sync-attention.ts} +75 -6
  159. package/src/{sync-format.ts → ui/sync-format.ts} +7 -13
  160. package/src/{sync-resolution-ui.ts → ui/sync-resolution-ui.ts} +4 -3
  161. package/src/{sync-setups-ui.ts → ui/sync-setups-ui.ts} +23 -10
  162. package/src/ui/terminal-text.ts +4 -0
  163. package/dist/chunks/chunk-44LPGQTI.ts.map +0 -7
  164. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  165. package/dist/chunks/chunk-5LZALPCD.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5T442OP3.ts.map +0 -7
  168. package/dist/chunks/chunk-ARN2672F.ts.map +0 -7
  169. package/dist/chunks/chunk-C7LXZ3UH.ts +0 -167
  170. package/dist/chunks/chunk-C7LXZ3UH.ts.map +0 -7
  171. package/dist/chunks/chunk-DFXBKHGN.ts.map +0 -7
  172. package/dist/chunks/chunk-E4COSV32.ts +0 -82
  173. package/dist/chunks/chunk-E4COSV32.ts.map +0 -7
  174. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  175. package/dist/chunks/chunk-GLQLN5GV.ts.map +0 -7
  176. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  177. package/dist/chunks/chunk-HVUSHMVL.ts.map +0 -7
  178. package/dist/chunks/chunk-JF6MZCT4.ts.map +0 -7
  179. package/dist/chunks/chunk-WSOBYHIZ.ts.map +0 -7
  180. package/dist/chunks/chunk-X7SQ2EQM.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-RKEHALNQ.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-LWPYJAD5.ts +0 -18
  185. package/dist/chunks/manager-ui-NF4RUHBY.ts +0 -2785
  186. package/dist/chunks/manager-ui-NF4RUHBY.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-NUF5P4ZJ.ts +0 -17
  188. package/dist/chunks/s3-backend-552LXHE4.ts.map +0 -7
  189. package/dist/chunks/sync-operations-3LLIHWZO.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -923
  192. package/src/git-ui.ts +0 -307
  193. package/src/manager-helpers.ts +0 -64
  194. package/src/manager-ui.ts +0 -997
  195. package/src/webdav-ui.ts +0 -421
  196. /package/dist/chunks/{manager-result-dispatcher-LWPYJAD5.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-NUF5P4ZJ.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-5YDG5YGJ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-2RYBUDRA.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-SRN6YRBK.ts.map → sync-inspection-FEP4L5TR.ts.map} +0 -0
  201. /package/src/{git-runner.ts → backends/git/git-runner.ts} +0 -0
  202. /package/src/{config-errors.ts → settings/config-errors.ts} +0 -0
  203. /package/src/{config-file.ts → settings/config-file.ts} +0 -0
  204. /package/src/{snapshot-paths.ts → snapshot/snapshot-paths.ts} +0 -0
  205. /package/src/{lock-policy.ts → state/lock-policy.ts} +0 -0
  206. /package/src/{lockfile-fs.ts → state/lockfile-fs.ts} +0 -0
  207. /package/src/{state-directory.ts → state/state-directory.ts} +0 -0
  208. /package/src/{sync-errors.ts → sync/sync-errors.ts} +0 -0
  209. /package/src/{manager-result-dispatcher.ts → ui/manager-result-dispatcher.ts} +0 -0
@@ -1,103 +1,25 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
6
- import type { RunRouteResult } from "./cancellable-operation.js";
7
- import {
8
- completeSyncArguments,
9
- parseOptions,
10
- resolveSyncCommand,
11
- setSyncSetupCompletions,
12
- splitArgs,
13
- usage,
14
- validateCommandOptions,
15
- } from "./command.js";
16
- import {
17
- activeLocalConfigPath,
18
- configuredSyncSetupNames,
19
- consumeLocalConfigMigrationNotice,
20
- createLocalConfigDocument,
21
- ensureStateDir,
22
- isMissingConfigError,
23
- loadConfig,
24
- loadPartialConfig,
25
- localConfigPath,
26
- localConfigTemplate,
27
- readLocalConfigObject,
28
- readStateForConfig,
29
- sessionTokenWarnings,
30
- syncSessionsWarnings,
31
- } from "./config.js";
32
- import { unlock, withLock } from "./lock.js";
33
- import { recoverSnapshotTransactionsOnStartup } from "./snapshot-transaction.js";
34
- import {
35
- migrateLegacyStateDirectory,
36
- stateDirectoryMigrationNotice,
37
- withStateDirectoryAccess,
38
- } from "./state-directory.js";
39
- import {
40
- createSyncAttentionController,
41
- type SyncAttentionController,
42
- type SyncAttentionOrigin,
43
- syncAttentionMatchesConfig,
44
- } from "./sync-attention.js";
45
- import {
46
- errorMessage,
47
- isSyncDecisionRequiredError,
48
- SetupPullRequiresUiError,
49
- } from "./sync-errors.js";
50
- import {
51
- formatRemoteSelectionMismatch,
52
- type RemoteSelectionDecision,
53
- RemoteSelectionMismatchError,
54
- } from "./sync-policy.js";
55
- import type { AnySyncConfig, CommandOptions, SnapshotOptions } from "./types.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { completeSyncArguments, splitArgs } from "./commands/command.js";
3
+ import { handleCommand } from "./commands/command-handler.js";
4
+ import type { AnySyncConfig } from "./settings/settings-types.js";
5
+ import { withStateDirectoryAccess } from "./state/state-directory.js";
6
+ import { autoPushSessions, startSession } from "./sync/automatic-sync.js";
7
+ import { combineSignals } from "./sync/signals.js";
8
+ import { createStartupCheck } from "./sync/startup-check.js";
9
+ import { errorMessage } from "./sync/sync-errors.js";
10
+ import { createSyncLoaders, type SyncDependencies } from "./sync/sync-loaders.js";
11
+ import { createSyncAttentionController } from "./ui/sync-attention.js";
12
+ import { safeTerminalText } from "./ui/terminal-text.js";
56
13
 
57
14
  const STATUS_KEY = "sync";
58
15
 
59
- type SetupSwitchModule = Pick<typeof import("./setup-switch.js"), "useSyncSetup">;
60
- type SnapshotModule = Pick<typeof import("./snapshot.js"), "createSnapshot">;
61
- type SyncStateModule = Pick<typeof import("./sync-state.js"), "hasLocalChanges">;
62
- type SyncOperations = typeof import("./sync-operations.js");
63
-
64
- export interface SyncDependencies {
65
- loadSetupSwitch(): Promise<SetupSwitchModule>;
66
- loadSnapshot(): Promise<SnapshotModule>;
67
- loadSyncState(): Promise<SyncStateModule>;
68
- loadSyncOperations(): Promise<SyncOperations>;
69
- }
70
-
71
- interface SyncLoaders {
72
- setupSwitch(): Promise<SetupSwitchModule>;
73
- snapshot(): Promise<SnapshotModule>;
74
- syncState(): Promise<SyncStateModule>;
75
- operations(): Promise<SyncOperations>;
76
- }
77
-
78
- const AUTO_SYNC_OPTIONS: CommandOptions = {
79
- yes: true,
80
- force: false,
81
- stale: false,
82
- silent: true,
83
- reload: false,
84
- auto: true,
85
- args: [],
86
- };
87
16
  export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDependencies> = {}) {
88
- const loaders: SyncLoaders = {
89
- setupSwitch: cachedModuleLoader(
90
- dependencies.loadSetupSwitch ?? (() => import("./setup-switch.js")),
91
- ),
92
- snapshot: cachedModuleLoader(dependencies.loadSnapshot ?? (() => import("./snapshot.js"))),
93
- syncState: cachedModuleLoader(dependencies.loadSyncState ?? (() => import("./sync-state.js"))),
94
- operations: cachedModuleLoader(
95
- dependencies.loadSyncOperations ?? (() => import("./sync-operations.js")),
96
- ),
97
- };
17
+ const loaders = createSyncLoaders(dependencies);
98
18
  const attention = createSyncAttentionController();
19
+ const check = createStartupCheck(loaders, attention);
99
20
  let sessionAbort = new AbortController();
100
21
  let shutdownAbort: AbortController | undefined;
22
+ let initialization = Promise.resolve<AnySyncConfig | false | undefined>(undefined);
101
23
 
102
24
  pi.registerCommand("sync", {
103
25
  description: "Sync Pi settings through Git, WebDAV, R2, or S3-compatible storage",
@@ -108,8 +30,28 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
108
30
  "/sync requires TUI or RPC mode so results and safety prompts are observable.",
109
31
  );
110
32
  }
111
- const run = () => handleCommand(args, ctx, sessionAbort.signal, loaders, attention);
112
- if (splitArgs(args)[0] === "migrate-state") await run();
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();
113
55
  else await withStateDirectoryAccess(run);
114
56
  },
115
57
  });
@@ -121,32 +63,27 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
121
63
  sessionAbort = new AbortController();
122
64
  const signal = sessionAbort.signal;
123
65
  attention.reset(ctx);
124
- let decision: RemoteSelectionDecision | undefined;
125
- try {
126
- decision = await withStateDirectoryAccess(() => startSession(ctx, signal, loaders));
127
- } catch (error) {
128
- if (signal.aborted) return;
129
- ctx.ui.notify(`pi-sync state access failed: ${errorMessage(error)}`, "error");
130
- return;
131
- }
132
- if (!decision || signal.aborted) return;
133
- attention.set(decision, "sync");
134
- if (ctx.mode !== "tui") {
135
- ctx.ui.notify(
136
- `pi-sync auto sync skipped: ${formatRemoteSelectionMismatch(
137
- decision.setupName,
138
- decision.localInclude,
139
- decision.remoteInclude,
140
- )}\nRPC review is read-only.`,
141
- "warning",
142
- );
143
- } else if (attention.markOffered()) {
144
- await resolveSelectionAttention(ctx, attention, signal, loaders, {
145
- cancelLabel: "Later",
146
- withStateAccess: withStateDirectoryAccess,
147
- });
148
- }
149
- if (!signal.aborted) attention.publish(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);
150
87
  });
151
88
 
152
89
  pi.on("session_shutdown", async (event, ctx) => {
@@ -159,7 +96,11 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
159
96
  const reason =
160
97
  typeof event === "object" && event ? (event as { reason?: string }).reason : undefined;
161
98
  try {
162
- if (reason !== "reload") {
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") {
163
104
  await withStateDirectoryAccess(async () => {
164
105
  if (signal.aborted) return;
165
106
  await autoPushSessions(ctx, signal, loaders);
@@ -177,589 +118,4 @@ export default function sync(pi: ExtensionAPI, dependencies: Partial<SyncDepende
177
118
  });
178
119
  }
179
120
 
180
- async function startSession(ctx: ExtensionContext, signal: AbortSignal, loaders: SyncLoaders) {
181
- if (signal.aborted) return;
182
- try {
183
- const migrationNotice = stateDirectoryMigrationNotice();
184
- if (migrationNotice) ctx.ui.notify(migrationNotice, "warning");
185
- } catch (error) {
186
- ctx.ui.notify(`pi-sync state directory requires attention: ${errorMessage(error)}`, "error");
187
- return;
188
- }
189
- try {
190
- await recoverSnapshotTransactionsOnStartup();
191
- if (signal.aborted) return;
192
- } catch (error) {
193
- if (signal.aborted) return;
194
- ctx.ui.notify(`pi-sync recovery required: ${errorMessage(error)}`, "error");
195
- return;
196
- }
197
- try {
198
- setSyncSetupCompletions(await configuredSyncSetupNames());
199
- if (signal.aborted) return;
200
- } catch {
201
- if (signal.aborted) return;
202
- setSyncSetupCompletions([]);
203
- }
204
- const migrationNotice = consumeLocalConfigMigrationNotice();
205
- if (migrationNotice) ctx.ui.notify(migrationNotice, "warning");
206
- if (signal.aborted) return;
207
- return autoSync(ctx, signal, loaders);
208
- }
209
-
210
- async function handleCommand(
211
- rawArgs: string,
212
- ctx: ExtensionCommandContext,
213
- sessionSignal: AbortSignal,
214
- loaders: SyncLoaders,
215
- attention: SyncAttentionController,
216
- ) {
217
- if (!rawArgs.trim()) {
218
- try {
219
- const { showSyncManager } = await import("./manager-ui.js");
220
- if (sessionSignal.aborted) return;
221
- await showSyncManager(
222
- ctx,
223
- (route, signal, onCommit, target) =>
224
- executeCommand(
225
- route,
226
- ctx,
227
- combineSignals(sessionSignal, signal),
228
- loaders,
229
- onCommit,
230
- target,
231
- ),
232
- sessionSignal,
233
- {
234
- getAttention: () => attention.current(),
235
- onSelectionResolved: (expected) => {
236
- if (attention.current() === expected) attention.clear(ctx);
237
- },
238
- },
239
- );
240
- } catch (error) {
241
- if (sessionSignal.aborted) return;
242
- ctx.ui.setStatus(STATUS_KEY, undefined);
243
- ctx.ui.notify(errorMessage(error), "error");
244
- }
245
- if (!sessionSignal.aborted) attention.publish(ctx);
246
- return;
247
- }
248
- const result = await executeCommand(rawArgs, ctx, sessionSignal, loaders);
249
- if (result.kind === "decision-required") {
250
- ctx.ui.notify(result.decision.directMessage, "error");
251
- } else if (result.kind === "remote-selection-required") {
252
- const origin = directSelectionOrigin(rawArgs);
253
- if (origin) attention.set(result.decision, origin);
254
- const deterministic = splitArgs(rawArgs).some((arg) => arg === "--yes" || arg === "-y");
255
- if (origin && ctx.mode === "tui" && !deterministic) {
256
- await resolveSelectionAttention(ctx, attention, sessionSignal, loaders);
257
- } else {
258
- ctx.ui.notify(
259
- formatRemoteSelectionMismatch(
260
- result.decision.setupName,
261
- result.decision.localInclude,
262
- result.decision.remoteInclude,
263
- ),
264
- "error",
265
- );
266
- }
267
- }
268
- await clearAttentionAfterCompletedOperation(rawArgs, result, ctx, attention, sessionSignal);
269
- await reconcileSelectionAttention(ctx, attention, sessionSignal);
270
- if (!sessionSignal.aborted) attention.publish(ctx);
271
- }
272
-
273
- async function clearAttentionAfterCompletedOperation(
274
- rawArgs: string,
275
- result: RunRouteResult,
276
- ctx: ExtensionContext,
277
- attention: SyncAttentionController,
278
- signal: AbortSignal,
279
- ) {
280
- if (result.kind !== "completed" || result.outcome === "cancelled" || signal.aborted) return;
281
- const [command, ...rest] = splitArgs(rawArgs);
282
- if (command !== "sync" && command !== "pull" && command !== "push") return;
283
- const current = attention.current();
284
- if (!current) return;
285
- try {
286
- const options = parseOptions(rest);
287
- const setupName = options.setup ?? (await loadConfig()).setupName;
288
- if (signal.aborted || attention.current() !== current) return;
289
- if (current.decision.setupName === setupName) attention.clear(ctx);
290
- } catch {
291
- // Attention reconciliation below owns malformed or concurrently changed settings.
292
- }
293
- }
294
-
295
- async function reconcileSelectionAttention(
296
- ctx: ExtensionContext,
297
- attention: SyncAttentionController,
298
- signal: AbortSignal,
299
- ) {
300
- const current = attention.current();
301
- if (!current || signal.aborted) return;
302
- try {
303
- const config = await loadConfig(current.decision.setupName);
304
- if (signal.aborted || attention.current() !== current) return;
305
- if (!syncAttentionMatchesConfig(current, config)) attention.clear(ctx);
306
- } catch {
307
- if (!signal.aborted && attention.current() === current) attention.clear(ctx);
308
- }
309
- }
310
-
311
- function directSelectionOrigin(rawArgs: string): SyncAttentionOrigin | undefined {
312
- const command = splitArgs(rawArgs)[0];
313
- return command === "sync" || command === "pull" || command === "push" ? command : undefined;
314
- }
315
-
316
- async function resolveSelectionAttention(
317
- ctx: ExtensionContext,
318
- attention: SyncAttentionController,
319
- signal: AbortSignal,
320
- loaders: SyncLoaders,
321
- options: {
322
- cancelLabel?: string;
323
- withStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;
324
- } = {},
325
- ) {
326
- const current = attention.current();
327
- if (!current || signal.aborted) return;
328
- const { dispatchManagerResult } = await import("./manager-result-dispatcher.js");
329
- if (signal.aborted || attention.current() !== current) return;
330
- await dispatchManagerResult(
331
- ctx,
332
- { kind: "remote-selection-required", decision: current.decision },
333
- current.origin,
334
- (route, actionSignal, onCommit, target) => {
335
- const execute = () =>
336
- executeRecoveryCommand(
337
- route,
338
- ctx,
339
- combineSignals(signal, actionSignal),
340
- loaders,
341
- onCommit,
342
- target,
343
- );
344
- return options.withStateAccess ? options.withStateAccess(execute) : execute();
345
- },
346
- signal,
347
- {
348
- cancelLabel: options.cancelLabel,
349
- withStateAccess: options.withStateAccess,
350
- onSelectionResolved: () => {
351
- if (attention.current() === current) attention.clear(ctx);
352
- },
353
- },
354
- );
355
- }
356
-
357
- async function executeRecoveryCommand(
358
- rawArgs: string,
359
- ctx: ExtensionContext,
360
- signal: AbortSignal | undefined,
361
- loaders: SyncLoaders,
362
- onCommit?: () => void,
363
- setup?: string,
364
- ): Promise<RunRouteResult> {
365
- try {
366
- const [subcommand, ...rest] = splitArgs(rawArgs);
367
- if (subcommand !== "sync" && subcommand !== "pull" && subcommand !== "push") {
368
- throw new Error(`Unsupported sync recovery route: ${subcommand ?? "missing"}`);
369
- }
370
- const options = parseOptions(rest);
371
- if (setup !== undefined) options.setup = setup;
372
- if (signal) options.signal = signal;
373
- if (onCommit) options.onCommit = onCommit;
374
- options.reload = false;
375
- options.auto = false;
376
- validateCommandOptions(subcommand, options);
377
- const operations = await loaders.operations();
378
- throwIfAborted(options.signal);
379
- if (subcommand === "push") {
380
- const outcome = await withLock("push", () => operations.push(ctx, options));
381
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
382
- }
383
- if (subcommand === "pull") {
384
- const outcome = await withLock("pull", () => operations.pull(ctx, options));
385
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
386
- }
387
- await withLock("sync", () => operations.syncBoth(ctx, options));
388
- return { kind: "completed" };
389
- } catch (error) {
390
- if (signal?.aborted) return { kind: "failed" };
391
- ctx.ui.setStatus(STATUS_KEY, undefined);
392
- if (error instanceof RemoteSelectionMismatchError) {
393
- return { kind: "remote-selection-required", decision: error.decision };
394
- }
395
- if (isSyncDecisionRequiredError(error)) {
396
- return { kind: "decision-required", decision: error.decision };
397
- }
398
- ctx.ui.notify(errorMessage(error), "error");
399
- return { kind: "failed" };
400
- }
401
- }
402
-
403
- async function executeCommand(
404
- rawArgs: string,
405
- ctx: ExtensionCommandContext,
406
- signal: AbortSignal | undefined,
407
- loaders: SyncLoaders,
408
- onCommit?: () => void,
409
- setup?: string,
410
- ): Promise<RunRouteResult> {
411
- try {
412
- const command = await resolveSyncCommand(rawArgs, ctx);
413
- if (signal?.aborted || !command) return { kind: "completed" };
414
- const { subcommand, rest } = command;
415
- const options = parseOptions(rest);
416
- if (setup !== undefined) options.setup = setup;
417
- if (signal) options.signal = signal;
418
- if (onCommit) options.onCommit = onCommit;
419
- validateCommandOptions(subcommand, options);
420
-
421
- switch (subcommand) {
422
- case "help":
423
- ctx.ui.notify(usage(), "info");
424
- return { kind: "completed" };
425
- case "use": {
426
- const { useSyncSetup } = await loaders.setupSwitch();
427
- throwIfAborted(options.signal);
428
- await useSyncSetup(
429
- ctx,
430
- options.args[0] ?? "",
431
- async (selectedSetup) => {
432
- const operations = await loaders.operations();
433
- throwIfAborted(options.signal);
434
- return withLock("pull", () =>
435
- operations.pull(ctx, { ...options, setup: selectedSetup }),
436
- );
437
- },
438
- undefined,
439
- options.signal,
440
- );
441
- return { kind: "completed" };
442
- }
443
- case "init":
444
- await initConfig(ctx, signal);
445
- return { kind: "completed" };
446
- case "config":
447
- await showConfig(ctx, options);
448
- return { kind: "completed" };
449
- case "files": {
450
- const { showFileSelection } = await import("./file-selection.js");
451
- throwIfAborted(options.signal);
452
- await showFileSelection(ctx, options.setup, options.signal);
453
- return { kind: "completed" };
454
- }
455
- case "status": {
456
- const operations = await loaders.operations();
457
- throwIfAborted(options.signal);
458
- await operations.status(ctx, options);
459
- return { kind: "completed" };
460
- }
461
- case "diff": {
462
- const operations = await loaders.operations();
463
- throwIfAborted(options.signal);
464
- await operations.diff(ctx, options);
465
- return { kind: "completed" };
466
- }
467
- case "doctor": {
468
- const operations = await loaders.operations();
469
- throwIfAborted(options.signal);
470
- await operations.doctor(ctx, options);
471
- return { kind: "completed" };
472
- }
473
- case "push": {
474
- const operations = await loaders.operations();
475
- throwIfAborted(options.signal);
476
- const outcome = await withLock("push", () => operations.push(ctx, options));
477
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
478
- }
479
- case "pull": {
480
- const operations = await loaders.operations();
481
- throwIfAborted(options.signal);
482
- const outcome = await withLock("pull", () => operations.pull(ctx, options));
483
- return { kind: "completed", ...(outcome ? { outcome } : {}) };
484
- }
485
- case "sync": {
486
- const operations = await loaders.operations();
487
- throwIfAborted(options.signal);
488
- await withLock("sync", () => operations.syncBoth(ctx, options));
489
- return { kind: "completed" };
490
- }
491
- case "history": {
492
- const operations = await loaders.operations();
493
- throwIfAborted(options.signal);
494
- await operations.history(ctx, options);
495
- return { kind: "completed" };
496
- }
497
- case "rollback": {
498
- const operations = await loaders.operations();
499
- throwIfAborted(options.signal);
500
- await withLock("rollback", () => operations.rollback(ctx, options));
501
- return { kind: "completed" };
502
- }
503
- case "migrate-state":
504
- await migrateStateDirectory(ctx, options);
505
- return { kind: "completed" };
506
- case "unlock":
507
- await unlock(ctx, options);
508
- return { kind: "completed" };
509
- default:
510
- ctx.ui.notify(`Unknown /sync command: ${subcommand}\n\n${usage()}`, "warning");
511
- return { kind: "failed" };
512
- }
513
- } catch (error) {
514
- if (signal?.aborted) return { kind: "failed" };
515
- ctx.ui.setStatus(STATUS_KEY, undefined);
516
- if (error instanceof SetupPullRequiresUiError) throw error;
517
- if (error instanceof RemoteSelectionMismatchError) {
518
- return { kind: "remote-selection-required", decision: error.decision };
519
- }
520
- if (isSyncDecisionRequiredError(error)) {
521
- return { kind: "decision-required", decision: error.decision };
522
- }
523
- ctx.ui.notify(errorMessage(error), "error");
524
- return { kind: "failed" };
525
- }
526
- }
527
-
528
- async function migrateStateDirectory(ctx: ExtensionCommandContext, options: CommandOptions) {
529
- const notice = stateDirectoryMigrationNotice();
530
- if (!notice) {
531
- ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
532
- return;
533
- }
534
- if (
535
- !options.yes &&
536
- !(await ctx.ui.confirm(
537
- "Migrate pi-sync state directory",
538
- "Confirm that every other Pi process is closed. pi-sync will atomically rename .pisync/ to pi-sync/ without merging or deleting either root.",
539
- { signal: options.signal },
540
- ))
541
- ) {
542
- ctx.ui.notify("pi-sync state migration cancelled.", "info");
543
- return;
544
- }
545
- throwIfAborted(options.signal);
546
- const result = await migrateLegacyStateDirectory();
547
- throwIfAborted(options.signal);
548
- if (result.status === "ready") {
549
- ctx.ui.notify("pi-sync already uses the canonical pi-sync/ state directory.", "info");
550
- return;
551
- }
552
- ctx.ui.notify(result.message, result.status === "migrated" ? "info" : "warning");
553
- }
554
-
555
- async function autoSync(
556
- ctx: ExtensionContext,
557
- signal: AbortSignal,
558
- loaders: SyncLoaders,
559
- ): Promise<RemoteSelectionDecision | undefined> {
560
- try {
561
- const partial = await loadPartialConfig();
562
- throwIfAborted(signal);
563
- if (!partial.automatic) return;
564
- await ensureStateDir();
565
- throwIfAborted(signal);
566
- await loadConfig();
567
- throwIfAborted(signal);
568
- const operations = await loaders.operations();
569
- throwIfAborted(signal);
570
- await withLock("auto-sync", () => {
571
- throwIfAborted(signal);
572
- return operations.syncBoth(ctx, { ...AUTO_SYNC_OPTIONS, signal });
573
- });
574
- } catch (error) {
575
- if (signal.aborted || isMissingConfigError(error)) return;
576
- ctx.ui.setStatus(STATUS_KEY, undefined);
577
- if (error instanceof RemoteSelectionMismatchError) return error.decision;
578
- ctx.ui.notify(`pi-sync auto sync skipped: ${errorMessage(error)}`, "warning");
579
- }
580
- }
581
-
582
- async function autoPushSessions(ctx: ExtensionContext, signal: AbortSignal, loaders: SyncLoaders) {
583
- try {
584
- const partial = await loadPartialConfig();
585
- throwIfAborted(signal);
586
- if (!partial.automatic) return;
587
- if (!partial.include.includes("sessions")) return;
588
- await ensureStateDir();
589
- throwIfAborted(signal);
590
- const config = await loadConfig();
591
- throwIfAborted(signal);
592
- if (!config.include.includes("sessions")) return;
593
- const [operations, snapshotModule, syncStateModule] = await Promise.all([
594
- loaders.operations(),
595
- loaders.snapshot(),
596
- loaders.syncState(),
597
- ]);
598
- throwIfAborted(signal);
599
- await withLock("auto-session-push", async () => {
600
- throwIfAborted(signal);
601
- const state = await readStateForConfig(config);
602
- throwIfAborted(signal);
603
- const local = await snapshotModule.createSnapshot(
604
- config.snapshotIdentity,
605
- snapshotOptionsForContext(ctx, config),
606
- );
607
- throwIfAborted(signal);
608
- if (!syncStateModule.hasLocalChanges(local, state, config)) return;
609
- await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
610
- });
611
- } catch (error) {
612
- if (signal.aborted || isMissingConfigError(error)) return;
613
- ctx.ui.setStatus(STATUS_KEY, undefined);
614
- ctx.ui.notify(`pi-sync session push skipped: ${errorMessage(error)}`, "warning");
615
- }
616
- }
617
-
618
- async function initConfig(ctx: ExtensionCommandContext, signal?: AbortSignal) {
619
- const configPath = localConfigPath();
620
- if (await readLocalConfigObject()) {
621
- ctx.ui.notify(`Config already exists: ${await activeLocalConfigPath()}`, "info");
622
- return;
623
- }
624
-
625
- if (ctx.mode === "tui") {
626
- const { showSetupWizard } = await import("./manager-ui.js");
627
- throwIfAborted(signal);
628
- await showSetupWizard(ctx, signal);
629
- return;
630
- }
631
- await createLocalConfigDocument(localConfigTemplate());
632
- ctx.ui.notify(
633
- `Created ${configPath}. Add a storage connection and sync setup before syncing.`,
634
- "info",
635
- );
636
- }
637
-
638
- async function showConfig(ctx: ExtensionCommandContext, options: CommandOptions) {
639
- const config = await loadConfig(options.setup);
640
- const warnings = [
641
- ...(config.backend.type === "s3" ? sessionTokenWarnings(config.backend.profile) : []),
642
- ...syncSessionsWarnings(config),
643
- ];
644
- const storageLines = configStorageLines(config);
645
- ctx.ui.notify(
646
- [
647
- "pi-sync config:",
648
- `sync setup: ${config.setupName}`,
649
- `storage connection: ${config.connectionName}`,
650
- ...storageLines,
651
- `storage path: ${config.storagePath}`,
652
- `automatic sync: ${config.automatic ? "enabled" : "disabled"}`,
653
- `included content: ${config.include.join(", ") || "none"}`,
654
- `sessions: ${config.include.includes("sessions") ? "included" : "not included"}`,
655
- `settings file: ${localConfigPath()}`,
656
- ...warnings,
657
- ].join("\n"),
658
- warnings.length > 0 ? "warning" : "info",
659
- );
660
- }
661
-
662
- function configStorageLines(config: AnySyncConfig) {
663
- switch (config.backend.type) {
664
- case "git":
665
- return [
666
- "kind: git",
667
- `remote: ${displayGitRemote(config.backend.profile.remote)}`,
668
- "authentication: existing Git/SSH credentials (not stored)",
669
- `branch: ${config.backend.destination.branch}`,
670
- ];
671
- case "webdav":
672
- return [
673
- "kind: webdav",
674
- `url: ${displayWebDavUrl(config.backend.profile.url, config.backend.profile.username)}`,
675
- "username: configured (value hidden)",
676
- "password: configured",
677
- ];
678
- case "s3":
679
- return [
680
- "kind: s3",
681
- `endpoint: ${config.backend.profile.endpoint}`,
682
- `bucket: ${config.backend.destination.bucket}`,
683
- `region: ${config.backend.profile.region}`,
684
- "access key id: configured",
685
- "secret access key: configured",
686
- `session token: ${config.backend.profile.sessionToken ? "configured" : "not configured"}`,
687
- ];
688
- }
689
- }
690
-
691
- function displayGitRemote(value: string | undefined) {
692
- if (!value) return "missing";
693
- try {
694
- const url = new URL(value);
695
- url.username = "";
696
- url.password = "";
697
- url.search = "";
698
- url.hash = "";
699
- return url.toString();
700
- } catch {
701
- return value.replace(/^(?:[^@\s]+@)?(?<host>[^:]+):.+$/u, "$<host>:…");
702
- }
703
- }
704
-
705
- function displayWebDavUrl(value: string | undefined, username: string | undefined) {
706
- if (!value) return "missing";
707
- try {
708
- const url = new URL(value);
709
- url.username = "";
710
- url.password = "";
711
- url.search = "";
712
- url.hash = "";
713
- return username ? `${url.origin}/…` : `${url.origin}${url.pathname}`;
714
- } catch {
715
- return "invalid (value hidden)";
716
- }
717
- }
718
-
719
- function throwIfAborted(signal?: AbortSignal) {
720
- if (!signal?.aborted) return;
721
- throw signal.reason instanceof Error
722
- ? signal.reason
723
- : new DOMException("The operation was aborted", "AbortError");
724
- }
725
-
726
- function combineSignals(primary: AbortSignal, secondary?: AbortSignal) {
727
- return secondary ? AbortSignal.any([primary, secondary]) : primary;
728
- }
729
-
730
- function snapshotOptionsForContext(
731
- ctx: ExtensionCommandContext | ExtensionContext,
732
- config: AnySyncConfig,
733
- ): SnapshotOptions {
734
- return {
735
- include: config.include,
736
- sessionDir: sessionDirFromContext(ctx),
737
- };
738
- }
739
-
740
- function sessionDirFromContext(ctx: ExtensionCommandContext | ExtensionContext) {
741
- const manager = ctx.sessionManager as typeof ctx.sessionManager & {
742
- usesDefaultSessionDir?: () => boolean;
743
- };
744
- const usesDefaultSessionDir = manager.usesDefaultSessionDir;
745
- if (typeof usesDefaultSessionDir === "function" && usesDefaultSessionDir.call(manager)) {
746
- return undefined;
747
- }
748
- const getSessionDir = manager.getSessionDir;
749
- return typeof getSessionDir === "function"
750
- ? (getSessionDir.call(manager) as string | undefined)
751
- : undefined;
752
- }
753
-
754
- function cachedModuleLoader<Module>(load: () => Promise<Module>): () => Promise<Module> {
755
- let pending: Promise<Module> | undefined;
756
- return () => {
757
- if (!pending) {
758
- pending = load().catch((error) => {
759
- pending = undefined;
760
- throw error;
761
- });
762
- }
763
- return pending;
764
- };
765
- }
121
+ export type { SyncDependencies } from "./sync/sync-loaders.js";