@narumitw/pi-sync 0.50.3 → 0.51.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 (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
@@ -3,62 +3,57 @@ import { inspectLock, isLockGuardHeld, isStaleLock, type LockInspection } from "
3
3
  import type { LockFile } from "./state-types.js";
4
4
 
5
5
  export type OperationAvailability =
6
- | { kind: "free" }
7
- | { kind: "live"; lock: LockFile }
8
- | { kind: "busy"; lock?: LockFile; metadata: LockInspection["status"] }
9
- | { kind: "recoverable-stale"; lock: LockFile }
10
- | { kind: "recoverable-unreadable" }
11
- | { kind: "inspection-error"; message: string };
6
+ | { kind: "free" }
7
+ | { kind: "live"; lock: LockFile }
8
+ | { kind: "busy"; lock?: LockFile; metadata: LockInspection["status"] }
9
+ | { kind: "recoverable-stale"; lock: LockFile }
10
+ | { kind: "recoverable-unreadable" }
11
+ | { kind: "inspection-error"; message: string };
12
12
 
13
13
  interface OperationInspectionDependencies {
14
- inspectMetadata(): Promise<LockInspection>;
15
- inspectGuard(): Promise<boolean>;
14
+ inspectMetadata(): Promise<LockInspection>;
15
+ inspectGuard(): Promise<boolean>;
16
16
  }
17
17
 
18
18
  const DEFAULT_INSPECTION_DEPENDENCIES: OperationInspectionDependencies = {
19
- inspectMetadata: inspectLock,
20
- inspectGuard: isLockGuardHeld,
19
+ inspectMetadata: inspectLock,
20
+ inspectGuard: isLockGuardHeld,
21
21
  };
22
22
 
23
23
  export async function inspectOperationAvailability(
24
- dependencies: OperationInspectionDependencies = DEFAULT_INSPECTION_DEPENDENCIES,
24
+ dependencies: OperationInspectionDependencies = DEFAULT_INSPECTION_DEPENDENCIES,
25
25
  ): Promise<OperationAvailability> {
26
- try {
27
- const metadata = await dependencies.inspectMetadata();
28
- const guardHeld = await dependencies.inspectGuard();
29
- return classifyOperationAvailability(metadata, guardHeld);
30
- } catch (error) {
31
- return { kind: "inspection-error", message: errorMessage(error) };
32
- }
26
+ try {
27
+ const metadata = await dependencies.inspectMetadata();
28
+ const guardHeld = await dependencies.inspectGuard();
29
+ return classifyOperationAvailability(metadata, guardHeld);
30
+ } catch (error) {
31
+ return { kind: "inspection-error", message: errorMessage(error) };
32
+ }
33
33
  }
34
34
 
35
- export function classifyOperationAvailability(
36
- metadata: LockInspection,
37
- guardHeld: boolean,
38
- ): OperationAvailability {
39
- if (metadata.status === "valid" && !isStaleLock(metadata.lock)) {
40
- return { kind: "live", lock: metadata.lock };
41
- }
42
- if (guardHeld) {
43
- return {
44
- kind: "busy",
45
- metadata: metadata.status,
46
- ...(metadata.status === "valid" ? { lock: metadata.lock } : {}),
47
- };
48
- }
49
- if (metadata.status === "valid") {
50
- return { kind: "recoverable-stale", lock: metadata.lock };
51
- }
52
- if (metadata.status === "unreadable") return { kind: "recoverable-unreadable" };
53
- return { kind: "free" };
35
+ export function classifyOperationAvailability(metadata: LockInspection, guardHeld: boolean): OperationAvailability {
36
+ if (metadata.status === "valid" && !isStaleLock(metadata.lock)) {
37
+ return { kind: "live", lock: metadata.lock };
38
+ }
39
+ if (guardHeld) {
40
+ return {
41
+ kind: "busy",
42
+ metadata: metadata.status,
43
+ ...(metadata.status === "valid" ? { lock: metadata.lock } : {}),
44
+ };
45
+ }
46
+ if (metadata.status === "valid") {
47
+ return { kind: "recoverable-stale", lock: metadata.lock };
48
+ }
49
+ if (metadata.status === "unreadable") return { kind: "recoverable-unreadable" };
50
+ return { kind: "free" };
54
51
  }
55
52
 
56
53
  export function operationBlocksChanges(availability: OperationAvailability) {
57
- return availability.kind !== "free";
54
+ return availability.kind !== "free";
58
55
  }
59
56
 
60
57
  export function operationCanRecover(availability: OperationAvailability) {
61
- return (
62
- availability.kind === "recoverable-stale" || availability.kind === "recoverable-unreadable"
63
- );
58
+ return availability.kind === "recoverable-stale" || availability.kind === "recoverable-unreadable";
64
59
  }
@@ -13,226 +13,224 @@ const MIGRATION_LOCK_STALE_MS = 30_000;
13
13
  const MIGRATION_LOCK_UPDATE_MS = 10_000;
14
14
 
15
15
  export type StateDirectoryPreparation =
16
- | { status: "ready" }
17
- | { status: "migrated"; message: string }
18
- | { status: "deferred"; message: string };
16
+ | { status: "ready" }
17
+ | { status: "migrated"; message: string }
18
+ | { status: "deferred"; message: string };
19
19
 
20
20
  interface StateDirectoryGuard {
21
- release: () => Promise<void>;
22
- throwIfCompromised: () => void;
21
+ release: () => Promise<void>;
22
+ throwIfCompromised: () => void;
23
23
  }
24
24
 
25
25
  interface SharedStateDirectoryGuard {
26
- guard: Promise<StateDirectoryGuard>;
27
- users: number;
28
- closing?: Promise<void>;
26
+ guard: Promise<StateDirectoryGuard>;
27
+ users: number;
28
+ closing?: Promise<void>;
29
29
  }
30
30
 
31
31
  const sharedStateDirectoryGuards = new Map<string, SharedStateDirectoryGuard>();
32
32
 
33
33
  export function stateDir() {
34
- const roots = inspectStateRoots();
35
- if (roots.canonical) return canonicalStateDir();
36
- if (roots.legacy) return legacyStateDir();
37
- return canonicalStateDir();
34
+ const roots = inspectStateRoots();
35
+ if (roots.canonical) return canonicalStateDir();
36
+ if (roots.legacy) return legacyStateDir();
37
+ return canonicalStateDir();
38
38
  }
39
39
 
40
40
  export function legacyStateDir() {
41
- return path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);
41
+ return path.join(getAgentDir(), LEGACY_DIRECTORY_NAME);
42
42
  }
43
43
 
44
44
  export function stateDirectoryMigrationNotice() {
45
- const roots = inspectStateRoots();
46
- if (!roots.legacy) return undefined;
47
- return "Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.";
45
+ const roots = inspectStateRoots();
46
+ if (!roots.legacy) return undefined;
47
+ return "Legacy pi-sync state is still stored in .pisync. Close other Pi sessions, then run /sync migrate-state to move it to pi-sync/.";
48
48
  }
49
49
 
50
50
  export async function withStateDirectoryAccess<T>(fn: () => Promise<T>): Promise<T> {
51
- const roots = inspectStateRoots();
52
- if (!roots.legacy) return fn();
53
- return runWithStateDirectoryGuard(await acquireSharedStateDirectoryGuard(), fn);
51
+ const roots = inspectStateRoots();
52
+ if (!roots.legacy) return fn();
53
+ return runWithStateDirectoryGuard(await acquireSharedStateDirectoryGuard(), fn);
54
54
  }
55
55
 
56
56
  export async function migrateLegacyStateDirectory(): Promise<StateDirectoryPreparation> {
57
- const initial = inspectStateRoots();
58
- if (initial.canonical || !initial.legacy) return { status: "ready" };
59
-
60
- let guard: StateDirectoryGuard;
61
- try {
62
- guard = await acquireStateDirectoryGuard();
63
- } catch (error) {
64
- if (!isLockHeldError(error)) throw error;
65
- return {
66
- status: "deferred",
67
- message:
68
- "pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry.",
69
- };
70
- }
71
- return runWithStateDirectoryGuard(guard, async () => {
72
- const roots = inspectStateRoots();
73
- if (roots.canonical || !roots.legacy) return { status: "ready" };
74
-
75
- const legacyLock = path.join(legacyStateDir(), "lock");
76
- const legacyGuardHeld = await lockfile.check(legacyLock, {
77
- fs: LOCKFILE_FS_ADAPTER,
78
- lockfilePath: `${legacyLock}.guard`,
79
- realpath: false,
80
- stale: LOCK_GUARD_STALE_MS,
81
- });
82
- if (legacyGuardHeld || (await pathExists(legacyLock))) {
83
- return {
84
- status: "deferred",
85
- message:
86
- "pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry.",
87
- };
88
- }
89
- guard.throwIfCompromised();
90
- await fs.rename(legacyStateDir(), canonicalStateDir());
91
- guard.throwIfCompromised();
92
- inspectStateRoots();
93
- return {
94
- status: "migrated",
95
- message: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`,
96
- };
97
- });
57
+ const initial = inspectStateRoots();
58
+ if (initial.canonical || !initial.legacy) return { status: "ready" };
59
+
60
+ let guard: StateDirectoryGuard;
61
+ try {
62
+ guard = await acquireStateDirectoryGuard();
63
+ } catch (error) {
64
+ if (!isLockHeldError(error)) throw error;
65
+ return {
66
+ status: "deferred",
67
+ message:
68
+ "pi-sync state migration was deferred because another state user is active. Close other Pi sessions and retry.",
69
+ };
70
+ }
71
+ return runWithStateDirectoryGuard(guard, async () => {
72
+ const roots = inspectStateRoots();
73
+ if (roots.canonical || !roots.legacy) return { status: "ready" };
74
+
75
+ const legacyLock = path.join(legacyStateDir(), "lock");
76
+ const legacyGuardHeld = await lockfile.check(legacyLock, {
77
+ fs: LOCKFILE_FS_ADAPTER,
78
+ lockfilePath: `${legacyLock}.guard`,
79
+ realpath: false,
80
+ stale: LOCK_GUARD_STALE_MS,
81
+ });
82
+ if (legacyGuardHeld || (await pathExists(legacyLock))) {
83
+ return {
84
+ status: "deferred",
85
+ message:
86
+ "pi-sync state migration was deferred because the legacy directory is busy. Close other Pi sessions, clear any confirmed stale sync lock, and retry.",
87
+ };
88
+ }
89
+ guard.throwIfCompromised();
90
+ await fs.rename(legacyStateDir(), canonicalStateDir());
91
+ guard.throwIfCompromised();
92
+ inspectStateRoots();
93
+ return {
94
+ status: "migrated",
95
+ message: `Migrated pi-sync state from ${legacyStateDir()} to ${canonicalStateDir()}.`,
96
+ };
97
+ });
98
98
  }
99
99
 
100
100
  async function acquireSharedStateDirectoryGuard(): Promise<StateDirectoryGuard> {
101
- const key = migrationLockPath();
102
- for (;;) {
103
- let shared = sharedStateDirectoryGuards.get(key);
104
- if (shared?.closing) {
105
- await shared.closing;
106
- continue;
107
- }
108
- if (!shared) {
109
- shared = { guard: acquireStateDirectoryGuard(), users: 0 };
110
- sharedStateDirectoryGuards.set(key, shared);
111
- }
112
- shared.users += 1;
113
- let guard: StateDirectoryGuard;
114
- try {
115
- guard = await shared.guard;
116
- } catch (error) {
117
- if (sharedStateDirectoryGuards.get(key) === shared) {
118
- sharedStateDirectoryGuards.delete(key);
119
- }
120
- throw error;
121
- }
122
- let released = false;
123
- return {
124
- throwIfCompromised: guard.throwIfCompromised,
125
- release: async () => {
126
- if (released) return;
127
- released = true;
128
- shared.users -= 1;
129
- if (shared.users > 0) return;
130
- const closing = guard.release();
131
- shared.closing = closing;
132
- try {
133
- await closing;
134
- } finally {
135
- if (sharedStateDirectoryGuards.get(key) === shared) {
136
- sharedStateDirectoryGuards.delete(key);
137
- }
138
- }
139
- },
140
- };
141
- }
101
+ const key = migrationLockPath();
102
+ for (;;) {
103
+ let shared = sharedStateDirectoryGuards.get(key);
104
+ if (shared?.closing) {
105
+ await shared.closing;
106
+ continue;
107
+ }
108
+ if (!shared) {
109
+ shared = { guard: acquireStateDirectoryGuard(), users: 0 };
110
+ sharedStateDirectoryGuards.set(key, shared);
111
+ }
112
+ shared.users += 1;
113
+ let guard: StateDirectoryGuard;
114
+ try {
115
+ guard = await shared.guard;
116
+ } catch (error) {
117
+ if (sharedStateDirectoryGuards.get(key) === shared) {
118
+ sharedStateDirectoryGuards.delete(key);
119
+ }
120
+ throw error;
121
+ }
122
+ let released = false;
123
+ return {
124
+ throwIfCompromised: guard.throwIfCompromised,
125
+ release: async () => {
126
+ if (released) return;
127
+ released = true;
128
+ shared.users -= 1;
129
+ if (shared.users > 0) return;
130
+ const closing = guard.release();
131
+ shared.closing = closing;
132
+ try {
133
+ await closing;
134
+ } finally {
135
+ if (sharedStateDirectoryGuards.get(key) === shared) {
136
+ sharedStateDirectoryGuards.delete(key);
137
+ }
138
+ }
139
+ },
140
+ };
141
+ }
142
142
  }
143
143
 
144
144
  async function acquireStateDirectoryGuard(): Promise<StateDirectoryGuard> {
145
- let compromisedError: Error | undefined;
146
- const release = await lockfile.lock(getAgentDir(), {
147
- fs: LOCKFILE_FS_ADAPTER,
148
- lockfilePath: migrationLockPath(),
149
- realpath: false,
150
- stale: MIGRATION_LOCK_STALE_MS,
151
- update: MIGRATION_LOCK_UPDATE_MS,
152
- retries: { retries: 20, minTimeout: 10, maxTimeout: 50 },
153
- onCompromised: (error) => {
154
- compromisedError = error;
155
- },
156
- });
157
- return {
158
- release,
159
- throwIfCompromised: () => {
160
- if (compromisedError) throw compromisedError;
161
- },
162
- };
145
+ let compromisedError: Error | undefined;
146
+ const release = await lockfile.lock(getAgentDir(), {
147
+ fs: LOCKFILE_FS_ADAPTER,
148
+ lockfilePath: migrationLockPath(),
149
+ realpath: false,
150
+ stale: MIGRATION_LOCK_STALE_MS,
151
+ update: MIGRATION_LOCK_UPDATE_MS,
152
+ retries: { retries: 20, minTimeout: 10, maxTimeout: 50 },
153
+ onCompromised: (error) => {
154
+ compromisedError = error;
155
+ },
156
+ });
157
+ return {
158
+ release,
159
+ throwIfCompromised: () => {
160
+ if (compromisedError) throw compromisedError;
161
+ },
162
+ };
163
163
  }
164
164
 
165
165
  async function runWithStateDirectoryGuard<T>(guard: StateDirectoryGuard, fn: () => Promise<T>) {
166
- let result: T | undefined;
167
- let failed = false;
168
- let failure: unknown;
169
- try {
170
- guard.throwIfCompromised();
171
- result = await fn();
172
- guard.throwIfCompromised();
173
- } catch (error) {
174
- failed = true;
175
- failure = error;
176
- }
177
- try {
178
- await guard.release();
179
- } catch (error) {
180
- if (!failed) {
181
- failed = true;
182
- failure = error;
183
- }
184
- }
185
- if (failed) throw failure;
186
- return result as T;
166
+ let result: T | undefined;
167
+ let failed = false;
168
+ let failure: unknown;
169
+ try {
170
+ guard.throwIfCompromised();
171
+ result = await fn();
172
+ guard.throwIfCompromised();
173
+ } catch (error) {
174
+ failed = true;
175
+ failure = error;
176
+ }
177
+ try {
178
+ await guard.release();
179
+ } catch (error) {
180
+ if (!failed) {
181
+ failed = true;
182
+ failure = error;
183
+ }
184
+ }
185
+ if (failed) throw failure;
186
+ return result as T;
187
187
  }
188
188
 
189
189
  function isLockHeldError(error: unknown) {
190
- return (error as NodeJS.ErrnoException).code === "ELOCKED";
190
+ return (error as NodeJS.ErrnoException).code === "ELOCKED";
191
191
  }
192
192
 
193
193
  function canonicalStateDir() {
194
- return path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);
194
+ return path.join(getAgentDir(), CANONICAL_DIRECTORY_NAME);
195
195
  }
196
196
 
197
197
  function migrationLockPath() {
198
- return path.join(getAgentDir(), MIGRATION_LOCK_NAME);
198
+ return path.join(getAgentDir(), MIGRATION_LOCK_NAME);
199
199
  }
200
200
 
201
201
  function inspectStateRoots() {
202
- const canonical = isDirectoryRoot(canonicalStateDir(), "canonical");
203
- const legacy = isDirectoryRoot(legacyStateDir(), "legacy");
204
- if (canonical && legacy) {
205
- throw new Error(
206
- `Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`,
207
- );
208
- }
209
- return { canonical, legacy };
202
+ const canonical = isDirectoryRoot(canonicalStateDir(), "canonical");
203
+ const legacy = isDirectoryRoot(legacyStateDir(), "legacy");
204
+ if (canonical && legacy) {
205
+ throw new Error(
206
+ `Both legacy ${legacyStateDir()} and canonical ${canonicalStateDir()} pi-sync state directories exist. Close all Pi sessions and reconcile the directories manually; pi-sync will not merge or delete either directory.`,
207
+ );
208
+ }
209
+ return { canonical, legacy };
210
210
  }
211
211
 
212
212
  function isDirectoryRoot(directory: string, label: string) {
213
- try {
214
- const entry = lstatSync(directory);
215
- if (entry.isSymbolicLink()) {
216
- throw new Error(
217
- `Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`,
218
- );
219
- }
220
- if (!entry.isDirectory()) {
221
- throw new Error(`${label} pi-sync state path is not a directory: ${directory}`);
222
- }
223
- return true;
224
- } catch (error) {
225
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
226
- throw error;
227
- }
213
+ try {
214
+ const entry = lstatSync(directory);
215
+ if (entry.isSymbolicLink()) {
216
+ throw new Error(`Refusing to use ${label} pi-sync state directory symbolic link: ${directory}`);
217
+ }
218
+ if (!entry.isDirectory()) {
219
+ throw new Error(`${label} pi-sync state path is not a directory: ${directory}`);
220
+ }
221
+ return true;
222
+ } catch (error) {
223
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
224
+ throw error;
225
+ }
228
226
  }
229
227
 
230
228
  async function pathExists(filePath: string) {
231
- try {
232
- await fs.lstat(filePath);
233
- return true;
234
- } catch (error) {
235
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
236
- throw error;
237
- }
229
+ try {
230
+ await fs.lstat(filePath);
231
+ return true;
232
+ } catch (error) {
233
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
234
+ throw error;
235
+ }
238
236
  }
@@ -1,20 +1,20 @@
1
1
  export interface SyncState {
2
- version: number;
3
- profile: string;
4
- lastAppliedSnapshot?: string;
5
- lastRemoteRevision?: string;
6
- lastRemoteEtag?: string;
7
- lastFileHashes: Record<string, string>;
8
- include?: string[];
9
- /** Legacy state fields are read only so v3 can detect and replace stale policy state. */
10
- syncFiles?: string[];
11
- syncSessions?: boolean;
12
- extraFiles?: string[];
2
+ version: number;
3
+ profile: string;
4
+ lastAppliedSnapshot?: string;
5
+ lastRemoteRevision?: string;
6
+ lastRemoteEtag?: string;
7
+ lastFileHashes: Record<string, string>;
8
+ include?: string[];
9
+ /** Legacy state fields are read only so v3 can detect and replace stale policy state. */
10
+ syncFiles?: string[];
11
+ syncSessions?: boolean;
12
+ extraFiles?: string[];
13
13
  }
14
14
 
15
15
  export interface LockFile {
16
- id: string;
17
- pid: number;
18
- command: string;
19
- startedAt: string;
16
+ id: string;
17
+ pid: number;
18
+ command: string;
19
+ startedAt: string;
20
20
  }
@@ -12,51 +12,51 @@ const STATE_VERSION = 2;
12
12
 
13
13
  /** Bounded observation token; do not retain the full baseline in passive UI state. */
14
14
  export function syncStateFingerprint(state: SyncState) {
15
- return createHash("sha256").update(JSON.stringify(state)).digest("hex");
15
+ return createHash("sha256").update(JSON.stringify(state)).digest("hex");
16
16
  }
17
17
 
18
18
  export async function readState(profile: string): Promise<SyncState> {
19
- return (
20
- (await readJsonIfExists<SyncState>(statePath(profile))) ?? {
21
- version: STATE_VERSION,
22
- profile,
23
- lastFileHashes: {},
24
- }
25
- );
19
+ return (
20
+ (await readJsonIfExists<SyncState>(statePath(profile))) ?? {
21
+ version: STATE_VERSION,
22
+ profile,
23
+ lastFileHashes: {},
24
+ }
25
+ );
26
26
  }
27
27
 
28
28
  export async function writeState(profile: string, state: SyncState) {
29
- await writeJson(statePath(profile), state);
29
+ await writeJson(statePath(profile), state);
30
30
  }
31
31
 
32
32
  export async function readStateForConfig(config: AnySyncConfig): Promise<SyncState> {
33
- return (
34
- (await readJsonIfExists<SyncState>(statePathForConfig(config))) ?? {
35
- version: STATE_VERSION,
36
- profile: config.snapshotIdentity,
37
- lastFileHashes: {},
38
- }
39
- );
33
+ return (
34
+ (await readJsonIfExists<SyncState>(statePathForConfig(config))) ?? {
35
+ version: STATE_VERSION,
36
+ profile: config.snapshotIdentity,
37
+ lastFileHashes: {},
38
+ }
39
+ );
40
40
  }
41
41
 
42
42
  export async function writeStateForConfig(config: AnySyncConfig, state: SyncState) {
43
- await writeJson(statePathForConfig(config), state);
43
+ await writeJson(statePathForConfig(config), state);
44
44
  }
45
45
 
46
46
  export function statePathForConfig(config: AnySyncConfig) {
47
- const identity = backendIdentityCoordinates(config);
48
- const hash = createHash("sha256").update(identity).digest("hex").slice(0, 16);
49
- return path.join(stateDir(), "setups", `${config.backend.type}-${hash}.state.json`);
47
+ const identity = backendIdentityCoordinates(config);
48
+ const hash = createHash("sha256").update(identity).digest("hex").slice(0, 16);
49
+ return path.join(stateDir(), "setups", `${config.backend.type}-${hash}.state.json`);
50
50
  }
51
51
 
52
52
  function statePath(profile: string) {
53
- return path.join(stateDir(), `${safeName(profile)}.state.json`);
53
+ return path.join(stateDir(), `${safeName(profile)}.state.json`);
54
54
  }
55
55
 
56
56
  export function lockPath() {
57
- return path.join(stateDir(), "lock");
57
+ return path.join(stateDir(), "lock");
58
58
  }
59
59
 
60
60
  export async function ensureStateDir() {
61
- await fs.mkdir(stateDir(), { recursive: true });
61
+ await fs.mkdir(stateDir(), { recursive: true });
62
62
  }