@narumitw/pi-sync 0.51.1 → 0.52.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 (181) hide show
  1. package/README.md +30 -5
  2. package/dist/chunks/{chunk-7KHKWL3Q.ts → chunk-2CBF6SMT.ts} +8 -7
  3. package/dist/chunks/chunk-2CBF6SMT.ts.map +7 -0
  4. package/dist/chunks/{chunk-YFADF5ZQ.ts → chunk-2DI6PDZV.ts} +5 -1
  5. package/dist/chunks/chunk-2DI6PDZV.ts.map +7 -0
  6. package/dist/chunks/chunk-2GUXI2XT.ts +1702 -0
  7. package/dist/chunks/chunk-2GUXI2XT.ts.map +7 -0
  8. package/dist/chunks/{chunk-4MPIH46N.ts → chunk-3B4Z2SKV.ts} +40 -6
  9. package/dist/chunks/chunk-3B4Z2SKV.ts.map +7 -0
  10. package/dist/chunks/{chunk-YPXGU5OR.ts → chunk-4QVPTIZB.ts} +16 -6
  11. package/dist/chunks/{chunk-YPXGU5OR.ts.map → chunk-4QVPTIZB.ts.map} +2 -2
  12. package/dist/chunks/{chunk-UA7RXOIT.ts → chunk-4SBTAVFL.ts} +6 -6
  13. package/dist/chunks/{chunk-6546TS54.ts → chunk-5RZPINUQ.ts} +7 -4
  14. package/dist/chunks/{chunk-6546TS54.ts.map → chunk-5RZPINUQ.ts.map} +2 -2
  15. package/dist/chunks/chunk-76NB4R7U.ts +107 -0
  16. package/dist/chunks/chunk-76NB4R7U.ts.map +7 -0
  17. package/dist/chunks/{chunk-DM4X4FIA.ts → chunk-7D6RDCVC.ts} +8 -10
  18. package/dist/chunks/{chunk-DM4X4FIA.ts.map → chunk-7D6RDCVC.ts.map} +2 -2
  19. package/dist/chunks/chunk-A6HU4ERM.ts +124 -0
  20. package/dist/chunks/chunk-A6HU4ERM.ts.map +7 -0
  21. package/dist/chunks/chunk-AMJRUXFT.ts +17 -0
  22. package/dist/chunks/chunk-AMJRUXFT.ts.map +7 -0
  23. package/dist/chunks/chunk-ANWNQIR7.ts +1013 -0
  24. package/dist/chunks/chunk-ANWNQIR7.ts.map +7 -0
  25. package/dist/chunks/{chunk-T56RNRZJ.ts → chunk-BDB4IDM3.ts} +19 -10
  26. package/dist/chunks/chunk-BDB4IDM3.ts.map +7 -0
  27. package/dist/chunks/{chunk-2RZSQWBH.ts → chunk-CHSIAC4N.ts} +5 -5
  28. package/dist/chunks/{chunk-2RZSQWBH.ts.map → chunk-CHSIAC4N.ts.map} +2 -2
  29. package/dist/chunks/chunk-HUZ5CR4W.ts +110 -0
  30. package/dist/chunks/chunk-HUZ5CR4W.ts.map +7 -0
  31. package/dist/chunks/{chunk-QLSN7AGX.ts → chunk-IJ6VV6FN.ts} +81 -19
  32. package/dist/chunks/chunk-IJ6VV6FN.ts.map +7 -0
  33. package/dist/chunks/chunk-ORQRHEEO.ts +362 -0
  34. package/dist/chunks/chunk-ORQRHEEO.ts.map +7 -0
  35. package/dist/chunks/chunk-QE7NZZWT.ts +380 -0
  36. package/dist/chunks/chunk-QE7NZZWT.ts.map +7 -0
  37. package/dist/chunks/{chunk-HGJH56KR.ts → chunk-QHSQCK7L.ts} +4 -4
  38. package/dist/chunks/chunk-QOFMXPM5.ts +735 -0
  39. package/dist/chunks/chunk-QOFMXPM5.ts.map +7 -0
  40. package/dist/chunks/{chunk-DZUSTGQV.ts → chunk-UQ3ALBIB.ts} +4 -4
  41. package/dist/chunks/{chunk-Q32CQWPM.ts → chunk-V6VZ5TLG.ts} +13 -6
  42. package/dist/chunks/chunk-V6VZ5TLG.ts.map +7 -0
  43. package/dist/chunks/{chunk-E42GXD6D.ts → chunk-WKPX2SIC.ts} +4 -4
  44. package/dist/chunks/chunk-ZW5ZLGBI.ts +154 -0
  45. package/dist/chunks/chunk-ZW5ZLGBI.ts.map +7 -0
  46. package/dist/chunks/{chunk-6NT3YFGY.ts → chunk-ZX2Q6THB.ts} +8 -3
  47. package/dist/chunks/{chunk-6NT3YFGY.ts.map → chunk-ZX2Q6THB.ts.map} +2 -2
  48. package/dist/chunks/conflict-artifacts-BB2FZMIE.ts +27 -0
  49. package/dist/chunks/conflict-review-T3PB7ZSD.ts +175 -0
  50. package/dist/chunks/conflict-review-T3PB7ZSD.ts.map +7 -0
  51. package/dist/chunks/{file-selection-RAVHRYFG.ts → file-selection-WZX7HAY2.ts} +8 -7
  52. package/dist/chunks/{file-selection-RAVHRYFG.ts.map → file-selection-WZX7HAY2.ts.map} +1 -1
  53. package/dist/chunks/{git-backend-IAANDL3K.ts → git-backend-HLGKUQJT.ts} +19 -7
  54. package/dist/chunks/git-backend-HLGKUQJT.ts.map +7 -0
  55. package/dist/chunks/local-fields-6DGYOOIK.ts +22 -0
  56. package/dist/chunks/manager-result-dispatcher-VIDT3QBU.ts +23 -0
  57. package/dist/chunks/{manager-ui-763NEKJY.ts → manager-ui-HSMHS3OK.ts} +236 -57
  58. package/dist/chunks/manager-ui-HSMHS3OK.ts.map +7 -0
  59. package/dist/chunks/merge-baseline-store-EVQAAML6.ts +22 -0
  60. package/dist/chunks/remote-selection-ui-SMF5CLFO.ts +22 -0
  61. package/dist/chunks/{s3-backend-XGXUOPUN.ts → s3-backend-DJ2NT3SZ.ts} +16 -9
  62. package/dist/chunks/s3-backend-DJ2NT3SZ.ts.map +7 -0
  63. package/dist/chunks/{setup-switch-5G2VNJHL.ts → setup-switch-KTUWL6DE.ts} +9 -6
  64. package/dist/chunks/{setup-wizard-YIEWGX32.ts → setup-wizard-GSDPDFC4.ts} +10 -7
  65. package/dist/chunks/{setup-wizard-YIEWGX32.ts.map → setup-wizard-GSDPDFC4.ts.map} +1 -1
  66. package/dist/chunks/{snapshot-JL5DFSXI.ts → snapshot-THS5EY2L.ts} +6 -9
  67. package/dist/chunks/snapshot-THS5EY2L.ts.map +7 -0
  68. package/dist/chunks/{sync-inspection-P3ENJLNF.ts → sync-inspection-VBJF2FWC.ts} +10 -10
  69. package/dist/chunks/sync-inspection-VBJF2FWC.ts.map +7 -0
  70. package/dist/chunks/{sync-operations-AR76EB6C.ts → sync-operations-27VQUDH2.ts} +309 -508
  71. package/dist/chunks/sync-operations-27VQUDH2.ts.map +7 -0
  72. package/dist/chunks/{sync-state-VHJUZ4YG.ts → sync-state-CFL4RRRB.ts} +6 -6
  73. package/dist/chunks/sync-state-CFL4RRRB.ts.map +7 -0
  74. package/dist/chunks/{webdav-backend-3EGUONAT.ts → webdav-backend-ETQGSI6I.ts} +13 -7
  75. package/dist/chunks/webdav-backend-ETQGSI6I.ts.map +7 -0
  76. package/dist/index.ts +142 -43
  77. package/dist/index.ts.map +2 -2
  78. package/docs/content-merge-audit.md +156 -0
  79. package/docs/merge-implementation-audit.md +312 -0
  80. package/docs/settings-merge-audit.md +153 -0
  81. package/docs/settings.md +36 -0
  82. package/package.json +5 -5
  83. package/src/backends/git/git-backend.ts +2 -0
  84. package/src/backends/git/git-storage.ts +8 -3
  85. package/src/backends/s3/s3-backend.ts +2 -1
  86. package/src/backends/webdav/webdav-backend.ts +2 -1
  87. package/src/commands/command-execution.ts +10 -0
  88. package/src/commands/command.ts +4 -0
  89. package/src/settings/config.ts +12 -0
  90. package/src/settings/settings-management.ts +2 -2
  91. package/src/settings/settings-types.ts +14 -1
  92. package/src/settings/settings-validation.ts +24 -3
  93. package/src/snapshot/session-paths.ts +34 -7
  94. package/src/snapshot/snapshot-apply.ts +81 -32
  95. package/src/snapshot/snapshot-case-replacement.ts +80 -0
  96. package/src/snapshot/snapshot-codec.ts +6 -1
  97. package/src/snapshot/snapshot-root-transition.ts +154 -0
  98. package/src/snapshot/snapshot-transaction-plan.ts +72 -0
  99. package/src/snapshot/snapshot-transaction.ts +844 -102
  100. package/src/snapshot/snapshot-types.ts +5 -0
  101. package/src/state/json-file.ts +32 -5
  102. package/src/state/merge-baseline-store.ts +182 -0
  103. package/src/state/mutation-directory-sync.ts +34 -0
  104. package/src/state/state-types.ts +4 -0
  105. package/src/state/sync-state-store.ts +14 -1
  106. package/src/sync/automatic-sync.ts +11 -3
  107. package/src/sync/conflict-artifact-ownership.ts +63 -0
  108. package/src/sync/conflict-artifacts.ts +195 -0
  109. package/src/sync/conflict-groups.ts +68 -0
  110. package/src/sync/conflict-preview.ts +56 -0
  111. package/src/sync/conflict-resolution.ts +93 -0
  112. package/src/sync/conflict-retention.ts +72 -0
  113. package/src/sync/conflict-review.ts +95 -0
  114. package/src/sync/content-conflicts.ts +73 -0
  115. package/src/sync/field-migration.ts +39 -0
  116. package/src/sync/file-merge-planner.ts +143 -0
  117. package/src/sync/json-document.ts +184 -0
  118. package/src/sync/local-fields.ts +134 -0
  119. package/src/sync/merge-apply.ts +380 -0
  120. package/src/sync/merge-journal.ts +219 -0
  121. package/src/sync/merged-sync.ts +723 -0
  122. package/src/sync/partial-progress.ts +54 -0
  123. package/src/sync/remote-snapshot.ts +6 -0
  124. package/src/sync/session-merge.ts +194 -0
  125. package/src/sync/settings-conflicts.ts +52 -0
  126. package/src/sync/settings-merge.ts +57 -0
  127. package/src/sync/startup-check.ts +90 -5
  128. package/src/sync/sync-inspection.ts +1 -0
  129. package/src/sync/sync-local.ts +61 -0
  130. package/src/sync/sync-mutations.ts +256 -114
  131. package/src/sync/sync-queries.ts +6 -2
  132. package/src/sync/sync-state.ts +14 -5
  133. package/src/sync/text-merge.ts +133 -0
  134. package/src/sync-extension.ts +8 -0
  135. package/src/ui/manager-state.ts +12 -1
  136. package/src/ui/manager-ui.ts +10 -0
  137. package/src/ui/merge-review.ts +28 -0
  138. package/src/ui/remote-selection-ui.ts +1 -0
  139. package/src/ui/settings-ui.ts +183 -2
  140. package/src/ui/setup/setup-actions.ts +2 -2
  141. package/src/ui/setup/setup-switcher.ts +2 -1
  142. package/src/ui/storage-connections-ui.ts +11 -7
  143. package/src/ui/sync-attention.ts +3 -0
  144. package/src/ui/sync-format.ts +16 -12
  145. package/src/ui/sync-setups-ui.ts +1 -0
  146. package/src/ui/terminal-text.ts +9 -2
  147. package/dist/chunks/chunk-4AIHXRKE.ts +0 -197
  148. package/dist/chunks/chunk-4AIHXRKE.ts.map +0 -7
  149. package/dist/chunks/chunk-4MPIH46N.ts.map +0 -7
  150. package/dist/chunks/chunk-7KHKWL3Q.ts.map +0 -7
  151. package/dist/chunks/chunk-EEJPYTWE.ts +0 -348
  152. package/dist/chunks/chunk-EEJPYTWE.ts.map +0 -7
  153. package/dist/chunks/chunk-IVXEDPAD.ts +0 -370
  154. package/dist/chunks/chunk-IVXEDPAD.ts.map +0 -7
  155. package/dist/chunks/chunk-MSSW6F3R.ts +0 -12
  156. package/dist/chunks/chunk-MSSW6F3R.ts.map +0 -7
  157. package/dist/chunks/chunk-OVDTF5CH.ts +0 -90
  158. package/dist/chunks/chunk-OVDTF5CH.ts.map +0 -7
  159. package/dist/chunks/chunk-Q32CQWPM.ts.map +0 -7
  160. package/dist/chunks/chunk-QLSN7AGX.ts.map +0 -7
  161. package/dist/chunks/chunk-SWUXA3MH.ts +0 -18
  162. package/dist/chunks/chunk-SWUXA3MH.ts.map +0 -7
  163. package/dist/chunks/chunk-T56RNRZJ.ts.map +0 -7
  164. package/dist/chunks/chunk-YFADF5ZQ.ts.map +0 -7
  165. package/dist/chunks/git-backend-IAANDL3K.ts.map +0 -7
  166. package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +0 -23
  167. package/dist/chunks/manager-ui-763NEKJY.ts.map +0 -7
  168. package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +0 -22
  169. package/dist/chunks/s3-backend-XGXUOPUN.ts.map +0 -7
  170. package/dist/chunks/sync-operations-AR76EB6C.ts.map +0 -7
  171. package/dist/chunks/webdav-backend-3EGUONAT.ts.map +0 -7
  172. /package/dist/chunks/{chunk-UA7RXOIT.ts.map → chunk-4SBTAVFL.ts.map} +0 -0
  173. /package/dist/chunks/{chunk-HGJH56KR.ts.map → chunk-QHSQCK7L.ts.map} +0 -0
  174. /package/dist/chunks/{chunk-DZUSTGQV.ts.map → chunk-UQ3ALBIB.ts.map} +0 -0
  175. /package/dist/chunks/{chunk-E42GXD6D.ts.map → chunk-WKPX2SIC.ts.map} +0 -0
  176. /package/dist/chunks/{manager-result-dispatcher-OTUMQM26.ts.map → conflict-artifacts-BB2FZMIE.ts.map} +0 -0
  177. /package/dist/chunks/{remote-selection-ui-TZTJF32Y.ts.map → local-fields-6DGYOOIK.ts.map} +0 -0
  178. /package/dist/chunks/{setup-switch-5G2VNJHL.ts.map → manager-result-dispatcher-VIDT3QBU.ts.map} +0 -0
  179. /package/dist/chunks/{snapshot-JL5DFSXI.ts.map → merge-baseline-store-EVQAAML6.ts.map} +0 -0
  180. /package/dist/chunks/{sync-inspection-P3ENJLNF.ts.map → remote-selection-ui-SMF5CLFO.ts.map} +0 -0
  181. /package/dist/chunks/{sync-state-VHJUZ4YG.ts.map → setup-switch-KTUWL6DE.ts.map} +0 -0
