@narumitw/pi-sync 0.57.9 → 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 +14 -0
- package/README.md +7 -1
- package/dist/chunks/{agent-candidate-GNEYR225.ts → agent-candidate-OCDPRI5G.ts} +12 -9
- package/dist/chunks/{agent-plan-3L7CRFDM.ts → agent-plan-P5B5RGZ7.ts} +22 -18
- package/dist/chunks/{agent-plan-3L7CRFDM.ts.map → agent-plan-P5B5RGZ7.ts.map} +1 -1
- package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts → agent-refusal-retry-476YGNU5.ts} +11 -8
- package/dist/chunks/{agent-refusal-retry-RPTMUQXK.ts.map → agent-refusal-retry-476YGNU5.ts.map} +1 -1
- package/dist/chunks/{agent-store-55CZOCPX.ts → agent-store-RJSMNOYP.ts} +12 -11
- package/dist/chunks/{agent-store-55CZOCPX.ts.map → agent-store-RJSMNOYP.ts.map} +1 -1
- package/dist/chunks/agent-sync-EUK2CPSS.ts +22 -0
- package/dist/chunks/{chunk-U3XRI2HF.ts → chunk-3IYAPMST.ts} +11 -11
- package/dist/chunks/{chunk-B4QZBDBT.ts → chunk-45COPAK7.ts} +10 -3
- package/dist/chunks/chunk-45COPAK7.ts.map +7 -0
- package/dist/chunks/chunk-5SFMBY5P.ts +26 -0
- package/dist/chunks/chunk-5SFMBY5P.ts.map +7 -0
- package/dist/chunks/{chunk-ED3TYIVW.ts → chunk-5TSCBBPP.ts} +5 -5
- package/dist/chunks/{chunk-5QAE3G3K.ts → chunk-67INJPKR.ts} +6 -6
- package/dist/chunks/{chunk-IWZH5KYS.ts → chunk-6LD5KX55.ts} +10 -10
- package/dist/chunks/{chunk-AAEEHR7H.ts → chunk-6O3EDBRL.ts} +2 -2
- package/dist/chunks/{chunk-CRKTAPSL.ts → chunk-7A35ZAVO.ts} +333 -237
- package/dist/chunks/chunk-7A35ZAVO.ts.map +7 -0
- package/dist/chunks/{chunk-6FVVRTL5.ts → chunk-7OLLPPLU.ts} +77 -63
- package/dist/chunks/chunk-7OLLPPLU.ts.map +7 -0
- package/dist/chunks/{chunk-PQW6GS4J.ts → chunk-7ZIYU5QS.ts} +12 -10
- package/dist/chunks/{chunk-PQW6GS4J.ts.map → chunk-7ZIYU5QS.ts.map} +1 -1
- package/dist/chunks/{chunk-S7QS4AWK.ts → chunk-AKMV47YC.ts} +5 -5
- package/dist/chunks/chunk-AR2DGDUI.ts +55 -0
- package/dist/chunks/chunk-AR2DGDUI.ts.map +7 -0
- package/dist/chunks/{chunk-GNH73BJ5.ts → chunk-BEST45H2.ts} +7 -7
- package/dist/chunks/chunk-CBLTWXAU.ts +114 -0
- package/dist/chunks/chunk-CBLTWXAU.ts.map +7 -0
- package/dist/chunks/{chunk-GP5DU62N.ts → chunk-CPT2BN6N.ts} +2 -2
- package/dist/chunks/{chunk-UPNVIUBN.ts → chunk-DN4IA7MR.ts} +6 -6
- package/dist/chunks/{chunk-PG7NMFP2.ts → chunk-DRTC4PKM.ts} +8 -8
- package/dist/chunks/chunk-ESKOPQVB.ts +348 -0
- package/dist/chunks/chunk-ESKOPQVB.ts.map +7 -0
- package/dist/chunks/{chunk-MKKF4IMY.ts → chunk-F7E3VFXU.ts} +5 -5
- package/dist/chunks/{chunk-HUZ5CR4W.ts → chunk-FHE2DCA2.ts} +3 -52
- package/dist/chunks/chunk-FHE2DCA2.ts.map +7 -0
- package/dist/chunks/{chunk-UBV24CG4.ts → chunk-FLHCOJCD.ts} +3 -3
- package/dist/chunks/chunk-GNYGNHFF.ts +27 -0
- package/dist/chunks/chunk-GNYGNHFF.ts.map +7 -0
- package/dist/chunks/chunk-KYTT7KV4.ts +319 -0
- package/dist/chunks/chunk-KYTT7KV4.ts.map +7 -0
- package/dist/chunks/{chunk-A6HU4ERM.ts → chunk-MKSMJPVA.ts} +6 -23
- package/dist/chunks/chunk-MKSMJPVA.ts.map +7 -0
- package/dist/chunks/{chunk-ORGBI36T.ts → chunk-OJARL2TG.ts} +4 -4
- package/dist/chunks/{chunk-JCH3VXIO.ts → chunk-OKPPZA4D.ts} +150 -91
- package/dist/chunks/chunk-OKPPZA4D.ts.map +7 -0
- package/dist/chunks/{chunk-QO333NM4.ts → chunk-OVBAW7KV.ts} +180 -133
- package/dist/chunks/chunk-OVBAW7KV.ts.map +7 -0
- package/dist/chunks/{chunk-GSNPASKB.ts → chunk-Q23JJW7E.ts} +6 -6
- package/dist/chunks/{chunk-YK52VDXG.ts → chunk-TIMWN2RH.ts} +10 -8
- package/dist/chunks/{chunk-YK52VDXG.ts.map → chunk-TIMWN2RH.ts.map} +1 -1
- package/dist/chunks/{chunk-UOHR7LRO.ts → chunk-TZC7GDJE.ts} +10 -22
- package/dist/chunks/chunk-TZC7GDJE.ts.map +7 -0
- package/dist/chunks/chunk-X7R6DCVF.ts +28 -0
- package/dist/chunks/chunk-X7R6DCVF.ts.map +7 -0
- package/dist/chunks/{chunk-76NB4R7U.ts → chunk-XJHQPB3E.ts} +2 -2
- package/dist/chunks/{chunk-BDB4IDM3.ts → chunk-Y4UVXOLQ.ts} +8 -6
- package/dist/chunks/{chunk-BDB4IDM3.ts.map → chunk-Y4UVXOLQ.ts.map} +1 -1
- package/dist/chunks/{chunk-P7DD5PWZ.ts → chunk-ZDDBZH72.ts} +3 -3
- package/dist/chunks/command-handler-F2E7A5T5.ts +548 -0
- package/dist/chunks/command-handler-F2E7A5T5.ts.map +7 -0
- package/dist/chunks/{config-KGIYTTXQ.ts → config-HYG7G5EM.ts} +7 -6
- package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts → conflict-artifacts-J6WSDCWP.ts} +13 -9
- package/dist/chunks/{conflict-review-3X37QEIT.ts → conflict-review-2QMO2K6Q.ts} +31 -25
- package/dist/chunks/{conflict-review-3X37QEIT.ts.map → conflict-review-2QMO2K6Q.ts.map} +1 -1
- package/dist/chunks/{continuous-record-SQJ47CXN.ts → continuous-record-AH7JGYKM.ts} +8 -7
- package/dist/chunks/{continuous-sync-Q6QUD6TY.ts → continuous-sync-VRMXCA5X.ts} +33 -28
- package/dist/chunks/{continuous-sync-Q6QUD6TY.ts.map → continuous-sync-VRMXCA5X.ts.map} +1 -1
- package/dist/chunks/{file-selection-3JJYOVNR.ts → file-selection-IELTXSGZ.ts} +11 -10
- package/dist/chunks/{file-selection-3JJYOVNR.ts.map → file-selection-IELTXSGZ.ts.map} +1 -1
- package/dist/chunks/{git-backend-NYKSG5X6.ts → git-backend-KHD36W27.ts} +15 -12
- package/dist/chunks/{git-backend-NYKSG5X6.ts.map → git-backend-KHD36W27.ts.map} +1 -1
- package/dist/chunks/{local-fields-6DGYOOIK.ts → local-fields-JEKAPZKD.ts} +9 -5
- package/dist/chunks/manager-result-dispatcher-6W52NM7F.ts +32 -0
- package/dist/chunks/{manager-ui-ZH6DEC2G.ts → manager-ui-LM7DSJ2Y.ts} +124 -67
- package/dist/chunks/manager-ui-LM7DSJ2Y.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts → merge-baseline-store-65GKUMXI.ts} +6 -5
- package/dist/chunks/remote-selection-ui-TD4C4W3J.ts +31 -0
- package/dist/chunks/{s3-backend-F7PR5FB3.ts → s3-backend-L24UZ6IK.ts} +24 -18
- package/dist/chunks/s3-backend-L24UZ6IK.ts.map +7 -0
- package/dist/chunks/{setup-switch-E7PHKI6J.ts → setup-switch-PIPXYPCW.ts} +6 -7
- package/dist/chunks/{setup-wizard-OVHZYXNW.ts → setup-wizard-RR5U3SUB.ts} +8 -9
- package/dist/chunks/{setup-wizard-OVHZYXNW.ts.map → setup-wizard-RR5U3SUB.ts.map} +1 -1
- package/dist/chunks/{snapshot-THS5EY2L.ts → snapshot-RGNDYT6A.ts} +10 -6
- package/dist/chunks/snapshot-transaction-26BQO36S.ts +24 -0
- package/dist/chunks/sync-inspection-MOMLGUTD.ts +25 -0
- package/dist/chunks/sync-inspection-MOMLGUTD.ts.map +7 -0
- package/dist/chunks/{sync-operations-Y72GNIQ2.ts → sync-operations-CV3TULLD.ts} +66 -57
- package/dist/chunks/{sync-operations-Y72GNIQ2.ts.map → sync-operations-CV3TULLD.ts.map} +1 -1
- package/dist/chunks/{sync-state-CFL4RRRB.ts → sync-state-7L4NT7W2.ts} +8 -5
- package/dist/chunks/sync-state-7L4NT7W2.ts.map +7 -0
- package/dist/chunks/{webdav-backend-TGFUH3SF.ts → webdav-backend-3ZCACILP.ts} +15 -12
- package/dist/chunks/{webdav-backend-TGFUH3SF.ts.map → webdav-backend-3ZCACILP.ts.map} +1 -1
- package/dist/index.ts +54 -537
- package/dist/index.ts.map +3 -3
- package/package.json +2 -2
- 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 +2 -2
- package/src/settings/settings-management.ts +15 -1
- package/src/settings/settings-store.ts +0 -14
- package/src/settings/settings-validation.ts +1 -1
- package/src/snapshot/session-paths.ts +15 -2
- package/src/snapshot/snapshot-mutation-queues.ts +142 -0
- package/src/snapshot/snapshot-transaction-directory.ts +16 -0
- package/src/snapshot/snapshot-transaction.ts +123 -137
- package/src/sync/automatic-sync.ts +12 -6
- package/src/sync/local-field-policy.ts +29 -0
- package/src/sync/local-fields.ts +3 -29
- package/src/sync/merge-apply.ts +6 -0
- package/src/sync/mutation-owner.ts +22 -0
- package/src/sync/startup-check.ts +3 -2
- package/src/sync/sync-loaders.ts +4 -0
- package/src/sync/sync-local.ts +2 -19
- package/src/sync-extension.ts +7 -2
- 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-6FVVRTL5.ts.map +0 -7
- package/dist/chunks/chunk-A6HU4ERM.ts.map +0 -7
- package/dist/chunks/chunk-B4QZBDBT.ts.map +0 -7
- package/dist/chunks/chunk-CRKTAPSL.ts.map +0 -7
- package/dist/chunks/chunk-HUZ5CR4W.ts.map +0 -7
- package/dist/chunks/chunk-JCH3VXIO.ts.map +0 -7
- package/dist/chunks/chunk-QO333NM4.ts.map +0 -7
- package/dist/chunks/chunk-QOFMXPM5.ts +0 -735
- package/dist/chunks/chunk-QOFMXPM5.ts.map +0 -7
- package/dist/chunks/chunk-UOHR7LRO.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-HOCM2GDA.ts +0 -24
- package/dist/chunks/manager-ui-ZH6DEC2G.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-YFPGYGNE.ts +0 -23
- package/dist/chunks/s3-backend-F7PR5FB3.ts.map +0 -7
- package/dist/chunks/sync-inspection-J25EEKZK.ts +0 -21
- /package/dist/chunks/{agent-candidate-GNEYR225.ts.map → agent-candidate-OCDPRI5G.ts.map} +0 -0
- /package/dist/chunks/{config-KGIYTTXQ.ts.map → agent-sync-EUK2CPSS.ts.map} +0 -0
- /package/dist/chunks/{chunk-U3XRI2HF.ts.map → chunk-3IYAPMST.ts.map} +0 -0
- /package/dist/chunks/{chunk-ED3TYIVW.ts.map → chunk-5TSCBBPP.ts.map} +0 -0
- /package/dist/chunks/{chunk-5QAE3G3K.ts.map → chunk-67INJPKR.ts.map} +0 -0
- /package/dist/chunks/{chunk-IWZH5KYS.ts.map → chunk-6LD5KX55.ts.map} +0 -0
- /package/dist/chunks/{chunk-AAEEHR7H.ts.map → chunk-6O3EDBRL.ts.map} +0 -0
- /package/dist/chunks/{chunk-S7QS4AWK.ts.map → chunk-AKMV47YC.ts.map} +0 -0
- /package/dist/chunks/{chunk-GNH73BJ5.ts.map → chunk-BEST45H2.ts.map} +0 -0
- /package/dist/chunks/{chunk-GP5DU62N.ts.map → chunk-CPT2BN6N.ts.map} +0 -0
- /package/dist/chunks/{chunk-UPNVIUBN.ts.map → chunk-DN4IA7MR.ts.map} +0 -0
- /package/dist/chunks/{chunk-PG7NMFP2.ts.map → chunk-DRTC4PKM.ts.map} +0 -0
- /package/dist/chunks/{chunk-MKKF4IMY.ts.map → chunk-F7E3VFXU.ts.map} +0 -0
- /package/dist/chunks/{chunk-UBV24CG4.ts.map → chunk-FLHCOJCD.ts.map} +0 -0
- /package/dist/chunks/{chunk-ORGBI36T.ts.map → chunk-OJARL2TG.ts.map} +0 -0
- /package/dist/chunks/{chunk-GSNPASKB.ts.map → chunk-Q23JJW7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-76NB4R7U.ts.map → chunk-XJHQPB3E.ts.map} +0 -0
- /package/dist/chunks/{chunk-P7DD5PWZ.ts.map → chunk-ZDDBZH72.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-FXFIIBCB.ts.map → config-HYG7G5EM.ts.map} +0 -0
- /package/dist/chunks/{continuous-record-SQJ47CXN.ts.map → conflict-artifacts-J6WSDCWP.ts.map} +0 -0
- /package/dist/chunks/{local-fields-6DGYOOIK.ts.map → continuous-record-AH7JGYKM.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-HOCM2GDA.ts.map → local-fields-JEKAPZKD.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-RFCAH7KL.ts.map → manager-result-dispatcher-6W52NM7F.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-YFPGYGNE.ts.map → merge-baseline-store-65GKUMXI.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-E7PHKI6J.ts.map → remote-selection-ui-TD4C4W3J.ts.map} +0 -0
- /package/dist/chunks/{snapshot-THS5EY2L.ts.map → setup-switch-PIPXYPCW.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-J25EEKZK.ts.map → snapshot-RGNDYT6A.ts.map} +0 -0
- /package/dist/chunks/{sync-state-CFL4RRRB.ts.map → snapshot-transaction-26BQO36S.ts.map} +0 -0
|
@@ -1,27 +1,38 @@
|
|
|
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
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
pendingTransactionEntries,
|
|
5
|
+
transactionRoot
|
|
6
|
+
} from "./chunk-5SFMBY5P.ts";
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
expandSessionDir,
|
|
10
|
-
sessionStorageRoot
|
|
11
|
-
} from "./chunk-QOFMXPM5.ts";
|
|
8
|
+
withLock
|
|
9
|
+
} from "./chunk-6O3EDBRL.ts";
|
|
12
10
|
import {
|
|
13
11
|
mergePathIdentity
|
|
14
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XJHQPB3E.ts";
|
|
13
|
+
import {
|
|
14
|
+
syncDirectory,
|
|
15
|
+
writeJson
|
|
16
|
+
} from "./chunk-AR2DGDUI.ts";
|
|
15
17
|
import {
|
|
16
18
|
stateDir
|
|
17
19
|
} from "./chunk-SWRVXVFD.ts";
|
|
20
|
+
import {
|
|
21
|
+
agentDir,
|
|
22
|
+
configuredSessionDir,
|
|
23
|
+
expandSessionDir,
|
|
24
|
+
sessionStorageRoot
|
|
25
|
+
} from "./chunk-CBLTWXAU.ts";
|
|
18
26
|
import {
|
|
19
27
|
assertWithinRoot,
|
|
20
28
|
isDeniedPath,
|
|
21
|
-
isPathInside
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
isPathInside
|
|
30
|
+
} from "./chunk-FHE2DCA2.ts";
|
|
31
|
+
|
|
32
|
+
// src/snapshot/snapshot-transaction.ts
|
|
33
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
34
|
+
import fs4 from "node:fs/promises";
|
|
35
|
+
import path6 from "node:path";
|
|
25
36
|
|
|
26
37
|
// src/state/mutation-directory-sync.ts
|
|
27
38
|
import path from "node:path";
|
|
@@ -50,12 +61,6 @@ async function syncMutationParents(targets, roots, options = {}) {
|
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
// src/snapshot/snapshot-transaction.ts
|
|
54
|
-
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
55
|
-
import fs3 from "node:fs/promises";
|
|
56
|
-
import path5 from "node:path";
|
|
57
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
58
|
-
|
|
59
64
|
// src/snapshot/snapshot-case-replacement.ts
|
|
60
65
|
import fs from "node:fs/promises";
|
|
61
66
|
import path2 from "node:path";
|
|
@@ -126,13 +131,124 @@ async function coalesceCaseReplacements(root, plan) {
|
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
// src/snapshot/snapshot-
|
|
134
|
+
// src/snapshot/snapshot-mutation-queues.ts
|
|
130
135
|
import fs2 from "node:fs/promises";
|
|
131
|
-
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";
|
|
132
248
|
|
|
133
249
|
// src/snapshot/snapshot-transaction-plan.ts
|
|
134
250
|
import { createHash } from "node:crypto";
|
|
135
|
-
import
|
|
251
|
+
import path4 from "node:path";
|
|
136
252
|
function fileImage(content) {
|
|
137
253
|
return `file:${createHash("sha256").update(content).digest("hex")}`;
|
|
138
254
|
}
|
|
@@ -145,7 +261,7 @@ function indexTransactionPlan(plan, targets) {
|
|
|
145
261
|
if (!writeImages.has(write.target)) writeImages.set(write.target, image2);
|
|
146
262
|
for (const ancestor of ancestorTargets(write.target, targetIndex, false)) {
|
|
147
263
|
if (ancestor !== write.target && isStrictDescendant(ancestor, write.target)) {
|
|
148
|
-
postFiles.get(ancestor)?.push({ relative:
|
|
264
|
+
postFiles.get(ancestor)?.push({ relative: path4.relative(ancestor, write.target), image: image2 });
|
|
149
265
|
}
|
|
150
266
|
}
|
|
151
267
|
}
|
|
@@ -169,10 +285,10 @@ function indexTransactionPlan(plan, targets) {
|
|
|
169
285
|
return { writeImages, postFiles, deletes, deletedWrites };
|
|
170
286
|
}
|
|
171
287
|
function isStrictDescendant(parent, target) {
|
|
172
|
-
return
|
|
288
|
+
return path4.relative(path4.resolve(parent), path4.resolve(target)) !== "" && isPathInside(parent, target);
|
|
173
289
|
}
|
|
174
290
|
function pathKey(target) {
|
|
175
|
-
const resolved =
|
|
291
|
+
const resolved = path4.resolve(target);
|
|
176
292
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
177
293
|
}
|
|
178
294
|
function indexTargets(targets) {
|
|
@@ -186,10 +302,10 @@ function indexTargets(targets) {
|
|
|
186
302
|
return index;
|
|
187
303
|
}
|
|
188
304
|
function* ancestorTargets(target, index, includeSelf = true) {
|
|
189
|
-
const resolved =
|
|
190
|
-
for (let ancestor = includeSelf ? resolved :
|
|
305
|
+
const resolved = path4.resolve(target);
|
|
306
|
+
for (let ancestor = includeSelf ? resolved : path4.dirname(resolved); ; ancestor = path4.dirname(ancestor)) {
|
|
191
307
|
yield* index.get(pathKey(ancestor)) ?? [];
|
|
192
|
-
if (
|
|
308
|
+
if (path4.dirname(ancestor) === ancestor) return;
|
|
193
309
|
}
|
|
194
310
|
}
|
|
195
311
|
|
|
@@ -209,36 +325,36 @@ function rootFromSettings(root, bytes) {
|
|
|
209
325
|
throw new Error("Invalid settings root-transition evidence.");
|
|
210
326
|
const value = settings.sessionDir;
|
|
211
327
|
if (value !== void 0 && typeof value !== "string") throw new Error("Invalid settings root-transition evidence.");
|
|
212
|
-
return
|
|
328
|
+
return path5.resolve(sessionStorageRoot(root, value ? expandSessionDir(value) : void 0));
|
|
213
329
|
}
|
|
214
330
|
async function beforeRoot(directory, root, entry, guards) {
|
|
215
331
|
validateOwner(guards);
|
|
216
|
-
if (entry.kind === "missing" && entry.beforeImage === "missing") return
|
|
332
|
+
if (entry.kind === "missing" && entry.beforeImage === "missing") return path5.join(root, "sessions");
|
|
217
333
|
if (entry.kind !== "file") throw new Error("Root transition requires regular settings preimage evidence.");
|
|
218
|
-
const backup =
|
|
219
|
-
const stat = await
|
|
334
|
+
const backup = path5.join(directory, "before", entry.backupName);
|
|
335
|
+
const stat = await fs3.lstat(backup);
|
|
220
336
|
validateOwner(guards);
|
|
221
337
|
if (!stat.isFile()) throw new Error("Invalid settings root-transition backup.");
|
|
222
|
-
const bytes = await
|
|
338
|
+
const bytes = await fs3.readFile(backup);
|
|
223
339
|
validateOwner(guards);
|
|
224
340
|
if (fileImage(bytes) !== entry.beforeImage) throw new Error("Changed settings root-transition backup.");
|
|
225
341
|
return rootFromSettings(root, bytes);
|
|
226
342
|
}
|
|
227
343
|
async function prepareSessionRootTransition(directory, root, sessionRoot, entries, plan, guards = {}) {
|
|
228
|
-
const settingsTarget =
|
|
344
|
+
const settingsTarget = path5.join(root, "settings.json");
|
|
229
345
|
const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
|
|
230
346
|
const write = plan.writes.find((item) => item.target === settingsTarget || item.target === entry?.target);
|
|
231
347
|
if (!entry || !entries.some((item) => item !== entry && isPathInside(sessionRoot, item.target))) return void 0;
|
|
232
348
|
const deleted = !write && plan.deletes.includes(entry.target) && entry.afterImage === "missing";
|
|
233
349
|
if (!write && !deleted) return void 0;
|
|
234
|
-
const afterRoot = write ? rootFromSettings(root, write.content) :
|
|
350
|
+
const afterRoot = write ? rootFromSettings(root, write.content) : path5.join(root, "sessions");
|
|
235
351
|
if (afterRoot !== sessionRoot) return void 0;
|
|
236
352
|
const previous = await beforeRoot(directory, root, entry, guards);
|
|
237
353
|
if (previous === sessionRoot) return void 0;
|
|
238
354
|
if (write && fileImage(write.content) !== entry.afterImage) throw new Error("Changed settings transition postimage.");
|
|
239
355
|
if (!write) return { beforeRoot: previous, settingsAfterBase64: null };
|
|
240
356
|
validateOwner(guards);
|
|
241
|
-
const handle = await
|
|
357
|
+
const handle = await fs3.open(path5.join(directory, "settings-after"), "wx", 384);
|
|
242
358
|
try {
|
|
243
359
|
validateOwner(guards);
|
|
244
360
|
await handle.writeFile(write.content);
|
|
@@ -253,25 +369,25 @@ async function prepareSessionRootTransition(directory, root, sessionRoot, entrie
|
|
|
253
369
|
return { beforeRoot: previous, settingsAfterFile: true };
|
|
254
370
|
}
|
|
255
371
|
function validateSessionRootTransition(value) {
|
|
256
|
-
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)))
|
|
257
373
|
throw new Error("Invalid session root-transition evidence.");
|
|
258
374
|
}
|
|
259
375
|
async function resolveTransitionSessionRoot(directory, root, sessionRoot, transition, entries, currentRoot, guards = {}) {
|
|
260
|
-
const settingsTarget =
|
|
376
|
+
const settingsTarget = path5.join(root, "settings.json");
|
|
261
377
|
const entry = entries.find((item) => item.target === settingsTarget || item.afterTarget === settingsTarget);
|
|
262
378
|
if (!entry) throw new Error("Missing settings root-transition evidence.");
|
|
263
379
|
let after;
|
|
264
380
|
if (transition.settingsAfterFile) {
|
|
265
|
-
const file =
|
|
266
|
-
const stat = await
|
|
381
|
+
const file = path5.join(directory, "settings-after");
|
|
382
|
+
const stat = await fs3.lstat(file);
|
|
267
383
|
validateOwner(guards);
|
|
268
384
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
269
385
|
throw new Error("Unsafe settings root-transition postimage.");
|
|
270
|
-
after = await
|
|
386
|
+
after = await fs3.readFile(file);
|
|
271
387
|
validateOwner(guards);
|
|
272
388
|
} else if (transition.settingsAfterBase64 !== null)
|
|
273
389
|
after = Buffer.from(transition.settingsAfterBase64, "base64");
|
|
274
|
-
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)
|
|
275
391
|
throw new Error("Inconsistent settings root-transition evidence.");
|
|
276
392
|
if (currentRoot !== transition.beforeRoot && currentRoot !== sessionRoot)
|
|
277
393
|
throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
|
|
@@ -283,13 +399,13 @@ var JOURNAL_VERSION = 4;
|
|
|
283
399
|
async function applySnapshotTransaction(plan, options = {}) {
|
|
284
400
|
options.validateMutation?.();
|
|
285
401
|
await recoverPendingSnapshotTransactions(options);
|
|
286
|
-
const coalesced = await coalesceCaseReplacements(
|
|
402
|
+
const coalesced = await coalesceCaseReplacements(path6.resolve(agentDir()), plan);
|
|
287
403
|
options.validateMutation?.();
|
|
288
404
|
options.signal?.throwIfAborted();
|
|
289
405
|
const targets = [.../* @__PURE__ */ new Set([...coalesced.plan.deletes, ...coalesced.plan.writes.map((item) => item.target)])].sort();
|
|
290
406
|
if (targets.length > 16384)
|
|
291
407
|
throw new Error("Snapshot transaction exceeds its target bound; review a smaller transfer.");
|
|
292
|
-
return withTargetQueues(targets, async () => {
|
|
408
|
+
return withTargetQueues(targets, options, async (heldKeys) => {
|
|
293
409
|
options.signal?.throwIfAborted();
|
|
294
410
|
if (targets.some(
|
|
295
411
|
(target) => options.protectedTargets?.some(
|
|
@@ -313,7 +429,7 @@ async function applySnapshotTransaction(plan, options = {}) {
|
|
|
313
429
|
for (const [target, expected] of options.expectedPreimages ?? []) {
|
|
314
430
|
if (originalTargets.has(target)) continue;
|
|
315
431
|
const variants = coalesced.plan.deletes.filter(
|
|
316
|
-
(candidate) => candidate !== target &&
|
|
432
|
+
(candidate) => candidate !== target && path6.dirname(candidate) === path6.dirname(target) && mergePathIdentity(candidate) === mergePathIdentity(target)
|
|
317
433
|
);
|
|
318
434
|
if (variants.length > 1) throw new Error("Ambiguous reviewed file preimage spellings; review required.");
|
|
319
435
|
const original = variants[0];
|
|
@@ -345,79 +461,87 @@ async function applySnapshotTransaction(plan, options = {}) {
|
|
|
345
461
|
...entry.afterTarget ? [[entry.afterTarget, entry]] : []
|
|
346
462
|
])
|
|
347
463
|
);
|
|
464
|
+
const changesQueueKey = transaction.journal.entries.some(
|
|
465
|
+
(entry) => !heldKeys.has(entry.target) || entry.afterTarget && !heldKeys.has(entry.afterTarget)
|
|
466
|
+
);
|
|
348
467
|
try {
|
|
349
468
|
for (const target of transaction.deletes.sort((a, b) => a.length - b.length)) {
|
|
350
469
|
options.signal?.throwIfAborted();
|
|
351
470
|
const entry = entriesByTarget.get(target);
|
|
352
471
|
if (!entry) throw new Error("Unowned transaction target.");
|
|
353
472
|
await removeOwnedTarget(transaction.directory, entry, transaction.journal, options);
|
|
354
|
-
await syncDirectory(
|
|
473
|
+
await syncDirectory(path6.dirname(target));
|
|
355
474
|
}
|
|
356
|
-
await
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
options.validateMutation?.();
|
|
376
|
-
options.signal?.throwIfAborted();
|
|
377
|
-
const temp = path5.join(path5.dirname(item.target), `.pi-sync.json.${randomUUID()}.apply`);
|
|
378
|
-
try {
|
|
379
|
-
let mode = 384;
|
|
380
|
-
try {
|
|
381
|
-
const stat = await fs3.lstat(item.target);
|
|
382
|
-
if (stat.isFile()) mode = stat.mode & 511;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
if (error.code !== "ENOENT") throw error;
|
|
385
|
-
}
|
|
386
|
-
const handle = await fs3.open(temp, "wx", mode);
|
|
387
|
-
try {
|
|
388
|
-
await handle.writeFile(item.content);
|
|
389
|
-
await handle.sync();
|
|
390
|
-
} finally {
|
|
391
|
-
await handle.close();
|
|
392
|
-
}
|
|
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);
|
|
393
494
|
await verifyTarget(transaction.directory, entry, transaction.journal, deletedByThisCall);
|
|
495
|
+
options.validateMutation?.();
|
|
394
496
|
options.signal?.throwIfAborted();
|
|
497
|
+
await fs4.mkdir(path6.dirname(item.target), { recursive: true });
|
|
395
498
|
options.validateMutation?.();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
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();
|
|
401
534
|
}
|
|
402
|
-
|
|
403
|
-
options.signal?.throwIfAborted();
|
|
404
|
-
await syncMutationParents(
|
|
405
|
-
targets,
|
|
406
|
-
[transaction.journal.root, ...transaction.journal.sessionRoot ? [transaction.journal.sessionRoot] : []],
|
|
407
|
-
options
|
|
408
|
-
);
|
|
409
|
-
options.validateMutation?.();
|
|
410
|
-
options.signal?.throwIfAborted();
|
|
411
|
-
});
|
|
535
|
+
);
|
|
412
536
|
} catch (error) {
|
|
413
537
|
if (options.signal?.aborted)
|
|
414
538
|
throw new Error("Snapshot apply cancelled; guarded transaction evidence retained for review.");
|
|
415
|
-
if (
|
|
416
|
-
throw new Error("
|
|
539
|
+
if (changesQueueKey)
|
|
540
|
+
throw new Error("Snapshot apply failed; guarded transaction evidence retained for review.", {
|
|
417
541
|
cause: error
|
|
418
542
|
});
|
|
419
543
|
try {
|
|
420
|
-
await restoreTransaction(transaction.directory, transaction.journal, options);
|
|
544
|
+
await restoreTransaction(transaction.directory, transaction.journal, options, heldKeys);
|
|
421
545
|
} catch (recoveryError) {
|
|
422
546
|
throw new AggregateError(
|
|
423
547
|
[error, recoveryError],
|
|
@@ -444,13 +568,13 @@ async function recoverSnapshotTransactionsOnStartup(options = {}) {
|
|
|
444
568
|
async function recoverPendingSnapshotTransactions(options = {}) {
|
|
445
569
|
for (const entry of (await pendingTransactionEntries()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
446
570
|
if (!entry.isDirectory()) continue;
|
|
447
|
-
const directory =
|
|
571
|
+
const directory = path6.join(transactionRoot(), entry.name);
|
|
448
572
|
let journal;
|
|
449
573
|
try {
|
|
450
|
-
const file =
|
|
451
|
-
const stat = await
|
|
574
|
+
const file = path6.join(directory, "journal.json");
|
|
575
|
+
const stat = await fs4.lstat(file);
|
|
452
576
|
if (!stat.isFile() || stat.size > 32 * 1024 * 1024) throw new Error("Invalid journal file.");
|
|
453
|
-
journal = JSON.parse(await
|
|
577
|
+
journal = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
454
578
|
} catch (error) {
|
|
455
579
|
if (error.code === "ENOENT") continue;
|
|
456
580
|
throw new Error("Cannot recover malformed pi-sync transaction; preserve private evidence for review.");
|
|
@@ -469,7 +593,7 @@ async function recoverPendingSnapshotTransactions(options = {}) {
|
|
|
469
593
|
journal.sessionRoot,
|
|
470
594
|
journal.sessionRootTransition,
|
|
471
595
|
journal.entries,
|
|
472
|
-
|
|
596
|
+
path6.resolve(sessionStorageRoot(journal.root, sessionDir)),
|
|
473
597
|
options
|
|
474
598
|
);
|
|
475
599
|
options.validateMutation?.();
|
|
@@ -482,26 +606,19 @@ async function recoverPendingSnapshotTransactions(options = {}) {
|
|
|
482
606
|
}
|
|
483
607
|
await withTargetQueues(
|
|
484
608
|
journal.entries.map((entry2) => entry2.target).sort(),
|
|
485
|
-
|
|
609
|
+
options,
|
|
610
|
+
(heldKeys) => restoreTransaction(directory, journal, { ...options, sessionDir }, heldKeys)
|
|
486
611
|
);
|
|
487
612
|
}
|
|
488
613
|
}
|
|
489
|
-
async function pendingTransactionEntries() {
|
|
490
|
-
try {
|
|
491
|
-
return await fs3.readdir(transactionRoot(), { withFileTypes: true });
|
|
492
|
-
} catch (error) {
|
|
493
|
-
if (error.code === "ENOENT") return [];
|
|
494
|
-
throw error;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
614
|
async function prepareTransaction(plan, targets, options, replacements = /* @__PURE__ */ new Map()) {
|
|
498
615
|
const { sessionDir } = options;
|
|
499
|
-
const root =
|
|
500
|
-
const sessionRoot = sessionDir ?
|
|
616
|
+
const root = path6.resolve(agentDir());
|
|
617
|
+
const sessionRoot = sessionDir ? path6.resolve(sessionStorageRoot(root, sessionDir)) : void 0;
|
|
501
618
|
const indexed = indexTransactionPlan(plan, targets);
|
|
502
|
-
const directory =
|
|
503
|
-
const backupDirectory =
|
|
504
|
-
await
|
|
619
|
+
const directory = path6.join(transactionRoot(), randomUUID());
|
|
620
|
+
const backupDirectory = path6.join(directory, "before");
|
|
621
|
+
await fs4.mkdir(backupDirectory, { recursive: true, mode: 448 });
|
|
505
622
|
const entries = [];
|
|
506
623
|
for (const [index, target] of targets.entries()) {
|
|
507
624
|
options.signal?.throwIfAborted();
|
|
@@ -510,15 +627,15 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
510
627
|
await assertSafeParents(root, sessionRoot, target);
|
|
511
628
|
const entry = { target, backupName: `${index}`, kind: "missing" };
|
|
512
629
|
if (replacements.has(target)) entry.afterTarget = replacements.get(target);
|
|
513
|
-
const backup =
|
|
630
|
+
const backup = path6.join(backupDirectory, entry.backupName);
|
|
514
631
|
try {
|
|
515
|
-
const stat = await
|
|
632
|
+
const stat = await fs4.lstat(target);
|
|
516
633
|
if (stat.isSymbolicLink()) {
|
|
517
634
|
entry.kind = "symlink";
|
|
518
|
-
entry.linkTarget = await
|
|
635
|
+
entry.linkTarget = await fs4.readlink(target);
|
|
519
636
|
} else if (stat.isDirectory()) {
|
|
520
637
|
entry.kind = "directory";
|
|
521
|
-
await
|
|
638
|
+
await fs4.cp(target, backup, {
|
|
522
639
|
recursive: true,
|
|
523
640
|
dereference: false,
|
|
524
641
|
verbatimSymlinks: true,
|
|
@@ -531,8 +648,8 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
531
648
|
});
|
|
532
649
|
} else if (stat.isFile()) {
|
|
533
650
|
entry.kind = "file";
|
|
534
|
-
await
|
|
535
|
-
await
|
|
651
|
+
await fs4.copyFile(target, backup);
|
|
652
|
+
await fs4.chmod(backup, stat.mode);
|
|
536
653
|
} else throw new Error("Unsupported existing snapshot target.");
|
|
537
654
|
} catch (error) {
|
|
538
655
|
const code = error.code;
|
|
@@ -555,7 +672,7 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
555
672
|
entries.push(entry);
|
|
556
673
|
}
|
|
557
674
|
await syncDirectory(backupDirectory);
|
|
558
|
-
const effectiveRoot = sessionRoot ??
|
|
675
|
+
const effectiveRoot = sessionRoot ?? path6.join(root, "sessions");
|
|
559
676
|
const sessionRootTransition = await prepareSessionRootTransition(
|
|
560
677
|
directory,
|
|
561
678
|
root,
|
|
@@ -573,13 +690,32 @@ async function prepareTransaction(plan, targets, options, replacements = /* @__P
|
|
|
573
690
|
};
|
|
574
691
|
options.signal?.throwIfAborted();
|
|
575
692
|
options.validateMutation?.();
|
|
576
|
-
await writeJson(
|
|
693
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
577
694
|
await syncDirectory(transactionRoot());
|
|
578
695
|
await syncDirectory(stateDir());
|
|
579
|
-
await syncDirectory(
|
|
696
|
+
await syncDirectory(path6.dirname(stateDir()));
|
|
580
697
|
return { directory, journal, deletes: indexed.deletes, deletedWrites: indexed.deletedWrites };
|
|
581
698
|
}
|
|
582
|
-
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) {
|
|
583
719
|
validateJournal(directory, journal, options.sessionDir);
|
|
584
720
|
options.validateMutation?.();
|
|
585
721
|
options.signal?.throwIfAborted();
|
|
@@ -599,7 +735,7 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
599
735
|
await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
|
|
600
736
|
options.validateMutation?.();
|
|
601
737
|
options.signal?.throwIfAborted();
|
|
602
|
-
await
|
|
738
|
+
await fs4.rm(recoveryStagePath(directory, entry), { recursive: true, force: true });
|
|
603
739
|
options.validateMutation?.();
|
|
604
740
|
options.signal?.throwIfAborted();
|
|
605
741
|
}
|
|
@@ -613,9 +749,9 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
613
749
|
if (entry.kind === "file") {
|
|
614
750
|
const temporary2 = await prepareRecoveryStage(directory, journal, entry, options);
|
|
615
751
|
try {
|
|
616
|
-
await
|
|
617
|
-
await
|
|
618
|
-
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");
|
|
619
755
|
try {
|
|
620
756
|
await handle.sync();
|
|
621
757
|
} finally {
|
|
@@ -630,8 +766,9 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
630
766
|
await removeOwnedTarget(directory, entry, journal, options);
|
|
631
767
|
} else if (verified.startsWith("directory:") && !await removeOwnedTarget(directory, entry, journal, options, before))
|
|
632
768
|
continue;
|
|
633
|
-
await
|
|
634
|
-
|
|
769
|
+
await withMissingTargetQueues(
|
|
770
|
+
entry.afterTarget && (needsCaseQueue || verified === "missing") ? [entry.target, entry.afterTarget] : verified.startsWith("directory:") ? [entry.target] : [],
|
|
771
|
+
heldKeys,
|
|
635
772
|
options,
|
|
636
773
|
async () => {
|
|
637
774
|
const atRename = await verifyTarget(directory, entry, journal);
|
|
@@ -643,12 +780,12 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
643
780
|
options.validateMutation?.();
|
|
644
781
|
options.signal?.throwIfAborted();
|
|
645
782
|
if (atInstall === before && await spellingMatches()) return;
|
|
646
|
-
await
|
|
647
|
-
await syncDirectory(
|
|
783
|
+
await fs4.rename(temporary2, entry.target);
|
|
784
|
+
await syncDirectory(path6.dirname(entry.target));
|
|
648
785
|
}
|
|
649
786
|
);
|
|
650
787
|
} finally {
|
|
651
|
-
await
|
|
788
|
+
await fs4.rm(temporary2, { force: true });
|
|
652
789
|
}
|
|
653
790
|
continue;
|
|
654
791
|
}
|
|
@@ -658,10 +795,10 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
658
795
|
}
|
|
659
796
|
const temporary = await prepareRecoveryStage(directory, journal, entry, options);
|
|
660
797
|
try {
|
|
661
|
-
await
|
|
798
|
+
await fs4.mkdir(path6.dirname(entry.target), { recursive: true });
|
|
662
799
|
await assertSafeParents(journal.root, journal.sessionRoot, entry.target);
|
|
663
800
|
if (entry.kind === "directory") {
|
|
664
|
-
await
|
|
801
|
+
await fs4.cp(path6.join(directory, "before", entry.backupName), temporary, {
|
|
665
802
|
recursive: true,
|
|
666
803
|
dereference: false,
|
|
667
804
|
verbatimSymlinks: true,
|
|
@@ -672,25 +809,27 @@ async function restoreTransaction(directory, journal, options) {
|
|
|
672
809
|
return true;
|
|
673
810
|
}
|
|
674
811
|
});
|
|
675
|
-
} else if (entry.linkTarget !== void 0) await
|
|
812
|
+
} else if (entry.linkTarget !== void 0) await fs4.symlink(entry.linkTarget, temporary);
|
|
676
813
|
if (await image(temporary, true) !== before)
|
|
677
814
|
throw new Error("Staged recovery image differs from the backup; evidence retained.");
|
|
678
815
|
options.validateMutation?.();
|
|
679
816
|
options.signal?.throwIfAborted();
|
|
680
817
|
if (!await removeOwnedTarget(directory, entry, journal, options, before)) continue;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
});
|
|
692
831
|
} finally {
|
|
693
|
-
await
|
|
832
|
+
await fs4.rm(temporary, { recursive: true, force: true });
|
|
694
833
|
}
|
|
695
834
|
}
|
|
696
835
|
await syncMutationParents(
|
|
@@ -735,13 +874,13 @@ async function removeOwnedTarget(directory, entry, journal, options, restoringBe
|
|
|
735
874
|
affected.replacementStarted = false;
|
|
736
875
|
}
|
|
737
876
|
}
|
|
738
|
-
await writeJson(
|
|
877
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
739
878
|
const verified = await verifyTarget(directory, entry, journal);
|
|
740
879
|
options.validateMutation?.();
|
|
741
880
|
options.signal?.throwIfAborted();
|
|
742
881
|
if (restoringBefore !== void 0 && verified === restoringBefore) return false;
|
|
743
|
-
await
|
|
744
|
-
await syncDirectory(
|
|
882
|
+
await fs4.rm(entry.target, { recursive: true, force: true });
|
|
883
|
+
await syncDirectory(path6.dirname(entry.target));
|
|
745
884
|
return true;
|
|
746
885
|
}
|
|
747
886
|
function upgradeRemovalEvidence(journal) {
|
|
@@ -759,7 +898,7 @@ async function retireRemovalIntent(directory, journal, entries, options) {
|
|
|
759
898
|
entry.removalPending = false;
|
|
760
899
|
entry.replacementStarted = true;
|
|
761
900
|
}
|
|
762
|
-
await writeJson(
|
|
901
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
763
902
|
options.validateMutation?.();
|
|
764
903
|
options.signal?.throwIfAborted();
|
|
765
904
|
}
|
|
@@ -774,8 +913,8 @@ async function retireRestorationIntent(directory, journal, entry, options) {
|
|
|
774
913
|
}
|
|
775
914
|
function recoveryStagePath(directory, entry) {
|
|
776
915
|
const token = createHash2("sha256").update(directory).update("\\0").update(entry.backupName).digest("hex");
|
|
777
|
-
return
|
|
778
|
-
|
|
916
|
+
return path6.join(
|
|
917
|
+
path6.dirname(entry.target),
|
|
779
918
|
`.pi-sync.json.${token}.${entry.kind === "file" ? "restore" : "restore-tree"}`
|
|
780
919
|
);
|
|
781
920
|
}
|
|
@@ -783,7 +922,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
783
922
|
const temporary = recoveryStagePath(directory, entry);
|
|
784
923
|
if (!entry.recoveryStaged) {
|
|
785
924
|
try {
|
|
786
|
-
await
|
|
925
|
+
await fs4.lstat(temporary);
|
|
787
926
|
throw new Error("Unowned recovery staging path; preserve evidence for review.");
|
|
788
927
|
} catch (error) {
|
|
789
928
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -791,7 +930,7 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
791
930
|
options.validateMutation?.();
|
|
792
931
|
options.signal?.throwIfAborted();
|
|
793
932
|
entry.recoveryStaged = true;
|
|
794
|
-
await writeJson(
|
|
933
|
+
await writeJson(path6.join(directory, "journal.json"), journal, { maxBytes: 32 * 1024 * 1024 });
|
|
795
934
|
options.validateMutation?.();
|
|
796
935
|
options.signal?.throwIfAborted();
|
|
797
936
|
}
|
|
@@ -800,24 +939,24 @@ async function prepareRecoveryStage(directory, journal, entry, options) {
|
|
|
800
939
|
async function beforeImage(directory, entry) {
|
|
801
940
|
if (entry.kind === "missing") return "missing";
|
|
802
941
|
if (entry.kind === "symlink") return `symlink:${entry.linkTarget}`;
|
|
803
|
-
const value = await image(
|
|
942
|
+
const value = await image(path6.join(directory, "before", entry.backupName));
|
|
804
943
|
if (value === "missing") throw new Error("Transaction backup is missing; preserve evidence for review.");
|
|
805
944
|
return value;
|
|
806
945
|
}
|
|
807
946
|
async function image(target, durable = false) {
|
|
808
947
|
let stat;
|
|
809
948
|
try {
|
|
810
|
-
stat = await
|
|
949
|
+
stat = await fs4.lstat(target);
|
|
811
950
|
} catch (error) {
|
|
812
951
|
const code = error.code;
|
|
813
952
|
if (code === "ENOENT" || code === "ENOTDIR") return "missing";
|
|
814
953
|
throw error;
|
|
815
954
|
}
|
|
816
|
-
if (stat.isSymbolicLink()) return `symlink:${await
|
|
955
|
+
if (stat.isSymbolicLink()) return `symlink:${await fs4.readlink(target)}`;
|
|
817
956
|
if (stat.isFile()) {
|
|
818
|
-
const content = await
|
|
957
|
+
const content = await fs4.readFile(target);
|
|
819
958
|
if (durable) {
|
|
820
|
-
const handle = await
|
|
959
|
+
const handle = await fs4.open(target, process.platform === "win32" ? "r+" : "r");
|
|
821
960
|
try {
|
|
822
961
|
await handle.sync();
|
|
823
962
|
} finally {
|
|
@@ -828,8 +967,8 @@ async function image(target, durable = false) {
|
|
|
828
967
|
}
|
|
829
968
|
if (!stat.isDirectory()) throw new Error("Unsupported transaction image.");
|
|
830
969
|
const entries = [];
|
|
831
|
-
for (const name of (await
|
|
832
|
-
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)]);
|
|
833
972
|
if (durable) await syncDirectory(target);
|
|
834
973
|
return `directory:${createHash2("sha256").update(JSON.stringify(entries)).digest("hex")}`;
|
|
835
974
|
}
|
|
@@ -838,22 +977,22 @@ async function ownedPostTree(target, files, requireComplete) {
|
|
|
838
977
|
const expected = new Map(files.map((file) => [mergePathIdentity(file.relative), file.image]));
|
|
839
978
|
const prefixes = /* @__PURE__ */ new Set();
|
|
840
979
|
for (const relative of expected.keys()) {
|
|
841
|
-
for (let parent =
|
|
980
|
+
for (let parent = path6.dirname(relative); parent !== "." && parent !== path6.dirname(parent); parent = path6.dirname(parent))
|
|
842
981
|
prefixes.add(parent);
|
|
843
982
|
}
|
|
844
983
|
const seen = /* @__PURE__ */ new Set();
|
|
845
984
|
async function visit(directory) {
|
|
846
985
|
let stat;
|
|
847
986
|
try {
|
|
848
|
-
stat = await
|
|
987
|
+
stat = await fs4.lstat(directory);
|
|
849
988
|
} catch {
|
|
850
989
|
return false;
|
|
851
990
|
}
|
|
852
991
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
853
|
-
for (const name of await
|
|
854
|
-
const child =
|
|
855
|
-
const relative = mergePathIdentity(
|
|
856
|
-
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);
|
|
857
996
|
if (stat2.isDirectory() && !stat2.isSymbolicLink()) {
|
|
858
997
|
if (!prefixes.has(relative) || !await visit(child)) return false;
|
|
859
998
|
} else {
|
|
@@ -870,23 +1009,23 @@ function validateJournal(directory, journal, sessionDir) {
|
|
|
870
1009
|
throw new Error("Unsupported pi-sync transaction journal; preserve evidence for review.");
|
|
871
1010
|
if (journal.completed !== void 0 && (![6, 7].includes(journal.version) || journal.completed !== true))
|
|
872
1011
|
throw new Error("Invalid transaction completion evidence.");
|
|
873
|
-
const root =
|
|
874
|
-
if (typeof journal.root !== "string" ||
|
|
1012
|
+
const root = path6.resolve(agentDir());
|
|
1013
|
+
if (typeof journal.root !== "string" || path6.resolve(journal.root) !== root)
|
|
875
1014
|
throw new Error("Transaction root no longer matches the Pi agent directory.");
|
|
876
1015
|
if (journal.sessionRootTransition !== void 0) {
|
|
877
1016
|
if (journal.version !== 7 || typeof journal.sessionRoot !== "string")
|
|
878
1017
|
throw new Error("Invalid session root-transition version.");
|
|
879
1018
|
validateSessionRootTransition(journal.sessionRootTransition);
|
|
880
1019
|
} else if (journal.version === 7) throw new Error("Missing session root-transition evidence.");
|
|
881
|
-
const trustedSessionRoot = sessionDir ?
|
|
882
|
-
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))
|
|
883
1022
|
throw new Error("Transaction session root is not owned by this context; preserve evidence for review.");
|
|
884
1023
|
for (const entry of journal.entries) {
|
|
885
|
-
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))
|
|
886
1025
|
throw new Error("Invalid pi-sync transaction entry.");
|
|
887
|
-
assertWithinRoot(directory,
|
|
1026
|
+
assertWithinRoot(directory, path6.join(directory, "before", entry.backupName));
|
|
888
1027
|
assertAllowedTarget(root, journal.sessionRoot, entry.target);
|
|
889
|
-
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"))
|
|
890
1029
|
throw new Error("Invalid transaction case replacement evidence.");
|
|
891
1030
|
if (entry.afterTarget) assertAllowedTarget(root, journal.sessionRoot, entry.afterTarget);
|
|
892
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))))
|
|
@@ -900,29 +1039,29 @@ function validateJournal(directory, journal, sessionDir) {
|
|
|
900
1039
|
if (entry.removalPending && entry.replacementStarted)
|
|
901
1040
|
throw new Error("Conflicting transaction replacement evidence.");
|
|
902
1041
|
for (const file of entry.postFiles ?? []) {
|
|
903
|
-
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)))
|
|
904
1043
|
throw new Error("Invalid transaction subtree evidence.");
|
|
905
1044
|
}
|
|
906
1045
|
}
|
|
907
1046
|
}
|
|
908
1047
|
function transactionHasSessionTargets(journal) {
|
|
909
|
-
const root =
|
|
1048
|
+
const root = path6.resolve(agentDir());
|
|
910
1049
|
return Array.isArray(journal.entries) && journal.entries.some(
|
|
911
|
-
(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))
|
|
912
1051
|
);
|
|
913
1052
|
}
|
|
914
1053
|
function isStrictlyInside(root, target) {
|
|
915
|
-
return
|
|
1054
|
+
return path6.relative(path6.resolve(root), path6.resolve(target)) !== "" && isPathInside(root, target);
|
|
916
1055
|
}
|
|
917
1056
|
function assertAllowedTarget(root, sessionRoot, target) {
|
|
918
|
-
const resolved =
|
|
1057
|
+
const resolved = path6.resolve(target);
|
|
919
1058
|
if (isStrictlyInside(root, resolved)) {
|
|
920
|
-
if (isDeniedPath(
|
|
1059
|
+
if (isDeniedPath(path6.relative(root, resolved))) throw new Error("Transaction targets denied private storage.");
|
|
921
1060
|
assertWithinRoot(root, resolved);
|
|
922
1061
|
return;
|
|
923
1062
|
}
|
|
924
1063
|
if (sessionRoot && isStrictlyInside(sessionRoot, resolved)) {
|
|
925
|
-
if (isDeniedPath(`sessions/${
|
|
1064
|
+
if (isDeniedPath(`sessions/${path6.relative(sessionRoot, resolved)}`))
|
|
926
1065
|
throw new Error("Transaction targets denied session storage.");
|
|
927
1066
|
assertWithinRoot(sessionRoot, resolved);
|
|
928
1067
|
return;
|
|
@@ -932,10 +1071,10 @@ function assertAllowedTarget(root, sessionRoot, target) {
|
|
|
932
1071
|
async function assertSafeParents(root, sessionRoot, target) {
|
|
933
1072
|
assertAllowedTarget(root, sessionRoot, target);
|
|
934
1073
|
const boundary = isPathInside(root, target) ? root : sessionRoot;
|
|
935
|
-
for (let parent =
|
|
936
|
-
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.");
|
|
937
1076
|
try {
|
|
938
|
-
const stat = await
|
|
1077
|
+
const stat = await fs4.lstat(parent);
|
|
939
1078
|
if (!stat.isDirectory() && !stat.isFile() || stat.isSymbolicLink())
|
|
940
1079
|
throw new Error("Unsafe transaction parent.");
|
|
941
1080
|
} catch (error) {
|
|
@@ -944,50 +1083,9 @@ async function assertSafeParents(root, sessionRoot, target) {
|
|
|
944
1083
|
}
|
|
945
1084
|
}
|
|
946
1085
|
}
|
|
947
|
-
async function withTargetQueues(targets, action) {
|
|
948
|
-
const keys = /* @__PURE__ */ new Set();
|
|
949
|
-
for (const target of targets) {
|
|
950
|
-
let key = path5.resolve(target);
|
|
951
|
-
try {
|
|
952
|
-
key = await fs3.realpath(target);
|
|
953
|
-
} catch (error) {
|
|
954
|
-
const code = error.code;
|
|
955
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
956
|
-
}
|
|
957
|
-
if (keys.has(key)) throw new Error("Transaction targets alias the same file.");
|
|
958
|
-
keys.add(key);
|
|
959
|
-
}
|
|
960
|
-
async function acquire(index) {
|
|
961
|
-
const target = targets[index];
|
|
962
|
-
return target ? withFileMutationQueue(target, () => acquire(index + 1)) : action();
|
|
963
|
-
}
|
|
964
|
-
return acquire(0);
|
|
965
|
-
}
|
|
966
|
-
async function withCaseDestinationQueues(spellings, options, action) {
|
|
967
|
-
if (!spellings.length) return action();
|
|
968
|
-
const requireAbsent = async () => {
|
|
969
|
-
for (const [before, after] of spellings) {
|
|
970
|
-
options.validateMutation?.();
|
|
971
|
-
options.signal?.throwIfAborted();
|
|
972
|
-
const spelling = await caseReplacementSpelling(before, after);
|
|
973
|
-
options.validateMutation?.();
|
|
974
|
-
options.signal?.throwIfAborted();
|
|
975
|
-
if (spelling !== void 0)
|
|
976
|
-
throw new Error("Case replacement changed before destination queue reservation; evidence retained.");
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
await requireAbsent();
|
|
980
|
-
return withTargetQueues(
|
|
981
|
-
spellings.map(([, after]) => after),
|
|
982
|
-
async () => {
|
|
983
|
-
await requireAbsent();
|
|
984
|
-
return action();
|
|
985
|
-
}
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
1086
|
async function completeTransaction(directory, journal) {
|
|
989
1087
|
await writeJson(
|
|
990
|
-
|
|
1088
|
+
path6.join(directory, "journal.json"),
|
|
991
1089
|
{ ...journal, version: journal.sessionRootTransition ? 7 : 6, completed: true },
|
|
992
1090
|
{
|
|
993
1091
|
maxBytes: 32 * 1024 * 1024
|
|
@@ -996,18 +1094,16 @@ async function completeTransaction(directory, journal) {
|
|
|
996
1094
|
await removeTransaction(directory);
|
|
997
1095
|
}
|
|
998
1096
|
async function removeTransaction(directory) {
|
|
999
|
-
await
|
|
1097
|
+
await fs4.rm(directory, { recursive: true, force: true });
|
|
1000
1098
|
await syncDirectory(transactionRoot()).catch(() => {
|
|
1001
1099
|
});
|
|
1002
1100
|
}
|
|
1003
|
-
function transactionRoot() {
|
|
1004
|
-
return path5.join(stateDir(), "transactions");
|
|
1005
|
-
}
|
|
1006
1101
|
|
|
1007
1102
|
export {
|
|
1008
1103
|
syncMutationParents,
|
|
1104
|
+
assertCanonicalMutationTargets,
|
|
1009
1105
|
applySnapshotTransaction,
|
|
1010
1106
|
recoverSnapshotTransactionsOnStartup,
|
|
1011
1107
|
recoverPendingSnapshotTransactions
|
|
1012
1108
|
};
|
|
1013
|
-
//# sourceMappingURL=chunk-
|
|
1109
|
+
//# sourceMappingURL=chunk-7A35ZAVO.ts.map
|