@narumitw/pi-sync 0.57.10 → 0.57.11
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.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -1
- package/dist/chunks/{agent-candidate-EYB35AGL.ts → agent-candidate-OCDPRI5G.ts} +11 -11
- package/dist/chunks/{agent-plan-KO43GTOR.ts → agent-plan-P5B5RGZ7.ts} +14 -14
- package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts → agent-refusal-retry-476YGNU5.ts} +10 -10
- package/dist/chunks/{agent-store-UT52DS7B.ts → agent-store-RJSMNOYP.ts} +7 -7
- package/dist/chunks/{agent-sync-VM3TXWA3.ts → agent-sync-EUK2CPSS.ts} +6 -6
- package/dist/chunks/{chunk-UCD7TQDT.ts → chunk-3IYAPMST.ts} +4 -4
- package/dist/chunks/{chunk-JXWTEJS5.ts → chunk-45COPAK7.ts} +10 -3
- package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
- package/dist/chunks/{chunk-DOJRPBQU.ts → chunk-5TSCBBPP.ts} +4 -4
- package/dist/chunks/{chunk-IMMMZUFC.ts → chunk-67INJPKR.ts} +4 -4
- package/dist/chunks/{chunk-CDQGSZL3.ts → chunk-6LD5KX55.ts} +6 -6
- package/dist/chunks/{chunk-MDUORMCK.ts → chunk-6O3EDBRL.ts} +2 -2
- package/dist/chunks/{chunk-AAPAHANE.ts → chunk-7A35ZAVO.ts} +320 -219
- package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
- package/dist/chunks/{chunk-2VSN3MFX.ts → chunk-7OLLPPLU.ts} +29 -23
- package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
- package/dist/chunks/{chunk-STVQQ5KG.ts → chunk-7ZIYU5QS.ts} +6 -6
- package/dist/chunks/{chunk-AO6EDUA6.ts → chunk-AKMV47YC.ts} +3 -3
- package/dist/chunks/{chunk-66GAUVDL.ts → chunk-BEST45H2.ts} +6 -6
- package/dist/chunks/{chunk-DO55XUNU.ts → chunk-CBLTWXAU.ts} +15 -5
- package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
- package/dist/chunks/{chunk-PWA3YN5Z.ts → chunk-CPT2BN6N.ts} +2 -2
- package/dist/chunks/{chunk-73MDIVYJ.ts → chunk-DN4IA7MR.ts} +6 -6
- package/dist/chunks/{chunk-4RLATUP4.ts → chunk-DRTC4PKM.ts} +6 -6
- package/dist/chunks/{chunk-PHANBTVM.ts → chunk-ESKOPQVB.ts} +4 -4
- package/dist/chunks/{chunk-ODF2M4RF.ts → chunk-F7E3VFXU.ts} +5 -5
- package/dist/chunks/{chunk-YZ5BQNRZ.ts → chunk-FHE2DCA2.ts} +2 -2
- package/dist/chunks/{chunk-YZ5BQNRZ.ts.map → chunk-FHE2DCA2.ts.map} +2 -2
- package/dist/chunks/{chunk-MRIPZGPR.ts → chunk-FLHCOJCD.ts} +3 -3
- package/dist/chunks/{chunk-5WJQ6XOW.ts → chunk-GNYGNHFF.ts} +2 -2
- package/dist/chunks/{chunk-DFJGTVPX.ts → chunk-KYTT7KV4.ts} +2 -2
- package/dist/chunks/{chunk-UGSSZWQ6.ts → chunk-MKSMJPVA.ts} +2 -2
- package/dist/chunks/{chunk-RAMHGOX2.ts → chunk-OJARL2TG.ts} +4 -4
- package/dist/chunks/{chunk-BJXSCGQK.ts → chunk-OKPPZA4D.ts} +148 -89
- package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
- package/dist/chunks/{chunk-RX3WGWVL.ts → chunk-OVBAW7KV.ts} +177 -130
- package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
- package/dist/chunks/{chunk-U77DWNRH.ts → chunk-Q23JJW7E.ts} +3 -3
- package/dist/chunks/{chunk-QWV3XKNJ.ts → chunk-TIMWN2RH.ts} +5 -5
- package/dist/chunks/{chunk-Z5O2TLNF.ts → chunk-TZC7GDJE.ts} +7 -7
- package/dist/chunks/{chunk-TO2GMX3K.ts → chunk-XJHQPB3E.ts} +2 -2
- package/dist/chunks/{chunk-JY2RVOUL.ts → chunk-Y4UVXOLQ.ts} +5 -5
- package/dist/chunks/{chunk-M5IV3KL2.ts → chunk-ZDDBZH72.ts} +3 -3
- package/dist/chunks/{command-handler-FO6F2APH.ts → command-handler-F2E7A5T5.ts} +22 -17
- package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
- package/dist/chunks/{config-B37W7FXW.ts → config-HYG7G5EM.ts} +6 -4
- package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts → conflict-artifacts-J6WSDCWP.ts} +12 -12
- package/dist/chunks/{conflict-review-BQSHSJB3.ts → conflict-review-2QMO2K6Q.ts} +23 -23
- package/dist/chunks/{continuous-record-4DQ2EAZS.ts → continuous-record-AH7JGYKM.ts} +6 -6
- package/dist/chunks/{continuous-sync-ZT3UEQLK.ts → continuous-sync-VRMXCA5X.ts} +18 -18
- package/dist/chunks/{file-selection-FBLKM25V.ts → file-selection-IELTXSGZ.ts} +6 -7
- package/dist/chunks/{file-selection-FBLKM25V.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
- package/dist/chunks/{git-backend-7PCWECRN.ts → git-backend-KHD36W27.ts} +6 -7
- package/dist/chunks/{git-backend-7PCWECRN.ts.map → git-backend-KHD36W27.ts.map} +1 -1
- package/dist/chunks/{local-fields-B6JWLPAE.ts → local-fields-JEKAPZKD.ts} +6 -7
- package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
- package/dist/chunks/{manager-ui-DGRTG76I.ts → manager-ui-LM7DSJ2Y.ts} +103 -52
- package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts → merge-baseline-store-65GKUMXI.ts} +6 -6
- package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts → remote-selection-ui-TD4C4W3J.ts} +16 -12
- package/dist/chunks/{s3-backend-CJXBEWWU.ts → s3-backend-L24UZ6IK.ts} +12 -10
- package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
- package/dist/chunks/{setup-switch-66GWMBNH.ts → setup-switch-PIPXYPCW.ts} +5 -5
- package/dist/chunks/{setup-wizard-OHZXJVQZ.ts → setup-wizard-RR5U3SUB.ts} +6 -6
- package/dist/chunks/{snapshot-VRHNAUUH.ts → snapshot-RGNDYT6A.ts} +5 -6
- package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts → snapshot-transaction-26BQO36S.ts} +8 -8
- package/dist/chunks/{sync-inspection-4WFQEJOE.ts → sync-inspection-MOMLGUTD.ts} +11 -11
- package/dist/chunks/{sync-operations-X4AOXMBT.ts → sync-operations-CV3TULLD.ts} +25 -25
- package/dist/chunks/{sync-state-SWLSMK4G.ts → sync-state-7L4NT7W2.ts} +7 -8
- package/dist/chunks/{webdav-backend-2Q5ZEBST.ts → webdav-backend-3ZCACILP.ts} +7 -8
- package/dist/chunks/{webdav-backend-2Q5ZEBST.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
- package/dist/index.ts +19 -19
- package/package.json +1 -1
- package/skills/configure-pi-sync/references/configuration.md +8 -1
- package/src/backends/s3/s3-client.ts +8 -1
- package/src/commands/command-execution.ts +8 -3
- package/src/paths.ts +6 -1
- package/src/settings/config-file.ts +205 -118
- package/src/settings/config.ts +1 -1
- package/src/settings/settings-management.ts +15 -1
- package/src/settings/settings-store.ts +0 -14
- package/src/snapshot/session-paths.ts +15 -2
- package/src/snapshot/snapshot-mutation-queues.ts +142 -0
- package/src/snapshot/snapshot-transaction.ts +122 -125
- package/src/sync/merge-apply.ts +6 -0
- package/src/ui/cancellable-operation.ts +36 -13
- package/src/ui/manager-state.ts +3 -1
- package/src/ui/manager-ui.ts +76 -24
- package/src/ui/remote-selection-ui.ts +131 -82
- package/src/ui/settings-ui.ts +2 -1
- package/dist/chunks/chunk-2VSN3MFX.ts.map +0 -7
- package/dist/chunks/chunk-AAPAHANE.ts.map +0 -7
- package/dist/chunks/chunk-BJXSCGQK.ts.map +0 -7
- package/dist/chunks/chunk-DO55XUNU.ts.map +0 -7
- package/dist/chunks/chunk-JXWTEJS5.ts.map +0 -7
- package/dist/chunks/chunk-RX3WGWVL.ts.map +0 -7
- package/dist/chunks/command-handler-FO6F2APH.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-NODX3KKH.ts +0 -28
- package/dist/chunks/manager-ui-DGRTG76I.ts.map +0 -7
- package/dist/chunks/s3-backend-CJXBEWWU.ts.map +0 -7
- /package/dist/chunks/{agent-candidate-EYB35AGL.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
- /package/dist/chunks/{agent-plan-KO43GTOR.ts.map → agent-plan-P5B5RGZ7.ts.map} +0 -0
- /package/dist/chunks/{agent-refusal-retry-C2HQROTP.ts.map → agent-refusal-retry-476YGNU5.ts.map} +0 -0
- /package/dist/chunks/{agent-store-UT52DS7B.ts.map → agent-store-RJSMNOYP.ts.map} +0 -0
- /package/dist/chunks/{agent-sync-VM3TXWA3.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
- /package/dist/chunks/{chunk-UCD7TQDT.ts.map → chunk-3IYAPMST.ts.map} +0 -0
- /package/dist/chunks/{chunk-DOJRPBQU.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
- /package/dist/chunks/{chunk-IMMMZUFC.ts.map → chunk-67INJPKR.ts.map} +0 -0
- /package/dist/chunks/{chunk-CDQGSZL3.ts.map → chunk-6LD5KX55.ts.map} +0 -0
- /package/dist/chunks/{chunk-MDUORMCK.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
- /package/dist/chunks/{chunk-STVQQ5KG.ts.map → chunk-7ZIYU5QS.ts.map} +0 -0
- /package/dist/chunks/{chunk-AO6EDUA6.ts.map → chunk-AKMV47YC.ts.map} +0 -0
- /package/dist/chunks/{chunk-66GAUVDL.ts.map → chunk-BEST45H2.ts.map} +0 -0
- /package/dist/chunks/{chunk-PWA3YN5Z.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
- /package/dist/chunks/{chunk-73MDIVYJ.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
- /package/dist/chunks/{chunk-4RLATUP4.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
- /package/dist/chunks/{chunk-PHANBTVM.ts.map → chunk-ESKOPQVB.ts.map} +0 -0
- /package/dist/chunks/{chunk-ODF2M4RF.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
- /package/dist/chunks/{chunk-MRIPZGPR.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
- /package/dist/chunks/{chunk-5WJQ6XOW.ts.map → chunk-GNYGNHFF.ts.map} +0 -0
- /package/dist/chunks/{chunk-DFJGTVPX.ts.map → chunk-KYTT7KV4.ts.map} +0 -0
- /package/dist/chunks/{chunk-UGSSZWQ6.ts.map → chunk-MKSMJPVA.ts.map} +0 -0
- /package/dist/chunks/{chunk-RAMHGOX2.ts.map → chunk-OJARL2TG.ts.map} +0 -0
- /package/dist/chunks/{chunk-U77DWNRH.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-QWV3XKNJ.ts.map → chunk-TIMWN2RH.ts.map} +0 -0
- /package/dist/chunks/{chunk-Z5O2TLNF.ts.map → chunk-TZC7GDJE.ts.map} +0 -0
- /package/dist/chunks/{chunk-TO2GMX3K.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
- /package/dist/chunks/{chunk-JY2RVOUL.ts.map → chunk-Y4UVXOLQ.ts.map} +0 -0
- /package/dist/chunks/{chunk-M5IV3KL2.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
- /package/dist/chunks/{config-B37W7FXW.ts.map → config-HYG7G5EM.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-6XGSSU5S.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
- /package/dist/chunks/{conflict-review-BQSHSJB3.ts.map → conflict-review-2QMO2K6Q.ts.map} +0 -0
- /package/dist/chunks/{continuous-record-4DQ2EAZS.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
- /package/dist/chunks/{continuous-sync-ZT3UEQLK.ts.map → continuous-sync-VRMXCA5X.ts.map} +0 -0
- /package/dist/chunks/{local-fields-B6JWLPAE.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-NODX3KKH.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-HL3VLMAM.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-OHMIQ32X.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-66GWMBNH.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-OHZXJVQZ.ts.map → setup-wizard-RR5U3SUB.ts.map} +0 -0
- /package/dist/chunks/{snapshot-VRHNAUUH.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
- /package/dist/chunks/{snapshot-transaction-EUGA4VX6.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-4WFQEJOE.ts.map → sync-inspection-MOMLGUTD.ts.map} +0 -0
- /package/dist/chunks/{sync-operations-X4AOXMBT.ts.map → sync-operations-CV3TULLD.ts.map} +0 -0
- /package/dist/chunks/{sync-state-SWLSMK4G.ts.map → sync-state-7L4NT7W2.ts.map} +0 -0
|
@@ -6,10 +6,14 @@ import {
|
|
|
6
6
|
} from "./chunk-5SFMBY5P.ts";
|
|
7
7
|
import {
|
|
8
8
|
withLock
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6O3EDBRL.ts";
|
|
10
10
|
import {
|
|
11
11
|
mergePathIdentity
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XJHQPB3E.ts";
|
|
13
|
+
import {
|
|
14
|
+
syncDirectory,
|
|
15
|
+
writeJson
|
|
16
|
+
} from "./chunk-AR2DGDUI.ts";
|
|
13
17
|
import {
|
|
14
18
|
stateDir
|
|
15
19
|
} from "./chunk-SWRVXVFD.ts";
|
|
@@ -18,22 +22,17 @@ import {
|
|
|
18
22
|
configuredSessionDir,
|
|
19
23
|
expandSessionDir,
|
|
20
24
|
sessionStorageRoot
|
|
21
|
-
} from "./chunk-
|
|
22
|
-
import {
|
|
23
|
-
syncDirectory,
|
|
24
|
-
writeJson
|
|
25
|
-
} from "./chunk-AR2DGDUI.ts";
|
|
25
|
+
} from "./chunk-CBLTWXAU.ts";
|
|
26
26
|
import {
|
|
27
27
|
assertWithinRoot,
|
|
28
28
|
isDeniedPath,
|
|
29
29
|
isPathInside
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-FHE2DCA2.ts";
|
|
31
31
|
|
|
32
32
|
// src/snapshot/snapshot-transaction.ts
|
|
33
33
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import fs4 from "node:fs/promises";
|
|
35
|
+
import path6 from "node:path";
|
|
37
36
|
|
|
38
37
|
// src/state/mutation-directory-sync.ts
|
|
39
38
|
import path from "node:path";
|
|
@@ -132,13 +131,124 @@ async function coalesceCaseReplacements(root, plan) {
|
|
|
132
131
|
};
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
// src/snapshot/snapshot-
|
|
134
|
+
// src/snapshot/snapshot-mutation-queues.ts
|
|
136
135
|
import fs2 from "node:fs/promises";
|
|
137
|
-
import
|
|
136
|
+
import path3 from "node:path";
|
|
137
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
138
|
+
function assertCurrent(options) {
|
|
139
|
+
options.validateMutation?.();
|
|
140
|
+
options.signal?.throwIfAborted();
|
|
141
|
+
}
|
|
142
|
+
function isMissing(error) {
|
|
143
|
+
const code = error.code;
|
|
144
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
145
|
+
}
|
|
146
|
+
async function targetMissing(target, options) {
|
|
147
|
+
assertCurrent(options);
|
|
148
|
+
try {
|
|
149
|
+
await fs2.lstat(target);
|
|
150
|
+
assertCurrent(options);
|
|
151
|
+
return false;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
assertCurrent(options);
|
|
154
|
+
if (!isMissing(error)) throw error;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function queueKey(target, options) {
|
|
159
|
+
assertCurrent(options);
|
|
160
|
+
try {
|
|
161
|
+
const key = await fs2.realpath(target);
|
|
162
|
+
assertCurrent(options);
|
|
163
|
+
return key;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
assertCurrent(options);
|
|
166
|
+
if (!isMissing(error)) throw error;
|
|
167
|
+
return path3.resolve(target);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function assertCanonicalParent(target, options) {
|
|
171
|
+
const parent = path3.dirname(path3.resolve(target));
|
|
172
|
+
const suffix = [];
|
|
173
|
+
for (let current = parent; ; current = path3.dirname(current)) {
|
|
174
|
+
assertCurrent(options);
|
|
175
|
+
let canonical;
|
|
176
|
+
try {
|
|
177
|
+
canonical = await fs2.realpath(current);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
assertCurrent(options);
|
|
180
|
+
if (!isMissing(error) || path3.dirname(current) === current) throw error;
|
|
181
|
+
suffix.unshift(path3.basename(current));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
assertCurrent(options);
|
|
185
|
+
if (path3.join(canonical, ...suffix) !== parent)
|
|
186
|
+
throw new Error(
|
|
187
|
+
"Snapshot mutation through an aliased parent is unsafe; use the canonical agent/session path. Transaction evidence retained for review."
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function assertCanonicalMutationTargets(targets, options) {
|
|
193
|
+
for (const target of targets) await assertCanonicalParent(target, options);
|
|
194
|
+
}
|
|
195
|
+
async function withTargetQueues(targets, options, action) {
|
|
196
|
+
await assertCanonicalMutationTargets(targets, options);
|
|
197
|
+
const keys = /* @__PURE__ */ new Set();
|
|
198
|
+
const missing = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const target of targets) {
|
|
200
|
+
if (await targetMissing(target, options)) missing.add(target);
|
|
201
|
+
const key = await queueKey(target, options);
|
|
202
|
+
if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
|
|
203
|
+
keys.add(key);
|
|
204
|
+
}
|
|
205
|
+
const ordered = [...keys].sort();
|
|
206
|
+
async function acquire(index) {
|
|
207
|
+
assertCurrent(options);
|
|
208
|
+
const key = ordered[index];
|
|
209
|
+
if (key) return withFileMutationQueue(key, () => acquire(index + 1));
|
|
210
|
+
for (const target of targets) {
|
|
211
|
+
await assertCanonicalParent(target, options);
|
|
212
|
+
if (missing.has(target) && !await targetMissing(target, options))
|
|
213
|
+
throw new Error("Missing transaction target appeared during queue acquisition; review is required.");
|
|
214
|
+
if (!keys.has(await queueKey(target, options)))
|
|
215
|
+
throw new Error("Transaction queue identity changed; review is required.");
|
|
216
|
+
}
|
|
217
|
+
assertCurrent(options);
|
|
218
|
+
return action(keys);
|
|
219
|
+
}
|
|
220
|
+
return acquire(0);
|
|
221
|
+
}
|
|
222
|
+
async function withMissingTargetQueues(targets, heldKeys, options, action) {
|
|
223
|
+
const unique = [...new Set(targets)];
|
|
224
|
+
const requireAbsent = async () => {
|
|
225
|
+
for (const target of unique) {
|
|
226
|
+
await assertCanonicalParent(target, options);
|
|
227
|
+
if (!await targetMissing(target, options))
|
|
228
|
+
throw new Error("Snapshot target changed before destination queue reservation; evidence retained.");
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const keys = unique.map((target) => path3.resolve(target)).filter((key) => !heldKeys.has(key)).sort();
|
|
232
|
+
await requireAbsent();
|
|
233
|
+
const combined = /* @__PURE__ */ new Set([...heldKeys, ...keys]);
|
|
234
|
+
async function acquire(index) {
|
|
235
|
+
assertCurrent(options);
|
|
236
|
+
const key = keys[index];
|
|
237
|
+
if (key) return withFileMutationQueue(key, () => acquire(index + 1));
|
|
238
|
+
await requireAbsent();
|
|
239
|
+
assertCurrent(options);
|
|
240
|
+
return action(combined);
|
|
241
|
+
}
|
|
242
|
+
return acquire(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/snapshot/snapshot-root-transition.ts
|
|
246
|
+
import fs3 from "node:fs/promises";
|
|
247
|
+
import path5 from "node:path";
|
|
138
248
|
|
|
139
249
|
// src/snapshot/snapshot-transaction-plan.ts
|
|
140
250
|
import { createHash } from "node:crypto";
|
|
141
|
-
import
|
|
251
|
+
import path4 from "node:path";
|
|
142
252
|
function fileImage(content) {
|
|
143
253
|
return `file:${createHash("sha256").update(content).digest("hex")}`;
|
|
144
254
|
}
|
|
@@ -151,7 +261,7 @@ function indexTransactionPlan(plan, targets) {
|
|
|
151
261
|
if (!writeImages.has(write.target)) writeImages.set(write.target, image2);
|
|
152
262
|
for (const ancestor of ancestorTargets(write.target, targetIndex, false)) {
|
|
153
263
|
if (ancestor !== write.target && isStrictDescendant(ancestor, write.target)) {
|
|
154
|
-
postFiles.get(ancestor)?.push({ relative:
|
|
264
|
+
postFiles.get(ancestor)?.push({ relative: path4.relative(ancestor, write.target), image: image2 });
|
|
155
265
|
}
|
|
156
266
|
}
|
|
157
267
|
}
|
|
@@ -175,10 +285,10 @@ function indexTransactionPlan(plan, targets) {
|
|
|
175
285
|
return { writeImages, postFiles, deletes, deletedWrites };
|
|
176
286
|
}
|
|
177
287
|
function isStrictDescendant(parent, target) {
|
|
178
|
-
return
|
|
288
|
+
return path4.relative(path4.resolve(parent), path4.resolve(target)) !== "" && isPathInside(parent, target);
|
|
179
289
|
}
|
|
180
290
|
function pathKey(target) {
|
|
181
|
-
const resolved =
|
|
291
|
+
const resolved = path4.resolve(target);
|
|
182
292
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
183
293
|
}
|
|
184
294
|
function indexTargets(targets) {
|
|
@@ -192,10 +302,10 @@ function indexTargets(targets) {
|
|
|
192
302
|
return index;
|
|
193
303
|
}
|
|
194
304
|
function* ancestorTargets(target, index, includeSelf = true) {
|
|
195
|
-
const resolved =
|
|
196
|
-
for (let ancestor = includeSelf ? resolved :
|
|
305
|
+
const resolved = path4.resolve(target);
|
|
306
|
+
for (let ancestor = includeSelf ? resolved : path4.dirname(resolved); ; ancestor = path4.dirname(ancestor)) {
|
|
197
307
|
yield* index.get(pathKey(ancestor)) ?? [];
|
|
198
|
-
if (
|
|
308
|
+
if (path4.dirname(ancestor) === ancestor) return;
|
|
199
309
|
}
|
|
200
310
|
}
|
|
201
311
|
|
|
@@ -215,36 +325,36 @@ function rootFromSettings(root, bytes) {
|
|
|
215
325
|
throw new Error("Invalid settings root-transition evidence.");
|
|
216
326
|
const value = settings.sessionDir;
|
|
217
327
|
if (value !== void 0 && typeof value !== "string") throw new Error("Invalid settings root-transition evidence.");
|
|
218
|
-
return
|
|
328
|
+
return path5.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
|
|
219
329
|
}
|
|
220
330
|
async function beforeRoot(directory, root, entry, guards) {
|
|
221
331
|
validateOwner(guards);
|
|
222
|
-
if (entry.kind === "missing" && entry.beforeImage === "missing") return
|
|
332
|
+
if (entry.kind === "missing" && entry.beforeImage === "missing") return path5.join(root, "sessions");
|
|
223
333
|
if (entry.kind !== "file") throw new Error("Root transition requires regular settings preimage evidence.");
|
|
224
|
-
const backup =
|
|
225
|
-
const stat = await
|
|
334
|
+
const backup = path5.join(directory, "before", entry.backupName);
|
|
335
|
+
const stat = await fs3.lstat(backup);
|
|
226
336
|
validateOwner(guards);
|
|
227
337
|
if (!stat.isFile()) throw new Error("Invalid settings root-transition backup.");
|
|
228
|
-
const bytes = await
|
|
338
|
+
const bytes = await fs3.readFile(backup);
|
|
229
339
|
validateOwner(guards);
|
|
230
340
|
if (fileImage(bytes) !== entry.beforeImage) throw new Error("Changed settings root-transition backup.");
|
|
231
341
|
return rootFromSettings(root, bytes);
|
|
232
342
|
}
|
|
233
343
|
async function prepareSessionRootTransition(directory, root, sessionRoot, entries, plan, guards = {}) {
|
|
234
|
-
const settingsTarget =
|
|
344
|
+
const settingsTarget = path5.join(root, "settings.json");
|
|
235
345
|
const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
|
|
236
346
|
const write = plan.writes.find((item) => item.target === settingsTarget || item.target === entry?.target);
|
|
237
347
|
if (!entry || !entries.some((item) => item !== entry && isPathInside(sessionRoot, item.target))) return void 0;
|
|
238
348
|
const deleted = !write && plan.deletes.includes(entry.target) && entry.afterImage === "missing";
|
|
239
349
|
if (!write && !deleted) return void 0;
|
|
240
|
-
const afterRoot = write ? rootFromSettings(root, write.content) :
|
|
350
|
+
const afterRoot = write ? rootFromSettings(root, write.content) : path5.join(root, "sessions");
|
|
241
351
|
if (afterRoot !== sessionRoot) return void 0;
|
|
242
352
|
const previous = await beforeRoot(directory, root, entry, guards);
|
|
243
353
|
if (previous === sessionRoot) return void 0;
|
|
244
354
|
if (write && fileImage(write.content) !== entry.afterImage) throw new Error("Changed settings transition postimage.");
|
|
245
355
|
if (!write) return { beforeRoot: previous, settingsAfterBase64: null };
|
|
246
356
|
validateOwner(guards);
|
|
247
|
-
const handle = await
|
|
357
|
+
const handle = await fs3.open(path5.join(directory, "settings-after"), "wx", 384);
|
|
248
358
|
try {
|
|
249
359
|
validateOwner(guards);
|
|
250
360
|
await handle.writeFile(write.content);
|
|
@@ -259,25 +369,25 @@ async function prepareSessionRootTransition(directory, root, sessionRoot, entrie
|
|
|
259
369
|
return { beforeRoot: previous, settingsAfterFile: true };
|
|
260
370
|
}
|
|
261
371
|
function validateSessionRootTransition(value) {
|
|
262
|
-
if (!value || typeof value.beforeRoot !== "string" || !
|
|
372
|
+
if (!value || typeof value.beforeRoot !== "string" || !path5.isAbsolute(value.beforeRoot) || path5.resolve(value.beforeRoot) !== value.beforeRoot || value.settingsAfterFile !== void 0 && value.settingsAfterFile !== true || (value.settingsAfterFile === true ? value.settingsAfterBase64 !== void 0 : value.settingsAfterBase64 !== null && (typeof value.settingsAfterBase64 !== "string" || value.settingsAfterBase64.length > 32 * 1024 * 1024 || Buffer.from(value.settingsAfterBase64, "base64").toString("base64") !== value.settingsAfterBase64)))
|
|
263
373
|
throw new Error("Invalid session root-transition evidence.");
|
|
264
374
|
}
|
|
265
375
|
async function resolveTransitionSessionRoot(directory, root, sessionRoot, transition, entries, currentRoot, guards = {}) {
|
|
266
|
-
const settingsTarget =
|
|
376
|
+
const settingsTarget = path5.join(root, "settings.json");
|
|
267
377
|
const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
|
|
268
378
|
if (!entry) throw new Error("Missing settings root-transition evidence.");
|
|
269
379
|
let after;
|
|
270
380
|
if (transition.settingsAfterFile) {
|
|
271
|
-
const file =
|
|
272
|
-
const stat = await
|
|
381
|
+
const file = path5.join(directory, "settings-after");
|
|
382
|
+
const stat = await fs3.lstat(file);
|
|
273
383
|
validateOwner(guards);
|
|
274
384
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
275
385
|
throw new Error("Unsafe settings root-transition postimage.");
|
|
276
|
-
after = await
|
|
386
|
+
after = await fs3.readFile(file);
|
|
277
387
|
validateOwner(guards);
|
|
278
388
|
} else if (transition.settingsAfterBase64 !== null)
|
|
279
389
|
after = Buffer.from(transition.settingsAfterBase64, "base64");
|
|
280
|
-
if ((after ? fileImage(after) : "missing") !== entry.afterImage || (after ? rootFromSettings(root, after) :
|
|
390
|
+
if ((after ? fileImage(after) : "missing") !== entry.afterImage || (after ? rootFromSettings(root, after) : path5.join(root, "sessions")) !== sessionRoot || await beforeRoot(directory, root, entry, guards) !== transition.beforeRoot)
|
|
281
391
|
throw new Error("Inconsistent settings root-transition evidence.");
|
|
282
392
|
if (currentRoot !== transition.beforeRoot && currentRoot !== sessionRoot)
|
|
283
393
|
throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
|
|
@@ -289,13 +399,13 @@ var JOURNAL_VERSION = 4;
|
|
|
289
399
|
async function applySnapshotTransaction(plan, options = {}) {
|
|
290
400
|
options.validateMutation?.();
|
|
291
401
|
await recoverPendingSnapshotTransactions(options);
|
|
292
|
-
const coalesced = await coalesceCaseReplacements(
|
|
402
|
+
const coalesced = await coalesceCaseReplacements(path6.resolve(agentDir()), plan);
|
|
293
403
|
options.validateMutation?.();
|
|
294
404
|
options.signal?.throwIfAborted();
|
|
295
405
|
const targets = [.../* @__PURE__ */ new Set([...coalesced.plan.deletes, ...coalesced.plan.writes.map((item) => item.target)])].sort();
|
|
296
406
|
if (targets.length > 16384)
|
|
297
407
|
throw new Error("Snapshot transaction exceeds its target bound; review a smaller transfer.");
|
|
298
|
-
return withTargetQueues(targets, async () => {
|
|
408
|
+
return withTargetQueues(targets, options, async (heldKeys) => {
|
|
299
409
|
options.signal?.throwIfAborted();
|
|
300
410
|
if (targets.some(
|
|
301
411
|
(target) => options.protectedTargets?.some(
|
|
@@ -319,7 +429,7 @@ async function applySnapshotTransaction(plan, options = {}) {
|
|
|
319
429
|
for (const [target, expected] of options.expectedPreimages ?? []) {
|
|
320
430
|
if (originalTargets.has(target)) continue;
|
|
321
431
|
const variants = coalesced.plan.deletes.filter(
|
|
322
|
-
(candidate) => candidate !== target &&
|
|
432
|
+
(candidate) => candidate !== target && path6.dirname(candidate) === path6.dirname(target) && mergePathIdentity(candidate) === mergePathIdentity(target)
|
|
323
433
|
);
|
|
324
434
|
if (variants.length > 1) throw new Error("Ambiguous reviewed file preimage spellings; review required.");
|
|
325
435
|
const original = variants[0];
|
|
@@ -351,79 +461,87 @@ async function applySnapshotTransaction(plan, options = {}) {
|
|
|
351
461
|
...entry.afterTarget ? [[entry.afterTarget, entry]] : []
|
|
352
462
|
])
|
|
353
463
|
);
|
|
464
|
+
const changesQueueKey = transaction.journal.entries.some(
|
|
465
|
+
(entry) => !heldKeys.has(entry.target) || entry.afterTarget && !heldKeys.has(entry.afterTarget)
|
|
466
|
+
);
|
|
354
467
|
try {
|
|
355
468
|
for (const target of transaction.deletes.sort((a, b) => a.length - b.length)) {
|
|
356
469
|
options.signal?.throwIfAborted();
|
|
357
470
|
const entry = entriesByTarget.get(target);
|
|
358
471
|
if (!entry) throw new Error("Unowned transaction target.");
|
|
359
472
|
await removeOwnedTarget(transaction.directory, entry, transaction.journal, options);
|
|
360
|
-
await syncDirectory(
|
|
473
|
+
await syncDirectory(path6.dirname(target));
|
|
361
474
|
}
|
|
362
|
-
await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
options.validateMutation?.();
|
|
382
|
-
options.signal?.throwIfAborted();
|
|
383
|
-
const temp = path5.join(path5.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
|
|
384
|
-
try {
|
|
385
|
-
let mode = 384;
|
|
386
|
-
try {
|
|
387
|
-
const stat = await fs3.lstat(item.target);
|
|
388
|
-
if (stat.isFile()) mode = stat.mode & 511;
|
|
389
|
-
} catch (error) {
|
|
390
|
-
if (error.code !== "ENOENT") throw error;
|
|
391
|
-
}
|
|
392
|
-
const handle = await fs3.open(temp, "wx", mode);
|
|
393
|
-
try {
|
|
394
|
-
await handle.writeFile(item.content);
|
|
395
|
-
await handle.sync();
|
|
396
|
-
} finally {
|
|
397
|
-
await handle.close();
|
|
398
|
-
}
|
|
475
|
+
await withMissingTargetQueues(
|
|
476
|
+
transaction.journal.entries.filter((entry) => entry.removalPending).flatMap((entry) => [entry.target, ...entry.afterTarget ? [entry.afterTarget] : []]),
|
|
477
|
+
heldKeys,
|
|
478
|
+
options,
|
|
479
|
+
async () => {
|
|
480
|
+
await retireRemovalIntent(
|
|
481
|
+
transaction.directory,
|
|
482
|
+
transaction.journal,
|
|
483
|
+
transaction.journal.entries.filter(
|
|
484
|
+
(entry) => entry.removalPending && (entry.afterImage !== "missing" || (entry.postFiles?.length ?? 0) > 0)
|
|
485
|
+
),
|
|
486
|
+
options
|
|
487
|
+
);
|
|
488
|
+
const uninstalledWrites = new Set(transaction.deletedWrites);
|
|
489
|
+
for (const item of plan.writes) {
|
|
490
|
+
options.signal?.throwIfAborted();
|
|
491
|
+
const entry = entriesByTarget.get(item.target);
|
|
492
|
+
if (!entry) throw new Error("Unowned transaction target.");
|
|
493
|
+
const deletedByThisCall = uninstalledWrites.delete(entry.target);
|
|
399
494
|
await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
|
|
495
|
+
options.validateMutation?.();
|
|
400
496
|
options.signal?.throwIfAborted();
|
|
497
|
+
await fs4.mkdir(path6.dirname(item.target), { recursive: true });
|
|
401
498
|
options.validateMutation?.();
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
499
|
+
options.signal?.throwIfAborted();
|
|
500
|
+
const temp = path6.join(path6.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
|
|
501
|
+
try {
|
|
502
|
+
let mode = 384;
|
|
503
|
+
try {
|
|
504
|
+
const stat = await fs4.lstat(item.target);
|
|
505
|
+
if (stat.isFile()) mode = stat.mode & 511;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (error.code !== "ENOENT") throw error;
|
|
508
|
+
}
|
|
509
|
+
const handle = await fs4.open(temp, "wx", mode);
|
|
510
|
+
try {
|
|
511
|
+
await handle.writeFile(item.content);
|
|
512
|
+
await handle.sync();
|
|
513
|
+
} finally {
|
|
514
|
+
await handle.close();
|
|
515
|
+
}
|
|
516
|
+
await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
|
|
517
|
+
options.signal?.throwIfAborted();
|
|
518
|
+
options.validateMutation?.();
|
|
519
|
+
await fs4.rename(temp, item.target);
|
|
520
|
+
await syncDirectory(path6.dirname(item.target));
|
|
521
|
+
} finally {
|
|
522
|
+
await fs4.rm(temp, { force: true });
|
|
523
|
+
}
|
|
406
524
|
}
|
|
525
|
+
options.validateMutation?.();
|
|
526
|
+
options.signal?.throwIfAborted();
|
|
527
|
+
await syncMutationParents(
|
|
528
|
+
targets,
|
|
529
|
+
[transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
|
|
530
|
+
options
|
|
531
|
+
);
|
|
532
|
+
options.validateMutation?.();
|
|
533
|
+
options.signal?.throwIfAborted();
|
|
407
534
|
}
|
|
408
|
-
|
|
409
|
-
options.signal?.throwIfAborted();
|
|
410
|
-
await syncMutationParents(
|
|
411
|
-
targets,
|
|
412
|
-
[transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
|
|
413
|
-
options
|
|
414
|
-
);
|
|
415
|
-
options.validateMutation?.();
|
|
416
|
-
options.signal?.throwIfAborted();
|
|
417
|
-
});
|
|
535
|
+
);
|
|
418
536
|
} catch (error) {
|
|
419
537
|
if (options.signal?.aborted)
|
|
420
538
|
throw new Error("Snapshot apply cancelled; guarded transaction evidence retained for review.");
|
|
421
|
-
if (
|
|
422
|
-
throw new Error("
|
|
539
|
+
if (changesQueueKey)
|
|
540
|
+
throw new Error("Snapshot apply failed; guarded transaction evidence retained for review.", {
|
|
423
541
|
cause: error
|
|
424
542
|
});
|
|
425
543
|
try {
|
|
426
|
-
await restoreTransaction(transaction.directory, transaction.journal, options);
|
|
544
|
+
await restoreTransaction(transaction.directory, transaction.journal, options, heldKeys);
|
|
427
545
|
} catch (recoveryError) {
|
|
428
546
|
throw new AggregateError(
|
|
429
547
|
[error, recoveryError],
|
|
@@ -450,13 +568,13 @@ async function recoverSnapshotTransactionsOnStartup(options = {}) {
|
|
|
450
568
|
async function recoverPendingSnapshotTransactions(options = {}) {
|
|
451
569
|
for (const entry of (await pendingTransactionEntries()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
452
570
|
if (!entry.isDirectory()) continue;
|
|
453
|
-
const directory =
|
|
571
|
+
const directory = path6.join(transactionRoot(), entry.name);
|
|
454
572
|
let journal;
|
|
455
573
|
try {
|
|
456
|
-
const file =
|
|
457
|
-
const stat = await
|
|
574
|
+
const file = path6.join(directory, "journal.json");
|
|
575
|
+
const stat = await fs4.lstat(file);
|
|
458
576
|
if (!stat.isFile() || stat.size > 32 * 1024 * 1024) throw new Error("Invalid journal file.");
|
|
459
|
-
journal = JSON.parse(await
|
|
577
|
+
journal = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
460
578
|
} catch (error) {
|
|
461
579
|
if (error.code === "ENOENT") continue;
|
|
462
580
|
throw new Error("Cannot recover malformed pi-sync transaction; preserve private evidence for review.");
|
|
@@ -475,7 +593,7 @@ async function recoverPendingSnapshotTransactions(options = {}) {
|
|
|
475
593
|
journal.sessionRoot,
|
|
476
594
|
journal.sessionRootTransition,
|
|
477
595
|
journal.entries,
|
|
478
|
-
|
|
596
|
+
path6.resolve(sessionStorageRoot(journal.root, sessionDir)),
|
|
479
597
|
options
|
|
480
598
|
);
|
|
481
599
|
options.validateMutation?.();
|
|
@@ -488,18 +606,19 @@ async function recoverPendingSnapshotTransactions(options = {}) {
|
|
|
488
606
|
}
|
|
489
607
|
await withTargetQueues(
|
|
490
608
|
journal.entries.map((entry2) => entry2.target).sort(),
|
|
491
|
-
|
|
609
|
+
options,
|
|
610
|
+
(heldKeys) => restoreTransaction(directory, journal, { ...options, sessionDir }, heldKeys)
|
|
492
611
|
);
|
|
493
612
|
}
|
|
494
613
|
}
|
|
495
614
|
async function prepareTransaction(plan, targets, options, replacements = /* @__PURE__ */ new Map()) {
|
|
496
615
|
const { sessionDir } = options;
|
|
497
|
-
const root =
|
|
498
|
-
const sessionRoot = sessionDir ?
|
|
616
|
+
const root = path6.resolve(agentDir());
|
|
617
|
+
const sessionRoot = sessionDir ? path6.resolve(sessionStorageRoot(root, sessionDir)) : void 0;
|
|
499
618
|
const indexed = indexTransactionPlan(plan, targets);
|
|
500
|
-
const directory =
|
|
501
|
-
const backupDirectory =
|
|
502
|
-
await
|
|
619
|
+
const directory = path6.join(transactionRoot(), randomUUID());
|
|
620
|
+
const backupDirectory = path6.join(directory, "before");
|
|
621
|
+
await fs4.mkdir(backupDirectory, { recursive: true, mode: 448 });
|
|
503
622
|
const entries = [];
|
|
504
623
|
for (const [index, target] of targets.entries()) {
|
|
505
624
|
options.signal?.throwIfAborted();
|
|
@@ -508,15 +627,15 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
508
627
|
await assertSafeParents(root, sessionRoot, target);
|
|
509
628
|
const entry = { target, backupName: `${index}`, kind: "missing" };
|
|
510
629
|
if (replacements.has(target)) entry.afterTarget = replacements.get(target);
|
|
511
|
-
const backup =
|
|
630
|
+
const backup = path6.join(backupDirectory, entry.backupName);
|
|
512
631
|
try {
|
|
513
|
-
const stat = await
|
|
632
|
+
const stat = await fs4.lstat(target);
|
|
514
633
|
if (stat.isSymbolicLink()) {
|
|
515
634
|
entry.kind = "symlink";
|
|
516
|
-
entry.linkTarget = await
|
|
635
|
+
entry.linkTarget = await fs4.readlink(target);
|
|
517
636
|
} else if (stat.isDirectory()) {
|
|
518
637
|
entry.kind = "directory";
|
|
519
|
-
await
|
|
638
|
+
await fs4.cp(target, backup, {
|
|
520
639
|
recursive: true,
|
|
521
640
|
dereference: false,
|
|
522
641
|
verbatimSymlinks: true,
|
|
@@ -529,8 +648,8 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
529
648
|
});
|
|
530
649
|
} else if (stat.isFile()) {
|
|
531
650
|
entry.kind = "file";
|
|
532
|
-
await
|
|
533
|
-
await
|
|
651
|
+
await fs4.copyFile(target, backup);
|
|
652
|
+
await fs4.chmod(backup, stat.mode);
|
|
534
653
|
} else throw new Error("Unsupported existing snapshot target.");
|
|
535
654
|
} catch (error) {
|
|
536
655
|
const code = error.code;
|
|
@@ -553,7 +672,7 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
553
672
|
entries.push(entry);
|
|
554
673
|
}
|
|
555
674
|
await syncDirectory(backupDirectory);
|
|
556
|
-
const effectiveRoot = sessionRoot ??
|
|
675
|
+
const effectiveRoot = sessionRoot ?? path6.join(root, "sessions");
|
|
557
676
|
const sessionRootTransition = await prepareSessionRootTransition(
|
|
558
677
|
directory,
|
|
559
678
|
root,
|
|
@@ -571,13 +690,32 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
571
690
|
};
|
|
572
691
|
options.signal?.throwIfAborted();
|
|
573
692
|
options.validateMutation?.();
|
|
574
|
-
await writeJson(
|
|
693
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
575
694
|
await syncDirectory(transactionRoot());
|
|
576
695
|
await syncDirectory(stateDir());
|
|
577
|
-
await syncDirectory(
|
|
696
|
+
await syncDirectory(path6.dirname(stateDir()));
|
|
578
697
|
return { directory, journal, deletes: indexed.deletes, deletedWrites: indexed.deletedWrites };
|
|
579
698
|
}
|
|
580
|
-
async function restoreTransaction(directory, journal, options) {
|
|
699
|
+
async function restoreTransaction(directory, journal, options, heldKeys) {
|
|
700
|
+
validateJournal(directory, journal, options.sessionDir);
|
|
701
|
+
const missingSpellings = [];
|
|
702
|
+
for (const entry of journal.entries) {
|
|
703
|
+
if (!entry.afterTarget) continue;
|
|
704
|
+
options.validateMutation?.();
|
|
705
|
+
options.signal?.throwIfAborted();
|
|
706
|
+
const spelling = await caseReplacementSpelling(entry.target, entry.afterTarget);
|
|
707
|
+
options.validateMutation?.();
|
|
708
|
+
options.signal?.throwIfAborted();
|
|
709
|
+
if (spelling === void 0) missingSpellings.push(entry.target, entry.afterTarget);
|
|
710
|
+
}
|
|
711
|
+
return withMissingTargetQueues(
|
|
712
|
+
missingSpellings,
|
|
713
|
+
heldKeys,
|
|
714
|
+
options,
|
|
715
|
+
(reservedKeys) => restoreQueuedTransaction(directory, journal, options, reservedKeys)
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
async function restoreQueuedTransaction(directory, journal, options, heldKeys) {
|
|
581
719
|
validateJournal(directory, journal, options.sessionDir);
|
|
582
720
|
options.validateMutation?.();
|
|
583
721
|
options.signal?.throwIfAborted();
|
|
@@ -597,7 +735,7 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
597
735
|
await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
|
|
598
736
|
options.validateMutation?.();
|
|
599
737
|
options.signal?.throwIfAborted();
|
|
600
|
-
await
|
|
738
|
+
await fs4.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
|
|
601
739
|
options.validateMutation?.();
|
|
602
740
|
options.signal?.throwIfAborted();
|
|
603
741
|
}
|
|
@@ -611,9 +749,9 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
611
749
|
if (entry.kind === "file") {
|
|
612
750
|
const temporary2 = await prepareRecoveryStage(directory, journal, entry, options);
|
|
613
751
|
try {
|
|
614
|
-
await
|
|
615
|
-
await
|
|
616
|
-
const handle = await
|
|
752
|
+
await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
|
|
753
|
+
await fs4.copyFile(path6.join(directory, "before", entry.backupName), temporary2);
|
|
754
|
+
const handle = await fs4.open(temporary2, process.platform === "win32" ? "r+" : "r");
|
|
617
755
|
try {
|
|
618
756
|
await handle.sync();
|
|
619
757
|
} finally {
|
|
@@ -628,8 +766,9 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
628
766
|
await removeOwnedTarget(directory, entry, journal, options);
|
|
629
767
|
} else if (verified.startsWith("directory:") && !await removeOwnedTarget(directory, entry, journal, options, before))
|
|
630
768
|
continue;
|
|
631
|
-
await
|
|
632
|
-
|
|
769
|
+
await withMissingTargetQueues(
|
|
770
|
+
entry.afterTarget && (needsCaseQueue || verified === "missing") ? [entry.target, entry.afterTarget] : verified.startsWith("directory:") ? [entry.target] : [],
|
|
771
|
+
heldKeys,
|
|
633
772
|
options,
|
|
634
773
|
async () => {
|
|
635
774
|
const atRename = await verifyTarget(directory, entry, journal);
|
|
@@ -641,12 +780,12 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
641
780
|
options.validateMutation?.();
|
|
642
781
|
options.signal?.throwIfAborted();
|
|
643
782
|
if (atInstall === before && await spellingMatches()) return;
|
|
644
|
-
await
|
|
645
|
-
await syncDirectory(
|
|
783
|
+
await fs4.rename(temporary2, entry.target);
|
|
784
|
+
await syncDirectory(path6.dirname(entry.target));
|
|
646
785
|
}
|
|
647
786
|
);
|
|
648
787
|
} finally {
|
|
649
|
-
await
|
|
788
|
+
await fs4.rm(temporary2, { force: true });
|
|
650
789
|
}
|
|
651
790
|
continue;
|
|
652
791
|
}
|
|
@@ -656,10 +795,10 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
656
795
|
}
|
|
657
796
|
const temporary = await prepareRecoveryStage(directory, journal, entry, options);
|
|
658
797
|
try {
|
|
659
|
-
await
|
|
798
|
+
await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
|
|
660
799
|
await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
|
|
661
800
|
if (entry.kind === "directory") {
|
|
662
|
-
await
|
|
801
|
+
await fs4.cp(path6.join(directory, "before", entry.backupName), temporary, {
|
|
663
802
|
recursive: true,
|
|
664
803
|
dereference: false,
|
|
665
804
|
verbatimSymlinks: true,
|
|
@@ -670,25 +809,27 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
670
809
|
return true;
|
|
671
810
|
}
|
|
672
811
|
});
|
|
673
|
-
} else if (entry.linkTarget !== void 0) await
|
|
812
|
+
} else if (entry.linkTarget !== void 0) await fs4.symlink(entry.linkTarget, temporary);
|
|
674
813
|
if (await image(temporary, true) !== before)
|
|
675
814
|
throw new Error("Staged recovery image differs from the backup; evidence retained.");
|
|
676
815
|
options.validateMutation?.();
|
|
677
816
|
options.signal?.throwIfAborted();
|
|
678
817
|
if (!await removeOwnedTarget(directory, entry, journal, options, before)) continue;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
818
|
+
await withMissingTargetQueues([entry.target], heldKeys, options, async () => {
|
|
819
|
+
const atRename = await verifyTarget(directory, entry, journal);
|
|
820
|
+
options.validateMutation?.();
|
|
821
|
+
options.signal?.throwIfAborted();
|
|
822
|
+
if (atRename === before) return;
|
|
823
|
+
await retireRestorationIntent(directory, journal, entry, options);
|
|
824
|
+
const atInstall = await verifyTarget(directory, entry, journal, atRename === "missing");
|
|
825
|
+
options.validateMutation?.();
|
|
826
|
+
options.signal?.throwIfAborted();
|
|
827
|
+
if (atInstall === before) return;
|
|
828
|
+
await fs4.rename(temporary, entry.target);
|
|
829
|
+
await syncDirectory(path6.dirname(entry.target));
|
|
830
|
+
});
|
|
690
831
|
} finally {
|
|
691
|
-
await
|
|
832
|
+
await fs4.rm(temporary, { recursive: true, force: true });
|
|
692
833
|
}
|
|
693
834
|
}
|
|
694
835
|
await syncMutationParents(
|
|
@@ -733,13 +874,13 @@ async function removeOwnedTarget(directory, entry, journal, options, restoringBe
|
|
|
733
874
|
affected.replacementStarted = false;
|
|
734
875
|
}
|
|
735
876
|
}
|
|
736
|
-
await writeJson(
|
|
877
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
737
878
|
const verified = await verifyTarget(directory, entry, journal);
|
|
738
879
|
options.validateMutation?.();
|
|
739
880
|
options.signal?.throwIfAborted();
|
|
740
881
|
if (restoringBefore !== void 0 && verified === restoringBefore) return false;
|
|
741
|
-
await
|
|
742
|
-
await syncDirectory(
|
|
882
|
+
await fs4.rm(entry.target, { recursive: true, force: true });
|
|
883
|
+
await syncDirectory(path6.dirname(entry.target));
|
|
743
884
|
return true;
|
|
744
885
|
}
|
|
745
886
|
function upgradeRemovalEvidence(journal) {
|
|
@@ -757,7 +898,7 @@ async function retireRemovalIntent(directory, journal, entries, options) {
|
|
|
757
898
|
entry.removalPending = false;
|
|
758
899
|
entry.replacementStarted = true;
|
|
759
900
|
}
|
|
760
|
-
await writeJson(
|
|
901
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
761
902
|
options.validateMutation?.();
|
|
762
903
|
options.signal?.throwIfAborted();
|
|
763
904
|
}
|
|
@@ -772,8 +913,8 @@ async function retireRestorationIntent(directory, journal, entry, options) {
|
|
|
772
913
|
}
|
|
773
914
|
function recoveryStagePath(directory, entry) {
|
|
774
915
|
const token = createHash2("sha256").update(directory).update("\\0").update(entry.backupName).digest("hex");
|
|
775
|
-
return
|
|
776
|
-
|
|
916
|
+
return path6.join(
|
|
917
|
+
path6.dirname(entry.target),
|
|
777
918
|
`.pi-sync.json.${token}.${entry.kind === "file" ? "restore" : "restore-tree"}`
|
|
778
919
|
);
|
|
779
920
|
}
|
|
@@ -781,7 +922,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
781
922
|
const temporary = recoveryStagePath(directory, entry);
|
|
782
923
|
if (!entry.recoveryStaged) {
|
|
783
924
|
try {
|
|
784
|
-
await
|
|
925
|
+
await fs4.lstat(temporary);
|
|
785
926
|
throw new Error("Unowned recovery staging path; preserve evidence for review.");
|
|
786
927
|
} catch (error) {
|
|
787
928
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -789,7 +930,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
789
930
|
options.validateMutation?.();
|
|
790
931
|
options.signal?.throwIfAborted();
|
|
791
932
|
entry.recoveryStaged = true;
|
|
792
|
-
await writeJson(
|
|
933
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
793
934
|
options.validateMutation?.();
|
|
794
935
|
options.signal?.throwIfAborted();
|
|
795
936
|
}
|
|
@@ -798,24 +939,24 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
798
939
|
async function beforeImage(directory, entry) {
|
|
799
940
|
if (entry.kind === "missing") return "missing";
|
|
800
941
|
if (entry.kind === "symlink") return `symlink:${entry.linkTarget}`;
|
|
801
|
-
const value = await image(
|
|
942
|
+
const value = await image(path6.join(directory, "before", entry.backupName));
|
|
802
943
|
if (value === "missing") throw new Error("Transaction backup is missing; preserve evidence for review.");
|
|
803
944
|
return value;
|
|
804
945
|
}
|
|
805
946
|
async function image(target, durable = false) {
|
|
806
947
|
let stat;
|
|
807
948
|
try {
|
|
808
|
-
stat = await
|
|
949
|
+
stat = await fs4.lstat(target);
|
|
809
950
|
} catch (error) {
|
|
810
951
|
const code = error.code;
|
|
811
952
|
if (code === "ENOENT" || code === "ENOTDIR") return "missing";
|
|
812
953
|
throw error;
|
|
813
954
|
}
|
|
814
|
-
if (stat.isSymbolicLink()) return `symlink:${await
|
|
955
|
+
if (stat.isSymbolicLink()) return `symlink:${await fs4.readlink(target)}`;
|
|
815
956
|
if (stat.isFile()) {
|
|
816
|
-
const content = await
|
|
957
|
+
const content = await fs4.readFile(target);
|
|
817
958
|
if (durable) {
|
|
818
|
-
const handle = await
|
|
959
|
+
const handle = await fs4.open(target, process.platform === "win32" ? "r+" : "r");
|
|
819
960
|
try {
|
|
820
961
|
await handle.sync();
|
|
821
962
|
} finally {
|
|
@@ -826,8 +967,8 @@ async function image(target, durable = false) {
|
|
|
826
967
|
}
|
|
827
968
|
if (!stat.isDirectory()) throw new Error("Unsupported transaction image.");
|
|
828
969
|
const entries = [];
|
|
829
|
-
for (const name of (await
|
|
830
|
-
entries.push([name, await image(
|
|
970
|
+
for (const name of (await fs4.readdir(target)).sort())
|
|
971
|
+
entries.push([name, await image(path6.join(target, name), durable)]);
|
|
831
972
|
if (durable) await syncDirectory(target);
|
|
832
973
|
return `directory:${createHash2("sha256").update(JSON.stringify(entries)).digest("hex")}`;
|
|
833
974
|
}
|
|
@@ -836,22 +977,22 @@ async function ownedPostTree(target, files, requireComplete) {
|
|
|
836
977
|
const expected = new Map(files.map((file) => [mergePathIdentity(file.relative), file.image]));
|
|
837
978
|
const prefixes = /* @__PURE__ */ new Set();
|
|
838
979
|
for (const relative of expected.keys()) {
|
|
839
|
-
for (let parent =
|
|
980
|
+
for (let parent = path6.dirname(relative); parent !== "." && parent !== path6.dirname(parent); parent = path6.dirname(parent))
|
|
840
981
|
prefixes.add(parent);
|
|
841
982
|
}
|
|
842
983
|
const seen = /* @__PURE__ */ new Set();
|
|
843
984
|
async function visit(directory) {
|
|
844
985
|
let stat;
|
|
845
986
|
try {
|
|
846
|
-
stat = await
|
|
987
|
+
stat = await fs4.lstat(directory);
|
|
847
988
|
} catch {
|
|
848
989
|
return false;
|
|
849
990
|
}
|
|
850
991
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
851
|
-
for (const name of await
|
|
852
|
-
const child =
|
|
853
|
-
const relative = mergePathIdentity(
|
|
854
|
-
const stat2 = await
|
|
992
|
+
for (const name of await fs4.readdir(directory)) {
|
|
993
|
+
const child = path6.join(directory, name);
|
|
994
|
+
const relative = mergePathIdentity(path6.relative(target, child));
|
|
995
|
+
const stat2 = await fs4.lstat(child);
|
|
855
996
|
if (stat2.isDirectory() && !stat2.isSymbolicLink()) {
|
|
856
997
|
if (!prefixes.has(relative) || !await visit(child)) return false;
|
|
857
998
|
} else {
|
|
@@ -868,23 +1009,23 @@ function validateJournal(directory, journal, sessionDir) {
|
|
|
868
1009
|
throw new Error("Unsupported pi-sync transaction journal; preserve evidence for review.");
|
|
869
1010
|
if (journal.completed !== void 0 && (![6, 7].includes(journal.version) || journal.completed !== true))
|
|
870
1011
|
throw new Error("Invalid transaction completion evidence.");
|
|
871
|
-
const root =
|
|
872
|
-
if (typeof journal.root !== "string" ||
|
|
1012
|
+
const root = path6.resolve(agentDir());
|
|
1013
|
+
if (typeof journal.root !== "string" || path6.resolve(journal.root) !== root)
|
|
873
1014
|
throw new Error("Transaction root no longer matches the Pi agent directory.");
|
|
874
1015
|
if (journal.sessionRootTransition !== void 0) {
|
|
875
1016
|
if (journal.version !== 7 || typeof journal.sessionRoot !== "string")
|
|
876
1017
|
throw new Error("Invalid session root-transition version.");
|
|
877
1018
|
validateSessionRootTransition(journal.sessionRootTransition);
|
|
878
1019
|
} else if (journal.version === 7) throw new Error("Missing session root-transition evidence.");
|
|
879
|
-
const trustedSessionRoot = sessionDir ?
|
|
880
|
-
if (journal.sessionRoot !== void 0 && (typeof journal.sessionRoot !== "string" || !
|
|
1020
|
+
const trustedSessionRoot = sessionDir ? path6.resolve(sessionStorageRoot(root, sessionDir)) : path6.join(root, "sessions");
|
|
1021
|
+
if (journal.sessionRoot !== void 0 && (typeof journal.sessionRoot !== "string" || !path6.isAbsolute(journal.sessionRoot) || path6.resolve(journal.sessionRoot) !== journal.sessionRoot || !journal.completed && transactionHasSessionTargets(journal) && journal.sessionRoot !== trustedSessionRoot))
|
|
881
1022
|
throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
|
|
882
1023
|
for (const entry of journal.entries) {
|
|
883
|
-
if (!entry || typeof entry.target !== "string" || !
|
|
1024
|
+
if (!entry || typeof entry.target !== "string" || !path6.isAbsolute(entry.target) || path6.resolve(entry.target) !== entry.target || typeof entry.backupName !== "string" || !/^\d+$/u.test(entry.backupName) || !["missing", "file", "directory", "symlink"].includes(entry.kind))
|
|
884
1025
|
throw new Error("Invalid pi-sync transaction entry.");
|
|
885
|
-
assertWithinRoot(directory,
|
|
1026
|
+
assertWithinRoot(directory, path6.join(directory, "before", entry.backupName));
|
|
886
1027
|
assertAllowedTarget(root, journal.sessionRoot, entry.target);
|
|
887
|
-
if (entry.afterTarget !== void 0 && (![5, 6, 7].includes(journal.version) || typeof entry.afterTarget !== "string" || !isCaseReplacement(root, entry.target, entry.afterTarget) ||
|
|
1028
|
+
if (entry.afterTarget !== void 0 && (![5, 6, 7].includes(journal.version) || typeof entry.afterTarget !== "string" || !isCaseReplacement(root, entry.target, entry.afterTarget) || path6.resolve(entry.afterTarget) !== entry.afterTarget || entry.kind !== "file"))
|
|
888
1029
|
throw new Error("Invalid transaction case replacement evidence.");
|
|
889
1030
|
if (entry.afterTarget) assertAllowedTarget(root, journal.sessionRoot, entry.afterTarget);
|
|
890
1031
|
if (journal.version >= 2 && ((entry.beforeImage === void 0 ? !journal.completed : typeof entry.beforeImage !== "string") || (entry.afterImage === void 0 ? !journal.completed : typeof entry.afterImage !== "string") || (entry.postFiles === void 0 ? !journal.completed : !Array.isArray(entry.postFiles))))
|
|
@@ -898,29 +1039,29 @@ function validateJournal(directory, journal, sessionDir) {
|
|
|
898
1039
|
if (entry.removalPending && entry.replacementStarted)
|
|
899
1040
|
throw new Error("Conflicting transaction replacement evidence.");
|
|
900
1041
|
for (const file of entry.postFiles ?? []) {
|
|
901
|
-
if (typeof file.relative !== "string" || typeof file.image !== "string" || !isStrictlyInside(entry.target,
|
|
1042
|
+
if (typeof file.relative !== "string" || typeof file.image !== "string" || !isStrictlyInside(entry.target, path6.resolve(entry.target, file.relative)))
|
|
902
1043
|
throw new Error("Invalid transaction subtree evidence.");
|
|
903
1044
|
}
|
|
904
1045
|
}
|
|
905
1046
|
}
|
|
906
1047
|
function transactionHasSessionTargets(journal) {
|
|
907
|
-
const root =
|
|
1048
|
+
const root = path6.resolve(agentDir());
|
|
908
1049
|
return Array.isArray(journal.entries) && journal.entries.some(
|
|
909
|
-
(entry) => typeof entry?.target === "string" && (!isPathInside(root, entry.target) || isPathInside(
|
|
1050
|
+
(entry) => typeof entry?.target === "string" && (!isPathInside(root, entry.target) || isPathInside(path6.join(root, "sessions"), entry.target) || typeof journal.sessionRoot === "string" && isPathInside(journal.sessionRoot, entry.target))
|
|
910
1051
|
);
|
|
911
1052
|
}
|
|
912
1053
|
function isStrictlyInside(root, target) {
|
|
913
|
-
return
|
|
1054
|
+
return path6.relative(path6.resolve(root), path6.resolve(target)) !== "" && isPathInside(root, target);
|
|
914
1055
|
}
|
|
915
1056
|
function assertAllowedTarget(root, sessionRoot, target) {
|
|
916
|
-
const resolved =
|
|
1057
|
+
const resolved = path6.resolve(target);
|
|
917
1058
|
if (isStrictlyInside(root, resolved)) {
|
|
918
|
-
if (isDeniedPath(
|
|
1059
|
+
if (isDeniedPath(path6.relative(root, resolved))) throw new Error("Transaction targets denied private storage.");
|
|
919
1060
|
assertWithinRoot(root, resolved);
|
|
920
1061
|
return;
|
|
921
1062
|
}
|
|
922
1063
|
if (sessionRoot && isStrictlyInside(sessionRoot, resolved)) {
|
|
923
|
-
if (isDeniedPath(`sessions/${
|
|
1064
|
+
if (isDeniedPath(`sessions/${path6.relative(sessionRoot, resolved)}`))
|
|
924
1065
|
throw new Error("Transaction targets denied session storage.");
|
|
925
1066
|
assertWithinRoot(sessionRoot, resolved);
|
|
926
1067
|
return;
|
|
@@ -930,10 +1071,10 @@ function assertAllowedTarget(root, sessionRoot, target) {
|
|
|
930
1071
|
async function assertSafeParents(root, sessionRoot, target) {
|
|
931
1072
|
assertAllowedTarget(root, sessionRoot, target);
|
|
932
1073
|
const boundary = isPathInside(root, target) ? root : sessionRoot;
|
|
933
|
-
for (let parent =
|
|
934
|
-
if (
|
|
1074
|
+
for (let parent = path6.dirname(target); boundary && mergePathIdentity(parent) !== mergePathIdentity(boundary); parent = path6.dirname(parent)) {
|
|
1075
|
+
if (path6.dirname(parent) === parent) throw new Error("Transaction parent did not reach its owned boundary.");
|
|
935
1076
|
try {
|
|
936
|
-
const stat = await
|
|
1077
|
+
const stat = await fs4.lstat(parent);
|
|
937
1078
|
if (!stat.isDirectory() && !stat.isFile() || stat.isSymbolicLink())
|
|
938
1079
|
throw new Error("Unsafe transaction parent.");
|
|
939
1080
|
} catch (error) {
|
|
@@ -942,50 +1083,9 @@ async function assertSafeParents(root, sessionRoot, target) {
|
|
|
942
1083
|
}
|
|
943
1084
|
}
|
|
944
1085
|
}
|
|
945
|
-
async function withTargetQueues(targets, action) {
|
|
946
|
-
const keys = /* @__PURE__ */ new Set();
|
|
947
|
-
for (const target of targets) {
|
|
948
|
-
let key = path5.resolve(target);
|
|
949
|
-
try {
|
|
950
|
-
key = await fs3.realpath(target);
|
|
951
|
-
} catch (error) {
|
|
952
|
-
const code = error.code;
|
|
953
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
954
|
-
}
|
|
955
|
-
if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
|
|
956
|
-
keys.add(key);
|
|
957
|
-
}
|
|
958
|
-
async function acquire(index) {
|
|
959
|
-
const target = targets[index];
|
|
960
|
-
return target ? withFileMutationQueue(target, () => acquire(index + 1)) : action();
|
|
961
|
-
}
|
|
962
|
-
return acquire(0);
|
|
963
|
-
}
|
|
964
|
-
async function withCaseDestinationQueues(spellings, options, action) {
|
|
965
|
-
if (!spellings.length) return action();
|
|
966
|
-
const requireAbsent = async () => {
|
|
967
|
-
for (const [before, after] of spellings) {
|
|
968
|
-
options.validateMutation?.();
|
|
969
|
-
options.signal?.throwIfAborted();
|
|
970
|
-
const spelling = await caseReplacementSpelling(before, after);
|
|
971
|
-
options.validateMutation?.();
|
|
972
|
-
options.signal?.throwIfAborted();
|
|
973
|
-
if (spelling !== void 0)
|
|
974
|
-
throw new Error("Case replacement changed before destination queue reservation; evidence retained.");
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
await requireAbsent();
|
|
978
|
-
return withTargetQueues(
|
|
979
|
-
spellings.map(([, after]) => after),
|
|
980
|
-
async () => {
|
|
981
|
-
await requireAbsent();
|
|
982
|
-
return action();
|
|
983
|
-
}
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
1086
|
async function completeTransaction(directory, journal) {
|
|
987
1087
|
await writeJson(
|
|
988
|
-
|
|
1088
|
+
path6.join(directory, "journal.json"),
|
|
989
1089
|
{ ...journal, version: journal.sessionRootTransition ? 7 : 6, completed: true },
|
|
990
1090
|
{
|
|
991
1091
|
maxBytes: 32 * 1024 * 1024
|
|
@@ -994,15 +1094,16 @@ async function completeTransaction(directory, journal) {
|
|
|
994
1094
|
await removeTransaction(directory);
|
|
995
1095
|
}
|
|
996
1096
|
async function removeTransaction(directory) {
|
|
997
|
-
await
|
|
1097
|
+
await fs4.rm(directory, { recursive: true, force: true });
|
|
998
1098
|
await syncDirectory(transactionRoot()).catch(() => {
|
|
999
1099
|
});
|
|
1000
1100
|
}
|
|
1001
1101
|
|
|
1002
1102
|
export {
|
|
1003
1103
|
syncMutationParents,
|
|
1104
|
+
assertCanonicalMutationTargets,
|
|
1004
1105
|
applySnapshotTransaction,
|
|
1005
1106
|
recoverSnapshotTransactionsOnStartup,
|
|
1006
1107
|
recoverPendingSnapshotTransactions
|
|
1007
1108
|
};
|
|
1008
|
-
//# sourceMappingURL=chunk-
|
|
1109
|
+
//# sourceMappingURL=chunk-7A35ZAVO.ts.map
|