@@ -1,18 +1,77 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ throwIfAborted
5
+ } from "./chunk-PWMWOJP4.ts";
3
6
  import {
4
7
  ensureStateDir,
5
8
  lockPath
6
- } from "./chunk-YPXGU5OR.ts";
9
+ } from "./chunk-4QVPTIZB.ts";
7
10
  import {
8
11
  LOCKFILE_FS_ADAPTER,
9
12
  LOCK_GUARD_STALE_MS,
10
- LOCK_GUARD_UPDATE_MS
13
+ LOCK_GUARD_UPDATE_MS,
14
+ stateDir
11
15
  } from "./chunk-BVVP33U5.ts";
16
+ import {
17
+ encodeSnapshot
18
+ } from "./chunk-ZX2Q6THB.ts";
19
+ import {
20
+ agentDir,
21
+ createSnapshot,
22
+ sessionDirFromContext,
23
+ sessionSnapshotPathFromAbsolute
24
+ } from "./chunk-QOFMXPM5.ts";
25
+ import {
26
+ syncDirectory
27
+ } from "./chunk-HUZ5CR4W.ts";
28
+
29
+ // src/sync/sync-local.ts
30
+ import fs from "node:fs/promises";
31
+ import path from "node:path";
32
+ function protectedSessionPaths(ctx, sessionRoot = sessionDirFromContext(ctx)) {
33
+ const getSessionFile = ctx.sessionManager.getSessionFile;
34
+ if (typeof getSessionFile !== "function") return /* @__PURE__ */ new Set();
35
+ const sessionFile = getSessionFile.call(ctx.sessionManager);
36
+ const snapshotPath = sessionFile ? sessionSnapshotPathFromAbsolute(sessionFile, sessionRoot) : void 0;
37
+ return snapshotPath ? /* @__PURE__ */ new Set([snapshotPath]) : /* @__PURE__ */ new Set();
38
+ }
39
+ function captureMutationOwner(ctx, signal) {
40
+ const manager = ctx.sessionManager;
41
+ const file = manager.getSessionFile?.();
42
+ const id = manager.getSessionId?.();
43
+ const cwd = manager.getCwd?.();
44
+ const root = agentDir();
45
+ return () => {
46
+ throwIfAborted(signal);
47
+ if (ctx.sessionManager !== manager || manager.getSessionFile?.() !== file || manager.getSessionId?.() !== id || manager.getCwd?.() !== cwd || agentDir() !== root)
48
+ throw new Error("Session changed during file mutation; evidence retained for review.");
49
+ };
50
+ }
51
+ async function backupLocal(profile, options = {}, signal) {
52
+ throwIfAborted(signal);
53
+ const snapshot = await createSnapshot(profile, { ...options, signal });
54
+ throwIfAborted(signal);
55
+ const backupDirectory = path.join(stateDir(), "backups");
56
+ await fs.mkdir(backupDirectory, { recursive: true, mode: 448 });
57
+ throwIfAborted(signal);
58
+ const backupPath = path.join(backupDirectory, `${snapshot.id}.json.gz`);
59
+ const encoded = await encodeSnapshot(snapshot);
60
+ throwIfAborted(signal);
61
+ const handle = await fs.open(backupPath, "wx", 384);
62
+ try {
63
+ await handle.writeFile(encoded);
64
+ await handle.sync();
65
+ } finally {
66
+ await handle.close();
67
+ }
68
+ await syncDirectory(backupDirectory);
69
+ return backupPath;
70
+ }
12
71
 
