@narumitw/pi-sync 0.49.14 → 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 +134 -241
  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-RGJ6SOYR.ts → chunk-AUSDLD6F.ts} +4 -4
  9. package/dist/chunks/chunk-AUSDLD6F.ts.map +7 -0
  10. package/dist/chunks/{chunk-3NDD5QPM.ts → chunk-AXXS2BPA.ts} +9 -9
  11. package/dist/chunks/chunk-AXXS2BPA.ts.map +7 -0
  12. package/dist/chunks/{chunk-APZVBSM6.ts → chunk-C7XY5NF5.ts} +214 -365
  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-LJDSHNS3.ts → chunk-GWMHAQG4.ts} +4 -4
  21. package/dist/chunks/chunk-GWMHAQG4.ts.map +7 -0
  22. package/dist/chunks/{chunk-FR6CVXAL.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-4F3W4CSC.ts → chunk-LCRI3ABW.ts} +22 -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-EWX3TPLQ.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-BHJUISB4.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-LONPFFNP.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-5WJNWFTG.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-4UABOP44.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-3QZGO2EF.ts → s3-backend-3P33LQHU.ts} +96 -18
  69. package/dist/chunks/s3-backend-3P33LQHU.ts.map +7 -0
  70. package/dist/chunks/{setup-switch-X3W3FALZ.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-WBSV4MQG.ts → snapshot-HKEWCVK6.ts} +7 -6
  74. package/dist/chunks/sync-inspection-FEP4L5TR.ts +20 -0
  75. package/dist/chunks/{sync-operations-MPAFVVHA.ts → sync-operations-ONJ27QRL.ts} +381 -299
  76. package/dist/chunks/sync-operations-ONJ27QRL.ts.map +7 -0
  77. package/dist/chunks/{sync-state-36H7ULG2.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 +211 -0
  84. package/package.json +6 -8
  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} +40 -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} +41 -330
  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 -284
  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} +42 -11
  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-3NDD5QPM.ts.map +0 -7
  164. package/dist/chunks/chunk-4F3W4CSC.ts.map +0 -7
  165. package/dist/chunks/chunk-4U6AFLJB.ts.map +0 -7
  166. package/dist/chunks/chunk-5MQ76BK5.ts.map +0 -7
  167. package/dist/chunks/chunk-5WJNWFTG.ts.map +0 -7
  168. package/dist/chunks/chunk-APZVBSM6.ts.map +0 -7
  169. package/dist/chunks/chunk-BHJUISB4.ts.map +0 -7
  170. package/dist/chunks/chunk-EWX3TPLQ.ts.map +0 -7
  171. package/dist/chunks/chunk-F5QL2GPY.ts.map +0 -7
  172. package/dist/chunks/chunk-FR6CVXAL.ts.map +0 -7
  173. package/dist/chunks/chunk-HGV24P4T.ts.map +0 -7
  174. package/dist/chunks/chunk-JNOOSPEW.ts +0 -82
  175. package/dist/chunks/chunk-JNOOSPEW.ts.map +0 -7
  176. package/dist/chunks/chunk-LJDSHNS3.ts.map +0 -7
  177. package/dist/chunks/chunk-LONPFFNP.ts.map +0 -7
  178. package/dist/chunks/chunk-RGJ6SOYR.ts.map +0 -7
  179. package/dist/chunks/chunk-W64NEHXT.ts +0 -167
  180. package/dist/chunks/chunk-W64NEHXT.ts.map +0 -7
  181. package/dist/chunks/chunk-YQ6UW7IF.ts.map +0 -7
  182. package/dist/chunks/file-selection-4UABOP44.ts.map +0 -7
  183. package/dist/chunks/git-backend-E3UOUUOM.ts.map +0 -7
  184. package/dist/chunks/manager-result-dispatcher-RNRQA3PW.ts +0 -18
  185. package/dist/chunks/manager-ui-QO2P4V3Z.ts +0 -2757
  186. package/dist/chunks/manager-ui-QO2P4V3Z.ts.map +0 -7
  187. package/dist/chunks/remote-selection-ui-LHSAGKGA.ts +0 -17
  188. package/dist/chunks/s3-backend-3QZGO2EF.ts.map +0 -7
  189. package/dist/chunks/sync-operations-MPAFVVHA.ts.map +0 -7
  190. package/dist/chunks/webdav-backend-ZD5RHRIU.ts.map +0 -7
  191. package/src/config.ts +0 -910
  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-RNRQA3PW.ts.map → manager-result-dispatcher-U23ODD32.ts.map} +0 -0
  197. /package/dist/chunks/{remote-selection-ui-LHSAGKGA.ts.map → remote-selection-ui-ZE3TY67S.ts.map} +0 -0
  198. /package/dist/chunks/{setup-switch-X3W3FALZ.ts.map → setup-switch-IS2PEZ3J.ts.map} +0 -0
  199. /package/dist/chunks/{snapshot-WBSV4MQG.ts.map → snapshot-HKEWCVK6.ts.map} +0 -0
  200. /package/dist/chunks/{sync-state-36H7ULG2.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
@@ -0,0 +1,51 @@
1
+ import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
2
+ import type { AnySyncConfig } from "../settings/settings-types.js";
3
+ import { createSnapshot } from "../snapshot/snapshot.js";
4
+ import type { SnapshotOptions } from "../snapshot/snapshot-types.js";
5
+ import { readStateForConfig, syncStateFingerprint } from "../state/sync-state-store.js";
6
+ import { throwIfAborted } from "./signals.js";
7
+ import { inspectRemoteSelection } from "./sync-policy.js";
8
+ import { hasLocalChanges, remoteChangedSinceState } from "./sync-state.js";
9
+
10
+ /** Advisory baseline-relative observation, never an authorization to transfer files. */
11
+ export async function inspectSync(
12
+ config: AnySyncConfig,
13
+ options: SnapshotOptions,
14
+ signal?: AbortSignal,
15
+ factory: SyncBackendFactory = createSyncBackend,
16
+ ) {
17
+ throwIfAborted(signal);
18
+ const backend = await factory(config);
19
+ throwIfAborted(signal);
20
+ const local = await createSnapshot(config.snapshotIdentity, { ...options, signal });
21
+ throwIfAborted(signal);
22
+ const state = await readStateForConfig(config);
23
+ throwIfAborted(signal);
24
+ const head = await backend.readHead(signal);
25
+ throwIfAborted(signal);
26
+ return {
27
+ head,
28
+ selectionState: head
29
+ ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] })
30
+ : undefined,
31
+ localFiles: local.files.length,
32
+ localChanged: hasLocalChanges(local, state, config),
33
+ remoteChanged: remoteChangedSinceState(head, state, config, (left, right) =>
34
+ backend.sameRevision(left, right),
35
+ ),
36
+ firstSync: !state.lastAppliedSnapshot,
37
+ emptyInclude: config.include.length === 0,
38
+ stateIdentity: syncStateFingerprint(state),
39
+ destination: backend.destination,
40
+ capability: backend.capability,
41
+ };
42
+ }
43
+
44
+ export type SyncInspection = Awaited<ReturnType<typeof inspectSync>>;
45
+
46
+ export interface StartupObservation {
47
+ setupName: string;
48
+ configIdentity: string;
49
+ checkedAt: string;
50
+ inspection: SyncInspection;
51
+ }
@@ -0,0 +1,54 @@
1
+ type SetupSwitchModule = Pick<typeof import("./setup-switch.js"), "useSyncSetup">;
2
+ type SnapshotModule = Pick<typeof import("../snapshot/snapshot.js"), "createSnapshot">;
3
+ type SyncStateModule = Pick<typeof import("./sync-state.js"), "hasLocalChanges">;
4
+ type SyncOperations = typeof import("./sync-operations.js");
5
+
6
+ type SyncInspectionModule = Pick<typeof import("./sync-inspection.js"), "inspectSync">;
7
+
8
+ export interface SyncDependencies {
9
+ loadSyncInspection(): Promise<SyncInspectionModule>;
10
+ loadSetupSwitch(): Promise<SetupSwitchModule>;
11
+ loadSnapshot(): Promise<SnapshotModule>;
12
+ loadSyncState(): Promise<SyncStateModule>;
13
+ loadSyncOperations(): Promise<SyncOperations>;
14
+ }
15
+
16
+ export interface SyncLoaders {
17
+ inspection(): Promise<SyncInspectionModule>;
18
+ setupSwitch(): Promise<SetupSwitchModule>;
19
+ snapshot(): Promise<SnapshotModule>;
20
+ syncState(): Promise<SyncStateModule>;
21
+ operations(): Promise<SyncOperations>;
22
+ }
23
+
24
+ export function createSyncLoaders(dependencies: Partial<SyncDependencies>): SyncLoaders {
25
+ const loaders: SyncLoaders = {
26
+ inspection: cachedModuleLoader(
27
+ dependencies.loadSyncInspection ?? (() => import("./sync-inspection.js")),
28
+ ),
29
+ setupSwitch: cachedModuleLoader(
30
+ dependencies.loadSetupSwitch ?? (() => import("./setup-switch.js")),
31
+ ),
32
+ snapshot: cachedModuleLoader(
33
+ dependencies.loadSnapshot ?? (() => import("../snapshot/snapshot.js")),
34
+ ),
35
+ syncState: cachedModuleLoader(dependencies.loadSyncState ?? (() => import("./sync-state.js"))),
36
+ operations: cachedModuleLoader(
37
+ dependencies.loadSyncOperations ?? (() => import("./sync-operations.js")),
38
+ ),
39
+ };
40
+ return loaders;
41
+ }
42
+
43
+ function cachedModuleLoader<Module>(load: () => Promise<Module>): () => Promise<Module> {
44
+ let pending: Promise<Module> | undefined;
45
+ return () => {
46
+ if (!pending) {
47
+ pending = load().catch((error) => {
48
+ pending = undefined;
49
+ throw error;
50
+ });
51
+ }
52
+ return pending;
53
+ };
54
+ }
@@ -1,22 +1,21 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { createSyncBackend, type SyncBackendFactory } from "./backend-factory.js";
4
+ import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
5
5
  import {
6
- loadConfig,
7
- readStateForConfig,
8
- sessionDirForApply,
9
- stateDir,
10
- syncConfigReviewFingerprint,
11
- syncSessionsWarnings,
12
- writeStateForConfig,
13
- } from "./config.js";
14
- import { inspectLock, isLockGuardHeld, isStaleLock, withLock } from "./lock.js";
6
+ expectedRemoteHead,
7
+ type PublishSnapshotResult,
8
+ type RemoteHead,
9
+ type SyncBackend,
10
+ } from "../backends/sync-backend.js";
11
+ import type { CommandOptions } from "../commands/command-types.js";
12
+ import { loadConfig } from "../settings/config.js";
13
+ import type { AnySyncConfig } from "../settings/settings-types.js";
15
14
  import {
16
- formatRemoteSelectionStatus,
17
- readSnapshotForHead,
18
- requireCompatibleRemoteSelection,
19
- } from "./remote-snapshot.js";
15
+ sessionDirForApply,
16
+ sessionDirFromContext,
17
+ snapshotOptionsForContext,
18
+ } from "../snapshot/session-paths.js";
20
19
  import {
21
20
  createSnapshot,
22
21
  filterSnapshotForConfigPolicy,
@@ -26,28 +25,28 @@ import {
26
25
  sessionSnapshotPathFromAbsolute,
27
26
  snapshotIncludesSessions,
28
27
  snapshotWithoutSessions,
29
- } from "./snapshot.js";
30
- import { applySnapshot } from "./snapshot-apply.js";
31
- import { encodeSnapshot } from "./snapshot-codec.js";
32
- import {
33
- expectedRemoteHead,
34
- type PublishSnapshotResult,
35
- type RemoteHead,
36
- type SyncBackend,
37
- } from "./sync-backend.js";
38
- import { createSyncDecision } from "./sync-decision.js";
28
+ } from "../snapshot/snapshot.js";
29
+ import { applySnapshot } from "../snapshot/snapshot-apply.js";
30
+ import { encodeSnapshot } from "../snapshot/snapshot-codec.js";
31
+ import type { Snapshot, SnapshotOptions } from "../snapshot/snapshot-types.js";
32
+ import { stateDir } from "../state/state-directory.js";
33
+ import type { SyncState } from "../state/state-types.js";
34
+ import { readStateForConfig, writeStateForConfig } from "../state/sync-state-store.js";
39
35
  import {
40
36
  countPreservedRemoteFiles,
41
- errorMessage,
42
- formatDiff,
43
37
  formatPullSummary,
44
38
  formatPushSummary,
45
39
  formatRollbackSummary,
46
- formatSnapshotOnlyDiff,
47
- publicationCapabilityDescription,
48
- safeTerminalText,
49
- } from "./sync-format.js";
50
- import { inspectRemoteSelection, remoteSelectionMismatch } from "./sync-policy.js";
40
+ } from "../ui/sync-format.js";
41
+ import {
42
+ readRemoteSnapshot,
43
+ readSnapshotForHead,
44
+ requireCompatibleRemoteSelection,
45
+ } from "./remote-snapshot.js";
46
+ import { throwIfAborted } from "./signals.js";
47
+ import { createSyncDecision } from "./sync-decision.js";
48
+ import { errorMessage } from "./sync-errors.js";
49
+ import { inspectRemoteSelection } from "./sync-policy.js";
51
50
  import {
52
51
  canPullRemoteSessionsOnFirstSync,
53
52
  canPullRemoteSettingsOnFirstSync,
@@ -61,17 +60,9 @@ import {
61
60
  snapshotsMatch,
62
61
  syncPolicyChanged,
63
62
  } from "./sync-state.js";
64
- import type {
65
- AnySyncConfig,
66
- CommandOptions,
67
- Snapshot,
68
- SnapshotOptions,
69
- SyncState,
70
- } from "./types.js";
71
63
 
72
64
  const STATUS_KEY = "sync";
73
65
  const VERSION = 1;
74
- const DEFAULT_PROFILE = "default";
75
66
  const POST_LOCAL_COMMIT_TIMEOUT_MS = 30_000;
76
67
 
77
68
  export class PublicationStatePersistenceError extends Error {
@@ -109,190 +100,6 @@ interface PushInput {
109
100
  backend?: SyncBackend;
110
101
  }
111
102
 
112
- function backendFor(config: AnySyncConfig, factory: SyncBackendFactory) {
113
- return factory(config);
114
- }
115
-
116
- export async function status(
117
- ctx: ExtensionCommandContext,
118
- options: CommandOptions,
119
- factory: SyncBackendFactory = createSyncBackend,
120
- ) {
121
- const config = await loadConfig(options.setup);
122
- throwIfAborted(options.signal);
123
- ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
124
- const backend = await backendFor(config, factory);
125
- const local = await createSnapshot(
126
- config.snapshotIdentity,
127
- snapshotOptionsForContext(ctx, config),
128
- );
129
- throwIfAborted(options.signal);
130
- const state = await readStateForConfig(config);
131
- throwIfAborted(options.signal);
132
- const head = await backend.readHead(options.signal);
133
- throwIfAborted(options.signal);
134
- const selectionState = head
135
- ? inspectRemoteSelection(config.include, { selection: head.selection, files: [] })
136
- : undefined;
137
- const localChanged = hasLocalChanges(local, state, config);
138
-
139
- const remoteText = head
140
- ? `remote: ${head.snapshotId} from ${head.machine} at ${head.createdAt}`
141
- : "remote: empty";
142
- const remoteChanged = remoteChangedSinceState(head, state, config, (left, right) =>
143
- backend.sameRevision(left, right),
144
- );
145
- const warnings = syncSessionsWarnings(config);
146
- ctx.ui.setStatus(STATUS_KEY, undefined);
147
- ctx.ui.notify(
148
- [
149
- `sync setup: ${config.setupName}`,
150
- `storage connection: ${config.connectionName}`,
151
- `storage location: ${safeTerminalText(backend.destination)}`,
152
- `publication safety: ${publicationCapabilityDescription(backend.capability)}`,
153
- `included content: ${config.include.join(", ") || "none"}`,
154
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
155
- remoteText,
156
- formatRemoteSelectionStatus(selectionState),
157
- `local files: ${local.files.length}`,
158
- `local changed since last sync: ${localChanged ? "yes" : "no"}`,
159
- `remote changed since last sync: ${remoteChanged ? "yes" : "no"}`,
160
- ...warnings,
161
- ].join("\n"),
162
- localChanged || remoteChanged || selectionState?.kind === "different" || warnings.length > 0
163
- ? "warning"
164
- : "info",
165
- );
166
- }
167
-
168
- export async function diff(
169
- ctx: ExtensionCommandContext,
170
- options: CommandOptions,
171
- factory: SyncBackendFactory = createSyncBackend,
172
- ) {
173
- const config = await loadConfig(options.setup);
174
- throwIfAborted(options.signal);
175
- ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
176
- const backend = await backendFor(config, factory);
177
- const local = await createSnapshot(
178
- config.snapshotIdentity,
179
- snapshotOptionsForContext(ctx, config),
180
- );
181
- throwIfAborted(options.signal);
182
- const { snapshot: remote, selectionState } = await readRemoteSnapshot(
183
- backend,
184
- config,
185
- options.signal,
186
- { allowSelectionDifference: true },
187
- );
188
- throwIfAborted(options.signal);
189
- ctx.ui.setStatus(STATUS_KEY, undefined);
190
-
191
- const warnings = syncSessionsWarnings(config);
192
- const header = [
193
- `sync setup: ${config.setupName}`,
194
- `storage connection: ${config.connectionName}`,
195
- `storage location: ${safeTerminalText(backend.destination)}`,
196
- `included content: ${config.include.join(", ") || "none"}`,
197
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
198
- formatRemoteSelectionStatus(selectionState),
199
- ...warnings,
200
- ].join("\n");
201
- const level = warnings.length > 0 || selectionState?.kind === "different" ? "warning" : "info";
202
- if (!remote) {
203
- ctx.ui.notify(
204
- `${header}\n\n${formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local)}`,
205
- level,
206
- );
207
- return;
208
- }
209
-
210
- ctx.ui.notify(`${header}\n\n${formatDiff(local, remote)}`, level);
211
- }
212
-
213
- export async function doctor(
214
- ctx: ExtensionCommandContext,
215
- options: CommandOptions,
216
- factory: SyncBackendFactory = createSyncBackend,
217
- ) {
218
- const messages: string[] = [];
219
- let level: "info" | "warning" = "info";
220
- let snapshotOptions: SnapshotOptions = {};
221
- let profile = DEFAULT_PROFILE;
222
- let backend: SyncBackend | undefined;
223
- let backendSummary: string[] = [];
224
-
225
- try {
226
- const config = await loadConfig(options.setup);
227
- throwIfAborted(options.signal);
228
- backend = await backendFor(config, factory);
229
- profile = config.snapshotIdentity;
230
- snapshotOptions = snapshotOptionsForContext(ctx, config);
231
- messages.push(
232
- `config: ok (sync setup ${config.setupName})`,
233
- `included content: ${config.include.join(", ") || "none"}`,
234
- `sessions: ${config.include.includes("sessions") ? "included" : "excluded"}`,
235
- );
236
- backendSummary = [
237
- `storage location: ${safeTerminalText(backend.destination)}`,
238
- `publication safety: ${publicationCapabilityDescription(backend.capability)}`,
239
- ];
240
- const warnings = syncSessionsWarnings(config);
241
- if (warnings.length > 0) {
242
- level = "warning";
243
- messages.push(...warnings);
244
- }
245
- } catch (error) {
246
- throwIfAborted(options.signal);
247
- level = "warning";
248
- messages.push(`config: ${errorMessage(error)}`);
249
- }
250
-
251
- const local = await createSnapshot(profile, snapshotOptions);
252
- throwIfAborted(options.signal);
253
- const secrets = scanSnapshot(local);
254
- if (secrets.length > 0) {
255
- level = "warning";
256
- messages.push("secret scan: possible secrets found:");
257
- messages.push(...secrets.map((secret) => `- ${secret}`));
258
- } else {
259
- messages.push(`secret scan: ok (${local.files.length} files checked)`);
260
- }
261
-
262
- const lock = await inspectLock();
263
- throwIfAborted(options.signal);
264
- if (lock.status === "valid" && isStaleLock(lock.lock)) {
265
- level = "warning";
266
- messages.push(
267
- `lock: stale (pid ${lock.lock.pid}); run /sync unlock after verifying no sync is running`,
268
- );
269
- } else if (lock.status === "valid") {
270
- messages.push(`lock: held by pid ${lock.lock.pid} since ${lock.lock.startedAt}`);
271
- } else if (lock.status === "unreadable") {
272
- level = "warning";
273
- messages.push(
274
- "lock: unreadable; use /sync unlock --stale only after verifying no sync is running",
275
- );
276
- } else if (await isLockGuardHeld()) {
277
- throwIfAborted(options.signal);
278
- level = "warning";
279
- messages.push("lock: guard active while metadata is missing or still being initialized");
280
- } else {
281
- messages.push("lock: free");
282
- }
283
-
284
- if (backend) {
285
- messages.push(...backendSummary);
286
- const diagnostics = await backend.diagnose(options.signal);
287
- throwIfAborted(options.signal);
288
- for (const diagnostic of diagnostics) {
289
- messages.push(diagnostic.message);
290
- if (diagnostic.level !== "info") level = "warning";
291
- }
292
- }
293
- ctx.ui.notify(messages.join("\n"), level);
294
- }
295
-
296
103
  export async function push(
297
104
  ctx: ExtensionCommandContext | ExtensionContext,
298
105
  options: CommandOptions,
@@ -302,7 +109,7 @@ export async function push(
302
109
  const config = input?.config ?? (await loadConfig(options.setup));
303
110
  throwIfAborted(options.signal);
304
111
  ctx.ui.setStatus(STATUS_KEY, `pushing ${config.setupName}`);
305
- const backend = input?.backend ?? (await backendFor(config, factory));
112
+ const backend = input?.backend ?? (await factory(config));
306
113
  const state = input?.state ?? (await readStateForConfig(config));
307
114
  throwIfAborted(options.signal);
308
115
  const local =
@@ -362,11 +169,13 @@ export async function push(
362
169
  remoteForUpload,
363
170
  options.signal,
364
171
  );
365
- const secrets = scanSnapshot(local);
366
- if (secrets.length > 0) {
367
- throw new Error(
368
- `Refusing to push possible secrets:\n${secrets.map((s) => `- ${s}`).join("\n")}`,
369
- );
172
+ if (!config.skipSecretScan) {
173
+ const secrets = scanSnapshot(local);
174
+ if (secrets.length > 0) {
175
+ throw new Error(
176
+ `Refusing to push possible secrets:\n${secrets.map((s) => `- ${s}`).join("\n")}`,
177
+ );
178
+ }
370
179
  }
371
180
 
372
181
  if (!(await confirmPush(ctx, options, config, backend, local, upload, head, remoteForUpload))) {
@@ -446,7 +255,7 @@ export async function pull(
446
255
  const config = await loadConfig(options.setup);
447
256
  throwIfAborted(options.signal);
448
257
  ctx.ui.setStatus(STATUS_KEY, `pulling ${config.setupName}`);
449
- const backend = await backendFor(config, factory);
258
+ const backend = await factory(config);
450
259
  const state = await readStateForConfig(config);
451
260
  throwIfAborted(options.signal);
452
261
  const local = await createSnapshot(
@@ -551,7 +360,7 @@ export async function syncBoth(
551
360
  ) {
552
361
  const config = await loadConfig(options.setup);
553
362
  throwIfAborted(options.signal);
554
- const backend = await backendFor(config, factory);
363
+ const backend = await factory(config);
555
364
  const state = await readStateForConfig(config);
556
365
  throwIfAborted(options.signal);
557
366
  const local = await createSnapshot(
@@ -669,52 +478,6 @@ export async function syncBoth(
669
478
  if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
670
479
  }
671
480
 
672
- export async function history(
673
- ctx: ExtensionCommandContext,
674
- options: CommandOptions,
675
- factory: SyncBackendFactory = createSyncBackend,
676
- ) {
677
- const config = await loadConfig(options.setup);
678
- throwIfAborted(options.signal);
679
- const backend = await backendFor(config, factory);
680
- const snapshots = (await backend.listHistory(options.signal)).slice(-20).reverse();
681
- throwIfAborted(options.signal);
682
- if (snapshots.length === 0) {
683
- ctx.ui.notify("No remote pi-sync history found.", "info");
684
- return;
685
- }
686
-
687
- const currentSnapshot = snapshots[0]?.snapshotId;
688
- if (ctx.mode === "tui") {
689
- const labels = snapshots.map(
690
- (item, index) =>
691
- `${index + 1}. ${item.createdAt} · ${safeTerminalText(item.machine)} · ${item.snapshotId}${item.snapshotId === currentSnapshot ? " (current)" : ""}${item.syncSessions ? " · sessions" : ""}`,
692
- );
693
- const selected = await ctx.ui.select(
694
- `History for sync setup “${safeTerminalText(config.setupName)}”\n\nChoose a snapshot to preview rollback.`,
695
- [...labels, "Back"],
696
- );
697
- if (!selected || selected === "Back") return;
698
- throwIfAborted(options.signal);
699
- const index = labels.indexOf(selected);
700
- const snapshot = snapshots[index];
701
- if (!snapshot) return;
702
- await withLock("rollback", () =>
703
- rollback(ctx, { ...options, args: [snapshot.snapshotRef], yes: false }, factory, {
704
- backendIdentity: backend.identity,
705
- setup: config.setupName,
706
- }),
707
- );
708
- return;
709
- }
710
- ctx.ui.notify(
711
- snapshots
712
- .map((item) => `${item.snapshotRef} ${item.createdAt} ${safeTerminalText(item.machine)}`)
713
- .join("\n"),
714
- "info",
715
- );
716
- }
717
-
718
481
  export async function rollback(
719
482
  ctx: ExtensionCommandContext,
720
483
  options: CommandOptions,
@@ -726,7 +489,7 @@ export async function rollback(
726
489
 
727
490
  const config = await loadConfig(options.setup);
728
491
  throwIfAborted(options.signal);
729
- const backend = await backendFor(config, factory);
492
+ const backend = await factory(config);
730
493
  if (
731
494
  expectedSelection &&
732
495
  (backend.identity !== expectedSelection.backendIdentity ||
@@ -835,30 +598,6 @@ function protectedSessionPaths(ctx: ExtensionCommandContext | ExtensionContext)
835
598
  return snapshotPath ? new Set([snapshotPath]) : new Set<string>();
836
599
  }
837
600
 
838
- function snapshotOptionsForContext(
839
- ctx: ExtensionCommandContext | ExtensionContext,
840
- config: AnySyncConfig,
841
- ): SnapshotOptions {
842
- return {
843
- include: config.include,
844
- sessionDir: sessionDirFromContext(ctx),
845
- };
846
- }
847
-
848
- function sessionDirFromContext(ctx: ExtensionCommandContext | ExtensionContext) {
849
- const manager = ctx.sessionManager as typeof ctx.sessionManager & {
850
- usesDefaultSessionDir?: () => boolean;
851
- };
852
- const usesDefaultSessionDir = manager.usesDefaultSessionDir;
853
- if (typeof usesDefaultSessionDir === "function" && usesDefaultSessionDir.call(manager)) {
854
- return undefined;
855
- }
856
- const getSessionDir = manager.getSessionDir;
857
- return typeof getSessionDir === "function"
858
- ? (getSessionDir.call(manager) as string | undefined)
859
- : undefined;
860
- }
861
-
862
601
  async function maybeReload(ctx: ExtensionCommandContext | ExtensionContext, signal?: AbortSignal) {
863
602
  if (signal?.aborted || !("reload" in ctx)) return;
864
603
  if (
@@ -913,27 +652,6 @@ async function snapshotForUpload(
913
652
  return mergeRemotePreservedFiles(local, snapshot, config);
914
653
  }
915
654
 
916
- async function readRemoteSnapshot(
917
- backend: SyncBackend,
918
- config: AnySyncConfig,
919
- signal?: AbortSignal,
920
- options: { allowSelectionDifference?: boolean } = {},
921
- ) {
922
- const head = await backend.readHead(signal);
923
- if (!head) return { head: undefined, snapshot: undefined, selectionState: undefined };
924
- const snapshot = await readSnapshotForHead(backend, head, signal);
925
- const selectionState = inspectRemoteSelection(config.include, snapshot);
926
- if (!options.allowSelectionDifference && selectionState.kind === "different") {
927
- const configIdentity = syncConfigReviewFingerprint(config);
928
- throw remoteSelectionMismatch(config, selectionState.include, configIdentity);
929
- }
930
- return {
931
- head,
932
- snapshot: filterSnapshotForConfigPolicy(snapshot, config),
933
- selectionState,
934
- };
935
- }
936
-
937
655
  async function confirmPush(
938
656
  ctx: ExtensionCommandContext | ExtensionContext,
939
657
  options: CommandOptions,
@@ -965,13 +683,6 @@ async function confirmPush(
965
683
  return false;
966
684
  }
967
685
 
968
- function throwIfAborted(signal?: AbortSignal) {
969
- if (!signal?.aborted) return;
970
- throw signal.reason instanceof Error
971
- ? signal.reason
972
- : new DOMException("The operation was aborted", "AbortError");
973
- }
974
-
975
686
  function sameRemoteHead(
976
687
  backend: SyncBackend,
977
688
  left: RemoteHead | undefined,
@@ -0,0 +1,12 @@
1
+ // First-use operation surface for the cached loader; implementations stay lazy.
2
+
3
+ export {
4
+ backupLocal,
5
+ PublicationStatePersistenceError,
6
+ pull,
7
+ push,
8
+ RollbackPublicationError,
9
+ rollback,
10
+ syncBoth,
11
+ } from "./sync-mutations.js";
12
+ export { diff, doctor, history, status } from "./sync-queries.js";
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
- import { isDeniedPath, toPosix } from "./paths.js";
3
- import type { Snapshot, SnapshotSelection } from "./types.js";
2
+ import { isDeniedPath, toPosix } from "../paths.js";
3
+ import type { Snapshot, SnapshotSelection } from "../snapshot/snapshot-types.js";
4
4
 
5
5
  export const BUILT_IN_SYNC_ROOTS = [
6
6
  "settings.json",