13
72
  // src/state/lock.ts
14
73
  import { randomUUID } from "node:crypto";
15
- import fs from "node:fs/promises";
74
+ import fs2 from "node:fs/promises";
16
75
  import lockfile from "proper-lockfile";
17
76
  var LOCK_STALE_MS = 30 * 60 * 1e3;
18
77
  var MAX_PROCESS_ID = 2147483647;
@@ -45,7 +104,7 @@ async function withLock(command, fn, options = {}) {
45
104
  if (rechecked.status !== "valid" || rechecked.lock.id !== inspection.lock.id || !isStaleLock(rechecked.lock)) {
46
105
  throw new Error("pi-sync lock changed while preparing transaction recovery; retry.");
47
106
  }
48
- await fs.rm(lockPath(), { force: true });
107
+ await fs2.rm(lockPath(), { force: true });
49
108
  inspection = { status: "missing" };
50
109
  }
51
110
  if (inspection.status === "valid") {
@@ -58,7 +117,7 @@ async function withLock(command, fn, options = {}) {
58
117
  "pi-sync lock metadata is unreadable. Run /sync unlock --stale after verifying no sync is running."
59
118
  );
60
119
  }
61
- await fs.writeFile(lockPath(), JSON.stringify(lock, null, " "), { flag: "wx" });
120
+ await fs2.writeFile(lockPath(), JSON.stringify(lock, null, " "), { flag: "wx" });
62
121
  guard.throwIfCompromised();
63
122
  result = await fn();
64
123
  guard.throwIfCompromised();
@@ -68,7 +127,7 @@ async function withLock(command, fn, options = {}) {
68
127
  }
69
128
  try {
70
129
  const current = await readLock();
71
- if (current?.id === lock.id) await fs.rm(lockPath(), { force: true });
130
+ if (current?.id === lock.id) await fs2.rm(lockPath(), { force: true });
72
131
  } catch (error) {
73
132
  if (!failed) {
74
133
  failed = true;
@@ -87,7 +146,7 @@ async function withLock(command, fn, options = {}) {
87
146
  }
88
147
  async function inspectLock() {
89
148
  try {
90
- const text = await fs.readFile(lockPath(), "utf8");
149
+ const text = await fs2.readFile(lockPath(), "utf8");
91
150
  if (text.trim().length === 0) return { status: "unreadable" };
92
151
  const parsed = JSON.parse(text);
93
152
  return isLockFile(parsed) ? { status: "valid", lock: parsed } : { status: "unreadable" };
@@ -119,9 +178,9 @@ function isStaleLock(lock) {
119
178
  }
120
179
  }
121
180
  async function unlock(ctx, options) {
122
- throwIfAborted(options.signal);
181
+ throwIfAborted2(options.signal);
123
182
  await ensureStateDir();
124
- throwIfAborted(options.signal);
183
+ throwIfAborted2(options.signal);
125
184
  let guard;
126
185
  try {
127
186
  guard = await acquireGuard();
@@ -146,9 +205,9 @@ async function unlock(ctx, options) {
146
205
  if (failed) throw failure;
147
206
  }
148
207
  async function unlockGuarded(ctx, options) {
149
- throwIfAborted(options.signal);
208
+ throwIfAborted2(options.signal);
150
209
  let inspection = await inspectLock();
151
- throwIfAborted(options.signal);
210
+ throwIfAborted2(options.signal);
152
211
  if (inspection.status === "missing") {
153
212
  ctx.ui.notify("No pi-sync lock is present.", "info");
154
213
  return;
@@ -162,10 +221,10 @@ async function unlockGuarded(ctx, options) {
162
221
  return;
163
222
  }
164
223
  inspection = await inspectLock();
165
- throwIfAborted(options.signal);
224
+ throwIfAborted2(options.signal);
166
225
  if (inspection.status === "unreadable") {
167
- throwIfAborted(options.signal);
168
- await fs.rm(lockPath(), { force: true });
226
+ throwIfAborted2(options.signal);
227
+ await fs2.rm(lockPath(), { force: true });
169
228
  if (!options.signal?.aborted) {
170
229
  ctx.ui.notify(
171
230
  "Removed unreadable pi-sync lock. No settings, files, sync state, or remote data were changed.",
@@ -183,13 +242,13 @@ async function unlockGuarded(ctx, options) {
183
242
  ctx.ui.notify("Lock owner is still live; refusing to remove it.", "warning");
184
243
  return;
185
244
  }
186
- throwIfAborted(options.signal);
187
- await fs.rm(lockPath(), { force: true });
245
+ throwIfAborted2(options.signal);
246
+ await fs2.rm(lockPath(), { force: true });
188
247
  if (!options.signal?.aborted) {
189
248
  ctx.ui.notify("Removed stale pi-sync lock. No settings, files, sync state, or remote data were changed.", "info");
190
249
  }
191
250
  }
192
- function throwIfAborted(signal) {
251
+ function throwIfAborted2(signal) {
193
252
  if (!signal?.aborted) return;
194
253
  throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
195
254
  }
@@ -247,6 +306,9 @@ export {
247
306
  inspectLock,
248
307
  isLockGuardHeld,
249
308
  isStaleLock,
250
- unlock
309
+ unlock,
310
+ protectedSessionPaths,
311
+ captureMutationOwner,
312
+ backupLocal
251
313
  };
252
- //# sourceMappingURL=chunk-QLSN7AGX.ts.map
314
+ //# sourceMappingURL=chunk-IJ6VV6FN.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/sync/sync-local.ts", "../../src/state/lock.ts"],
4
+ "sourcesContent": ["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { ExtensionCommandContext, ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { agentDir, sessionDirFromContext } from \"../snapshot/session-paths.js\";\nimport { createSnapshot, sessionSnapshotPathFromAbsolute } from \"../snapshot/snapshot.js\";\nimport { encodeSnapshot } from \"../snapshot/snapshot-codec.js\";\nimport type { SnapshotOptions } from \"../snapshot/snapshot-types.js\";\nimport { syncDirectory } from \"../state/json-file.js\";\nimport { stateDir } from \"../state/state-directory.js\";\nimport { throwIfAborted } from \"./signals.js\";\n\nexport function protectedSessionPaths(\n ctx: ExtensionCommandContext | ExtensionContext,\n sessionRoot = sessionDirFromContext(ctx),\n) {\n const getSessionFile = ctx.sessionManager.getSessionFile;\n if (typeof getSessionFile !== \"function\") return new Set<string>();\n const sessionFile = getSessionFile.call(ctx.sessionManager) as string | undefined;\n const snapshotPath = sessionFile ? sessionSnapshotPathFromAbsolute(sessionFile, sessionRoot) : undefined;\n return snapshotPath ? new Set([snapshotPath]) : new Set<string>();\n}\n\nexport function captureMutationOwner(ctx: ExtensionContext | ExtensionCommandContext, signal?: AbortSignal) {\n const manager = ctx.sessionManager;\n const file = manager.getSessionFile?.();\n const id = manager.getSessionId?.();\n const cwd = manager.getCwd?.();\n const root = agentDir();\n return () => {\n throwIfAborted(signal);\n if (\n ctx.sessionManager !== manager ||\n manager.getSessionFile?.() !== file ||\n manager.getSessionId?.() !== id ||\n manager.getCwd?.() !== cwd ||\n agentDir() !== root\n )\n throw new Error(\"Session changed during file mutation; evidence retained for review.\");\n };\n}\n\nexport async function backupLocal(profile: string, options: SnapshotOptions = {}, signal?: AbortSignal) {\n throwIfAborted(signal);\n const snapshot = await createSnapshot(profile, { ...options, signal });\n throwIfAborted(signal);\n const backupDirectory = path.join(stateDir(), \"backups\");\n await fs.mkdir(backupDirectory, { recursive: true, mode: 0o700 });\n throwIfAborted(signal);\n const backupPath = path.join(backupDirectory, `${snapshot.id}.json.gz`);\n const encoded = await encodeSnapshot(snapshot);\n throwIfAborted(signal);\n const handle = await fs.open(backupPath, \"wx\", 0o600);\n try {\n await handle.writeFile(encoded);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await syncDirectory(backupDirectory);\n return backupPath;\n}\n", "import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport type { ExtensionCommandContext } from \"@earendil-works/pi-coding-agent\";\nimport lockfile from \"proper-lockfile\";\nimport type { CommandOptions } from \"../commands/command-types.js\";\nimport { LOCK_GUARD_STALE_MS, LOCK_GUARD_UPDATE_MS } from \"./lock-policy.js\";\nimport { LOCKFILE_FS_ADAPTER } from \"./lockfile-fs.js\";\nimport type { LockFile } from \"./state-types.js\";\nimport { ensureStateDir, lockPath } from \"./sync-state-store.js\";\n\nconst LOCK_STALE_MS = 30 * 60 * 1000;\nconst MAX_PROCESS_ID = 2_147_483_647;\n\ninterface Guard {\n release: () => Promise<void>;\n throwIfCompromised: () => void;\n isCompromised: () => boolean;\n}\n\nexport type LockInspection = { status: \"missing\" } | { status: \"unreadable\" } | { status: \"valid\"; lock: LockFile };\n\nexport async function withLock<T>(\n command: string,\n fn: () => Promise<T>,\n options: { reclaimStale?: boolean } = {},\n): Promise<T> {\n await ensureStateDir();\n const lock: LockFile = {\n id: randomUUID(),\n pid: process.pid,\n command,\n startedAt: new Date().toISOString(),\n };\n let guard: Guard | undefined;\n let result: T | undefined;\n let failed = false;\n let failure: unknown;\n try {\n try {\n guard = await acquireGuard();\n } catch (error) {\n if (!isLockHeldError(error)) throw error;\n throw await describeHeldLock();\n }\n\n let inspection = await inspectLock();\n if (inspection.status === \"valid\" && isStaleLock(inspection.lock)) {\n if (!options.reclaimStale) {\n throw new Error(`pi-sync lock is stale (pid ${inspection.lock.pid}). Run /sync unlock --stale, then retry.`);\n }\n guard.throwIfCompromised();\n const rechecked = await inspectLock();\n if (rechecked.status !== \"valid\" || rechecked.lock.id !== inspection.lock.id || !isStaleLock(rechecked.lock)) {\n throw new Error(\"pi-sync lock changed while preparing transaction recovery; retry.\");\n }\n await fs.rm(lockPath(), { force: true });\n inspection = { status: \"missing\" };\n }\n if (inspection.status === \"valid\") {\n throw new Error(\n `pi-sync is already running (${inspection.lock.command}, pid ${inspection.lock.pid}, started ${inspection.lock.startedAt}).`,\n );\n }\n if (inspection.status === \"unreadable\") {\n throw new Error(\n \"pi-sync lock metadata is unreadable. Run /sync unlock --stale after verifying no sync is running.\",\n );\n }\n\n await fs.writeFile(lockPath(), JSON.stringify(lock, null, \"\\t\"), { flag: \"wx\" });\n guard.throwIfCompromised();\n result = await fn();\n guard.throwIfCompromised();\n } catch (error) {\n failed = true;\n failure = error;\n }\n\n try {\n const current = await readLock();\n if (current?.id === lock.id) await fs.rm(lockPath(), { force: true });\n } catch (error) {\n if (!failed) {\n failed = true;\n failure = error;\n }\n }\n if (guard) {\n const releaseError = await releaseGuard(guard);\n if (releaseError && !failed) {\n failed = true;\n failure = releaseError;\n }\n }\n if (failed) throw failure;\n return result as T;\n}\n\nexport async function inspectLock(): Promise<LockInspection> {\n try {\n const text = await fs.readFile(lockPath(), \"utf8\");\n if (text.trim().length === 0) return { status: \"unreadable\" };\n const parsed = JSON.parse(text) as unknown;\n return isLockFile(parsed) ? { status: \"valid\", lock: parsed } : { status: \"unreadable\" };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return { status: \"missing\" };\n if (error instanceof SyntaxError) return { status: \"unreadable\" };\n throw error;\n }\n}\n\nexport async function readLock(): Promise<LockFile | undefined> {\n const inspection = await inspectLock();\n return inspection.status === \"valid\" ? inspection.lock : undefined;\n}\n\nexport async function lockFileExists(): Promise<boolean> {\n try {\n await fs.stat(lockPath());\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return false;\n throw error;\n }\n}\n\nexport function isLockGuardHeld() {\n return lockfile.check(lockPath(), {\n fs: LOCKFILE_FS_ADAPTER,\n lockfilePath: `${lockPath()}.guard`,\n realpath: false,\n stale: LOCK_GUARD_STALE_MS,\n });\n}\n\nexport function isStaleLock(lock: LockFile) {\n try {\n process.kill(lock.pid, 0);\n return false;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ESRCH\") return true;\n return Date.now() - Date.parse(lock.startedAt) > LOCK_STALE_MS;\n }\n}\n\nexport async function unlock(ctx: ExtensionCommandContext, options: CommandOptions) {\n throwIfAborted(options.signal);\n await ensureStateDir();\n throwIfAborted(options.signal);\n let guard: Guard;\n try {\n guard = await acquireGuard();\n } catch (error) {\n if (!isLockHeldError(error)) throw error;\n ctx.ui.notify((await describeHeldLock()).message, \"warning\");\n return;\n }\n\n let failed = false;\n let failure: unknown;\n try {\n await unlockGuarded(ctx, options);\n } catch (error) {\n failed = true;\n failure = error;\n }\n const releaseError = await releaseGuard(guard);\n if (releaseError && !failed) {\n failed = true;\n failure = releaseError;\n }\n if (failed) throw failure;\n}\n\nasync function unlockGuarded(ctx: ExtensionCommandContext, options: CommandOptions) {\n throwIfAborted(options.signal);\n let inspection = await inspectLock();\n throwIfAborted(options.signal);\n if (inspection.status === \"missing\") {\n ctx.ui.notify(\"No pi-sync lock is present.\", \"info\");\n return;\n }\n if (inspection.status === \"unreadable\") {\n if (!options.stale) {\n ctx.ui.notify(\n \"Pi-sync lock metadata is unreadable. Use /sync unlock --stale only after verifying no sync is running.\",\n \"warning\",\n );\n return;\n }\n inspection = await inspectLock();\n throwIfAborted(options.signal);\n if (inspection.status === \"unreadable\") {\n // Legacy writers expose an empty file before writing owner metadata, so no\n // automatic test can prove this file is abandoned. The explicit --stale\n // flag is the user's confirmation that no legacy sync is still running.\n throwIfAborted(options.signal);\n await fs.rm(lockPath(), { force: true });\n if (!options.signal?.aborted) {\n ctx.ui.notify(\n \"Removed unreadable pi-sync lock. No settings, files, sync state, or remote data were changed.\",\n \"info\",\n );\n }\n return;\n }\n if (inspection.status === \"missing\") {\n ctx.ui.notify(\"No pi-sync lock is present.\", \"info\");\n return;\n }\n }\n if (!isStaleLock(inspection.lock)) {\n ctx.ui.notify(\"Lock owner is still live; refusing to remove it.\", \"warning\");\n return;\n }\n throwIfAborted(options.signal);\n await fs.rm(lockPath(), { force: true });\n if (!options.signal?.aborted) {\n ctx.ui.notify(\"Removed stale pi-sync lock. No settings, files, sync state, or remote data were changed.\", \"info\");\n }\n}\n\nfunction throwIfAborted(signal?: AbortSignal) {\n if (!signal?.aborted) return;\n throw signal.reason instanceof Error ? signal.reason : new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n\nasync function releaseGuard(guard: Guard) {\n try {\n await guard.release();\n return undefined;\n } catch (error) {\n return guard.isCompromised() ? undefined : error;\n }\n}\n\nasync function acquireGuard(): Promise<Guard> {\n let compromisedError: Error | undefined;\n const release = await lockfile.lock(lockPath(), {\n fs: LOCKFILE_FS_ADAPTER,\n lockfilePath: `${lockPath()}.guard`,\n realpath: false,\n stale: LOCK_GUARD_STALE_MS,\n update: LOCK_GUARD_UPDATE_MS,\n onCompromised: (error) => {\n compromisedError = error;\n },\n });\n return {\n release,\n throwIfCompromised: () => {\n if (compromisedError) throw compromisedError;\n },\n isCompromised: () => compromisedError !== undefined,\n };\n}\n\nasync function describeHeldLock() {\n const current = await readLock();\n if (current && isStaleLock(current)) {\n return new Error(\"pi-sync lock owner exited; retry shortly while the lock guard expires.\");\n }\n if (current) {\n return new Error(\n `Pi-sync is currently running (${current.command}, pid ${current.pid}, started ${current.startedAt}).`,\n );\n }\n return new Error(\"Pi-sync is currently running (lock metadata is unreadable or still being written).\");\n}\n\nfunction isLockHeldError(error: unknown) {\n return (error as NodeJS.ErrnoException).code === \"ELOCKED\";\n}\n\nfunction isLockFile(value: unknown): value is LockFile {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const lock = value as Partial<LockFile>;\n return (\n typeof lock.id === \"string\" &&\n lock.id.length > 0 &&\n Number.isInteger(lock.pid) &&\n (lock.pid ?? 0) > 0 &&\n (lock.pid ?? 0) <= MAX_PROCESS_ID &&\n typeof lock.command === \"string\" &&\n lock.command.length > 0 &&\n typeof lock.startedAt === \"string\" &&\n Number.isFinite(Date.parse(lock.startedAt))\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["fs", "fs", "throwIfAborted", "fs"]
7
+ }
@@ -0,0 +1,362 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ fileHashMap,
5
+ sameHashes
6
+ } from "./chunk-BDB4IDM3.ts";
7
+ import {
8
+ mergePathIdentity,
9
+ planFileMerge
10
+ } from "./chunk-76NB4R7U.ts";
11
+ import {
12
+ statePathForConfig,
13
+ syncStateFingerprint
14
+ } from "./chunk-4QVPTIZB.ts";
15
+ import {
16
+ portableSnapshot,
17
+ sameLocalFields,
18
+ validatePortableSnapshot,
19
+ validateSnapshotFieldPolicy
20
+ } from "./chunk-A6HU4ERM.ts";
21
+ import {
22
+ readJsonIfExists,
23
+ syncDirectory,
24
+ writeJson
25
+ } from "./chunk-HUZ5CR4W.ts";
26
+
27
+ // src/sync/conflict-artifacts.ts
28
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
29
+ import fs2 from "node:fs/promises";
30
+ import path2 from "node:path";
31
+
32
+ // src/sync/conflict-groups.ts
33
+ function conflictGroups(plan, config) {
34
+ const paths = plan.decisions.map((item) => item.path);
35
+ const parents = paths.map((_, index) => index);
36
+ const ranks = paths.map(() => 0);
37
+ const find = (index) => {
38
+ while (parents[index] !== index) {
39
+ parents[index] = parents[parents[index] ?? index] ?? index;
40
+ index = parents[index] ?? index;
41
+ }
42
+ return index;
43
+ };
44
+ const join = (left, right) => {
45
+ let a = find(left);
46
+ let b = find(right);
47
+ if (a === b) return;
48
+ if ((ranks[a] ?? 0) < (ranks[b] ?? 0)) [a, b] = [b, a];
49
+ parents[b] = a;
50
+ if (ranks[a] === ranks[b]) ranks[a] = (ranks[a] ?? 0) + 1;
51
+ };
52
+ const identities = paths.map(mergePathIdentity);
53
+ const byIdentity = /* @__PURE__ */ new Map();
54
+ const byPath = new Map(paths.map((value, index) => [value, index]));
55
+ identities.forEach((value, index) => {
56
+ const previous = byIdentity.get(value);
57
+ if (previous !== void 0) join(index, previous);
58
+ else byIdentity.set(value, index);
59
+ });
60
+ const includes = new Set(
61
+ config.include.filter((value) => !["settings.json", "AGENTS.md", "sessions"].includes(value)).map(mergePathIdentity)
62
+ );
63
+ const buckets = /* @__PURE__ */ new Map();
64
+ const bucket = (key, index) => {
65
+ const previous = buckets.get(key);
66
+ if (previous === void 0) buckets.set(key, index);
67
+ else join(index, previous);
68
+ };
69
+ paths.forEach((value, index) => {
70
+ const root = value.split("/")[0] ?? "";
71
+ if (["extensions", "themes", "skills", "prompts"].includes(root)) bucket(`resource:${root}`, index);
72
+ const segments = (identities[index] ?? "").split("/");
73
+ let ancestor = "";
74
+ for (let length = 0; length < segments.length - 1; length++) {
75
+ ancestor = ancestor ? `${ancestor}/${segments[length]}` : segments[length] ?? "";
76
+ const parent = byIdentity.get(ancestor);
77
+ if (parent !== void 0) join(index, parent);
78
+ if (includes.has(ancestor)) bucket(`include:${ancestor}`, index);
79
+ }
80
+ });
81
+ const groups = /* @__PURE__ */ new Map();
82
+ for (const conflict of plan.conflicts) {
83
+ const index = byPath.get(conflict.path);
84
+ if (index === void 0) throw new Error("Conflict is missing from managed decisions.");
85
+ const key = find(index);
86
+ const group = groups.get(key) ?? { paths: [], reasons: [] };
87
+ if (!group.reasons.includes(conflict.reason)) group.reasons.push(conflict.reason);
88
+ groups.set(key, group);
89
+ }
90
+ paths.forEach((value, index) => {
91
+ groups.get(find(index))?.paths.push(value);
92
+ });
93
+ return [...groups.values()].map((group) => ({ ...group, paths: group.paths.sort() }));
94
+ }
95
+
96
+ // src/sync/merge-journal.ts
97
+ import { randomUUID } from "node:crypto";
98
+ import fs from "node:fs/promises";
99
+ import path from "node:path";
100
+ function mergeJournalIdentity(config, backendIdentity) {
101
+ return JSON.stringify([config.setupName, backendIdentity, [...config.include].sort(), config.localFields ?? null]);
102
+ }
103
+ function mergeJournalPath(config) {
104
+ return `${statePathForConfig(config)}.merge-journal.json`;
105
+ }
106
+ async function readMergeJournal(config) {
107
+ let journal;
108
+ try {
109
+ const stat = await fs.lstat(mergeJournalPath(config));
110
+ if (!stat.isFile() || stat.size > 384 * 1024 * 1024) throw new Error("Unsafe or oversized journal.");
111
+ journal = await readJsonIfExists(mergeJournalPath(config));
112
+ } catch (error) {
113
+ if (error.code === "ENOENT") return;
114
+ throw new Error("Cannot read the private merge journal; preserve it and review recovery.");
115
+ }
116
+ if (!journal) return;
117
+ if (journal.version !== 1 || typeof journal.identity !== "string" || typeof journal.backup !== "string" || typeof journal.stateIdentity !== "string" || journal.applyOnly !== void 0 && (journal.applyOnly !== true || !journal.committedHead || journal.committedHead.revision !== journal.expectedHead?.revision || journal.committedHead.snapshotId !== journal.expectedHead?.snapshotId || journal.upload?.id !== journal.expectedHead?.snapshotId) || journal.sessionRoot !== void 0 && (typeof journal.sessionRoot !== "string" || !path.isAbsolute(journal.sessionRoot) || path.resolve(journal.sessionRoot) !== journal.sessionRoot) || journal.warnings !== void 0 && (!Array.isArray(journal.warnings) || journal.warnings.length > 64 || journal.warnings.some((item) => typeof item !== "string")) || !journal.expectedHead || typeof journal.expectedHead.revision !== "string" || !journal.before || !journal.after || !journal.upload || !Array.isArray(journal.before.files) || !Array.isArray(journal.after.files) || !Array.isArray(journal.upload.files))
118
+ throw new Error("Unsupported or damaged merge journal; preserve it and review recovery before syncing.");
119
+ try {
120
+ const pinnedPaths = new Set(journal.progress?.groups.flatMap((group) => group.paths) ?? []);
121
+ for (const snapshot of [
122
+ journal.before,
123
+ journal.after,
124
+ journal.upload,
125
+ ...journal.accepted ? [journal.accepted] : []
126
+ ]) {
127
+ validateSnapshotFieldPolicy(snapshot);
128
+ if (snapshot.version !== 1 && snapshot.version !== 2 && snapshot.version !== 3 || typeof snapshot.id !== "string" || typeof snapshot.profile !== "string" || snapshot.files.length > 16384 || snapshot.files.reduce(
129
+ (bytes, file) => bytes + (typeof file?.contentBase64 === "string" ? file.contentBase64.length * 0.75 : Infinity),
130
+ 0
131
+ ) > 64 * 1024 * 1024)
132
+ throw new Error("Invalid journal input.");
133
+ const plan = planFileMerge({ baseline: {}, local: snapshot.files, remote: [], selectionCompatible: true });
134
+ if (plan.kind !== "planned" || plan.conflicts.some((conflict) => conflict.reason !== "path-collision" || !pinnedPaths.has(conflict.path)))
135
+ throw new Error("Invalid journal collision group.");
136
+ }
137
+ validatePortableSnapshot(journal.upload);
138
+ if ((journal.upload.version === 2 || journal.upload.version === 3) && !journal.accepted)
139
+ throw new Error("Portable merge journal requires an explicit accepted projection.");
140
+ if (journal.accepted) validatePortableSnapshot(journal.accepted);
141
+ } catch {
142
+ throw new Error("Invalid merge journal paths, metadata, or bytes; preserve evidence for review.");
143
+ }
144
+ try {
145
+ if (journal.accepted && (!sameLocalFields(journal.accepted.localFields, journal.upload.localFields) || !sameHashes(
146
+ fileHashMap(portableSnapshot(journal.after, journal.upload.localFields)),
147
+ fileHashMap(portableSnapshot(journal.accepted, journal.upload.localFields))
148
+ )))
149
+ throw new Error("Projection mismatch.");
150
+ } catch {
151
+ throw new Error("Invalid accepted merge projection; preserve journal evidence.");
152
+ }
153
+ if (journal.progress) {
154
+ const known = /* @__PURE__ */ new Set([
155
+ ...Object.keys(journal.progress.previous?.lastFileHashes ?? {}),
156
+ ...[...journal.before.files, ...journal.after.files, ...journal.upload.files].map((file) => file.path)
157
+ ]);
158
+ const progress = journal.progress;
159
+ if (!config.partialSync || !progress.previous || syncStateFingerprint(progress.previous) !== journal.stateIdentity || !Array.isArray(progress.groups) || progress.groups.length > 16384 || progress.groups.some(
160
+ (group) => !Array.isArray(group.paths) || !group.paths.length || group.paths.length > 16384 || group.paths.some((filePath) => !known.has(filePath)) || !/^[a-f0-9-]{36}$/u.test(group.artifact)
161
+ ))
162
+ throw new Error("Invalid partial acceptance metadata; preserve journal evidence.");
163
+ }
164
+ if (journal.progress) {
165
+ const before = fileHashMap(journal.before);
166
+ const after = fileHashMap(journal.after);
167
+ const { readConflictArtifact: readConflictArtifact2 } = await import("./conflict-artifacts-BB2FZMIE.ts");
168
+ const backendIdentity = JSON.parse(journal.identity)[1];
169
+ if (typeof backendIdentity !== "string") throw new Error("Invalid partial backend identity.");
170
+ const byArtifact = /* @__PURE__ */ new Map();
171
+ for (const group of journal.progress.groups) {
172
+ const groups = byArtifact.get(group.artifact) ?? [];
173
+ groups.push(group);
174
+ byArtifact.set(group.artifact, groups);
175
+ }
176
+ for (const [token, groups] of byArtifact) {
177
+ const artifact = await readConflictArtifact2(config, backendIdentity, token);
178
+ const retainedGroups = new Set(artifact.groups.map((group) => JSON.stringify(group.paths)));
179
+ const remote = fileHashMap(artifact.remote);
180
+ const upload = fileHashMap(journal.upload);
181
+ for (const group of groups) {
182
+ if (!retainedGroups.has(JSON.stringify(group.paths)))
183
+ throw new Error("Partial group does not match retained artifact evidence.");
184
+ for (const filePath of group.paths)
185
+ if (before[filePath] !== after[filePath] || remote[filePath] !== upload[filePath] || journal.progress.previous.lastFileHashes[filePath] !== artifact.state.lastFileHashes[filePath])
186
+ throw new Error("Withheld version changed in journal; preserve evidence.");
187
+ }
188
+ }
189
+ }
190
+ return journal;
191
+ }
192
+ async function writeMergeJournal(config, journal) {
193
+ await writeJson(mergeJournalPath(config), journal, { maxBytes: 384 * 1024 * 1024 });
194
+ }
195
+ async function clearMergeJournal(config) {
196
+ await fs.rm(mergeJournalPath(config), { force: true });
197
+ await syncDirectory(path.dirname(mergeJournalPath(config)));
198
+ }
199
+ async function retireMergeJournal(config) {
200
+ if (!await readMergeJournal(config)) return;
201
+ const file = mergeJournalPath(config);
202
+ await fs.rename(file, `${file}.${randomUUID()}.resolved`);
203
+ await syncDirectory(path.dirname(file));
204
+ }
205
+ async function requireNoMergeJournal(config, options = {}) {
206
+ const journal = await readMergeJournal(config);
207
+ if (options.force || !journal) return;
208
+ if (journal) {
209
+ throw new Error(
210
+ "A merged transfer needs recovery. Run /sync sync to reconcile it. If newer local edits or remote revisions prevent recovery, review /sync diff and explicitly choose push --force or pull --force; the selected directional result will archive the old journal, not restore old bytes. Keep its backup; do not downgrade while recovery is pending."
211
+ );
212
+ }
213
+ }
214
+
215
+ // src/sync/conflict-artifacts.ts
216
+ var LIMIT = 192 * 1024 * 1024;
217
+ function conflictArtifactFingerprint(artifact) {
218
+ return createHash("sha256").update(JSON.stringify(artifact)).digest("hex");
219
+ }
220
+ function conflictDirectory(config) {
221
+ return `${statePathForConfig(config)}.conflicts`;
222
+ }
223
+ async function directory(config, create = false) {
224
+ const target = conflictDirectory(config);
225
+ if (create) await fs2.mkdir(target, { recursive: true, mode: 448 });
226
+ const stat = await fs2.lstat(target);
227
+ if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== "win32" && (stat.mode & 63) !== 0)
228
+ throw new Error("Unsafe private conflict directory.");
229
+ return target;
230
+ }
231
+ function tokenPath(config, token) {
232
+ if (!/^[a-f0-9-]{36}$/u.test(token)) throw new Error("Invalid conflict token.");
233
+ return path2.join(conflictDirectory(config), `${token}.json`);
234
+ }
235
+ async function saveConflictArtifact(config, backend, artifact, validate, onCreated) {
236
+ validate();
237
+ const identity = mergeJournalIdentity(config, backend);
238
+ const local = Object.fromEntries(artifact.local.files.map((file) => [file.path, file.sha256]));
239
+ const remote = Object.fromEntries(artifact.remote.files.map((file) => [file.path, file.sha256]));
240
+ const fingerprint = createHash("sha256").update(
241
+ JSON.stringify([
242
+ identity,
243
+ artifact.groups.map(
244
+ (group) => group.paths.map((filePath) => [
245
+ filePath,
246
+ artifact.state.lastFileHashes[filePath] ?? null,
247
+ local[filePath] ?? null,
248
+ remote[filePath] ?? null
249
+ ])
250
+ )
251
+ ])
252
+ ).digest("hex").slice(0, 32);
253
+ const token = `${fingerprint.slice(0, 8)}-${fingerprint.slice(8, 12)}-${fingerprint.slice(12, 16)}-${fingerprint.slice(16, 20)}-${fingerprint.slice(20)}`;
254
+ await directory(config, true);
255
+ validate();
256
+ const reuse = async () => {
257
+ const existing = await readConflictArtifact(config, backend, token);
258
+ validate();
259
+ const oldLocal = Object.fromEntries(existing.local.files.map((file) => [file.path, file.sha256]));
260
+ const oldRemote = Object.fromEntries(existing.remote.files.map((file) => [file.path, file.sha256]));
261
+ if (JSON.stringify(existing.groups.map((group) => group.paths)) !== JSON.stringify(artifact.groups.map((group) => group.paths)) || artifact.groups.some(
262
+ (group) => group.paths.some(
263
+ (filePath) => existing.state.lastFileHashes[filePath] !== artifact.state.lastFileHashes[filePath] || oldLocal[filePath] !== local[filePath] || oldRemote[filePath] !== remote[filePath]
264
+ )
265
+ ))
266
+ throw new Error("Conflict identity does not match retained immutable evidence.");
267
+ return token;
268
+ };
269
+ const target = tokenPath(config, token);
270
+ try {
271
+ await fs2.lstat(target);
272
+ validate();
273
+ return await reuse();
274
+ } catch (error) {
275
+ if (error.code !== "ENOENT") throw error;
276
+ }
277
+ const temporary = `${target}.${randomUUID2()}.pending`;
278
+ const candidate = { version: 1, identity, ...artifact };
279
+ try {
280
+ await writeJson(temporary, candidate, { maxBytes: LIMIT });
281
+ validate();
282
+ const stat = await fs2.lstat(temporary);
283
+ validate();
284
+ try {
285
+ await fs2.link(temporary, target);
286
+ } catch (error) {
287
+ if (error.code !== "EEXIST") throw error;
288
+ validate();
289
+ return await reuse();
290
+ }
291
+ onCreated?.({
292
+ token,
293
+ target,
294
+ dev: stat.dev,
295
+ ino: stat.ino,
296
+ size: stat.size,
297
+ fingerprint: conflictArtifactFingerprint(candidate)
298
+ });
299
+ validate();
300
+ await syncDirectory(path2.dirname(target));
301
+ validate();
302
+ await syncDirectory(path2.dirname(conflictDirectory(config)));
303
+ validate();
304
+ return token;
305
+ } finally {
306
+ await fs2.rm(temporary, { force: true });
307
+ }
308
+ }
309
+ async function readConflictArtifact(config, backend, token) {
310
+ try {
311
+ await directory(config);
312
+ const target = tokenPath(config, token);
313
+ const stat = await fs2.lstat(target);
314
+ if (!stat.isFile() || stat.size > LIMIT || process.platform !== "win32" && (stat.mode & 63) !== 0)
315
+ throw new Error("Unsafe conflict artifact.");
316
+ const artifact = JSON.parse(await fs2.readFile(target, "utf8"));
317
+ if (artifact.version !== 1 || artifact.identity !== mergeJournalIdentity(config, backend) || !Array.isArray(artifact.groups) || artifact.groups.length > 16384 || artifact.groups.some(
318
+ (group) => !Array.isArray(group.paths) || !group.paths.length || group.paths.length > 16384 || !Array.isArray(group.reasons)
319
+ ))
320
+ throw new Error("Invalid conflict artifact.");
321
+ const plan = planFileMerge({
322
+ baseline: artifact.state.lastFileHashes,
323
+ local: artifact.local.files,
324
+ remote: artifact.remote.files,
325
+ selectionCompatible: true
326
+ });
327
+ if (plan.kind !== "planned") throw new Error("Invalid conflict versions.");
328
+ const known = new Set(plan.decisions.map((item) => item.path));
329
+ if (artifact.groups.some((group) => group.paths.some((value) => !known.has(value))))
330
+ throw new Error("Invalid conflict paths.");
331
+ if (artifact.ancestors) {
332
+ if (!Array.isArray(artifact.ancestors) || artifact.ancestors.length > 16384 || artifact.ancestors.some((file) => artifact.state.lastFileHashes[file.path] !== file.sha256))
333
+ throw new Error("Invalid artifact ancestor.");
334
+ const basePlan = planFileMerge({
335
+ baseline: {},
336
+ local: artifact.ancestors,
337
+ remote: [],
338
+ selectionCompatible: true
339
+ });
340
+ if (basePlan.kind !== "planned" || basePlan.conflicts.length) throw new Error("Invalid ancestor bytes.");
341
+ }
342
+ syncStateFingerprint(artifact.state);
343
+ return artifact;
344
+ } catch {
345
+ throw new Error("Cannot verify private conflict artifact; preserve evidence and review a fresh sync.");
346
+ }
347
+ }
348
+
349
+ export {
350
+ conflictGroups,
351
+ mergeJournalIdentity,
352
+ readMergeJournal,
353
+ writeMergeJournal,
354
+ clearMergeJournal,
355
+ retireMergeJournal,
356
+ requireNoMergeJournal,
357
+ conflictArtifactFingerprint,
358
+ conflictDirectory,
359
+ saveConflictArtifact,
360
+ readConflictArtifact
361
+ };
362
+ //# sourceMappingURL=chunk-ORQRHEEO.ts.map