@narumitw/pi-sync 0.51.1 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/chunks/{chunk-7KHKWL3Q.ts → chunk-2CBF6SMT.ts} +8 -7
- package/dist/chunks/chunk-2CBF6SMT.ts.map +7 -0
- package/dist/chunks/{chunk-YFADF5ZQ.ts → chunk-2DI6PDZV.ts} +5 -1
- package/dist/chunks/chunk-2DI6PDZV.ts.map +7 -0
- package/dist/chunks/chunk-2GUXI2XT.ts +1702 -0
- package/dist/chunks/chunk-2GUXI2XT.ts.map +7 -0
- package/dist/chunks/{chunk-4MPIH46N.ts → chunk-3B4Z2SKV.ts} +40 -6
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +7 -0
- package/dist/chunks/{chunk-YPXGU5OR.ts → chunk-4QVPTIZB.ts} +16 -6
- package/dist/chunks/{chunk-YPXGU5OR.ts.map → chunk-4QVPTIZB.ts.map} +2 -2
- package/dist/chunks/{chunk-UA7RXOIT.ts → chunk-4SBTAVFL.ts} +6 -6
- package/dist/chunks/{chunk-6546TS54.ts → chunk-5RZPINUQ.ts} +7 -4
- package/dist/chunks/{chunk-6546TS54.ts.map → chunk-5RZPINUQ.ts.map} +2 -2
- package/dist/chunks/chunk-76NB4R7U.ts +107 -0
- package/dist/chunks/chunk-76NB4R7U.ts.map +7 -0
- package/dist/chunks/{chunk-DM4X4FIA.ts → chunk-7D6RDCVC.ts} +8 -10
- package/dist/chunks/{chunk-DM4X4FIA.ts.map → chunk-7D6RDCVC.ts.map} +2 -2
- package/dist/chunks/chunk-A6HU4ERM.ts +124 -0
- package/dist/chunks/chunk-A6HU4ERM.ts.map +7 -0
- package/dist/chunks/chunk-AMJRUXFT.ts +17 -0
- package/dist/chunks/chunk-AMJRUXFT.ts.map +7 -0
- package/dist/chunks/chunk-ANWNQIR7.ts +1013 -0
- package/dist/chunks/chunk-ANWNQIR7.ts.map +7 -0
- package/dist/chunks/{chunk-T56RNRZJ.ts → chunk-BDB4IDM3.ts} +19 -10
- package/dist/chunks/chunk-BDB4IDM3.ts.map +7 -0
- package/dist/chunks/{chunk-2RZSQWBH.ts → chunk-CHSIAC4N.ts} +5 -5
- package/dist/chunks/{chunk-2RZSQWBH.ts.map → chunk-CHSIAC4N.ts.map} +2 -2
- package/dist/chunks/chunk-HUZ5CR4W.ts +110 -0
- package/dist/chunks/chunk-HUZ5CR4W.ts.map +7 -0
- package/dist/chunks/{chunk-QLSN7AGX.ts → chunk-IJ6VV6FN.ts} +81 -19
- package/dist/chunks/chunk-IJ6VV6FN.ts.map +7 -0
- package/dist/chunks/chunk-ORQRHEEO.ts +362 -0
- package/dist/chunks/chunk-ORQRHEEO.ts.map +7 -0
- package/dist/chunks/chunk-QE7NZZWT.ts +380 -0
- package/dist/chunks/chunk-QE7NZZWT.ts.map +7 -0
- package/dist/chunks/{chunk-HGJH56KR.ts → chunk-QHSQCK7L.ts} +4 -4
- package/dist/chunks/chunk-QOFMXPM5.ts +735 -0
- package/dist/chunks/chunk-QOFMXPM5.ts.map +7 -0
- package/dist/chunks/{chunk-DZUSTGQV.ts → chunk-UQ3ALBIB.ts} +4 -4
- package/dist/chunks/{chunk-Q32CQWPM.ts → chunk-V6VZ5TLG.ts} +13 -6
- package/dist/chunks/chunk-V6VZ5TLG.ts.map +7 -0
- package/dist/chunks/{chunk-E42GXD6D.ts → chunk-WKPX2SIC.ts} +4 -4
- package/dist/chunks/chunk-ZW5ZLGBI.ts +154 -0
- package/dist/chunks/chunk-ZW5ZLGBI.ts.map +7 -0
- package/dist/chunks/{chunk-6NT3YFGY.ts → chunk-ZX2Q6THB.ts} +8 -3
- package/dist/chunks/{chunk-6NT3YFGY.ts.map → chunk-ZX2Q6THB.ts.map} +2 -2
- package/dist/chunks/conflict-artifacts-BB2FZMIE.ts +27 -0
- package/dist/chunks/conflict-review-T3PB7ZSD.ts +175 -0
- package/dist/chunks/conflict-review-T3PB7ZSD.ts.map +7 -0
- package/dist/chunks/{file-selection-RAVHRYFG.ts → file-selection-WZX7HAY2.ts} +8 -7
- package/dist/chunks/{file-selection-RAVHRYFG.ts.map → file-selection-WZX7HAY2.ts.map} +1 -1
- package/dist/chunks/{git-backend-IAANDL3K.ts → git-backend-HLGKUQJT.ts} +19 -7
- package/dist/chunks/git-backend-HLGKUQJT.ts.map +7 -0
- package/dist/chunks/local-fields-6DGYOOIK.ts +22 -0
- package/dist/chunks/manager-result-dispatcher-VIDT3QBU.ts +23 -0
- package/dist/chunks/{manager-ui-763NEKJY.ts → manager-ui-HSMHS3OK.ts} +236 -57
- package/dist/chunks/manager-ui-HSMHS3OK.ts.map +7 -0
- package/dist/chunks/merge-baseline-store-EVQAAML6.ts +22 -0
- package/dist/chunks/remote-selection-ui-SMF5CLFO.ts +22 -0
- package/dist/chunks/{s3-backend-XGXUOPUN.ts → s3-backend-DJ2NT3SZ.ts} +16 -9
- package/dist/chunks/s3-backend-DJ2NT3SZ.ts.map +7 -0
- package/dist/chunks/{setup-switch-5G2VNJHL.ts → setup-switch-KTUWL6DE.ts} +9 -6
- package/dist/chunks/{setup-wizard-YIEWGX32.ts → setup-wizard-GSDPDFC4.ts} +10 -7
- package/dist/chunks/{setup-wizard-YIEWGX32.ts.map → setup-wizard-GSDPDFC4.ts.map} +1 -1
- package/dist/chunks/{snapshot-JL5DFSXI.ts → snapshot-THS5EY2L.ts} +6 -9
- package/dist/chunks/snapshot-THS5EY2L.ts.map +7 -0
- package/dist/chunks/{sync-inspection-P3ENJLNF.ts → sync-inspection-VBJF2FWC.ts} +10 -10
- package/dist/chunks/sync-inspection-VBJF2FWC.ts.map +7 -0
- package/dist/chunks/{sync-operations-AR76EB6C.ts → sync-operations-27VQUDH2.ts} +309 -508
- package/dist/chunks/sync-operations-27VQUDH2.ts.map +7 -0
- package/dist/chunks/{sync-state-VHJUZ4YG.ts → sync-state-CFL4RRRB.ts} +6 -6
- package/dist/chunks/sync-state-CFL4RRRB.ts.map +7 -0
- package/dist/chunks/{webdav-backend-3EGUONAT.ts → webdav-backend-ETQGSI6I.ts} +13 -7
- package/dist/chunks/webdav-backend-ETQGSI6I.ts.map +7 -0
- package/dist/index.ts +142 -43
- package/dist/index.ts.map +2 -2
- package/docs/content-merge-audit.md +156 -0
- package/docs/merge-implementation-audit.md +312 -0
- package/docs/settings-merge-audit.md +153 -0
- package/docs/settings.md +36 -0
- package/package.json +5 -5
- package/src/backends/git/git-backend.ts +2 -0
- package/src/backends/git/git-storage.ts +8 -3
- package/src/backends/s3/s3-backend.ts +2 -1
- package/src/backends/webdav/webdav-backend.ts +2 -1
- package/src/commands/command-execution.ts +10 -0
- package/src/commands/command.ts +4 -0
- package/src/settings/config.ts +12 -0
- package/src/settings/settings-management.ts +2 -2
- package/src/settings/settings-types.ts +14 -1
- package/src/settings/settings-validation.ts +24 -3
- package/src/snapshot/session-paths.ts +34 -7
- package/src/snapshot/snapshot-apply.ts +81 -32
- package/src/snapshot/snapshot-case-replacement.ts +80 -0
- package/src/snapshot/snapshot-codec.ts +6 -1
- package/src/snapshot/snapshot-root-transition.ts +154 -0
- package/src/snapshot/snapshot-transaction-plan.ts +72 -0
- package/src/snapshot/snapshot-transaction.ts +844 -102
- package/src/snapshot/snapshot-types.ts +5 -0
- package/src/state/json-file.ts +32 -5
- package/src/state/merge-baseline-store.ts +182 -0
- package/src/state/mutation-directory-sync.ts +34 -0
- package/src/state/state-types.ts +4 -0
- package/src/state/sync-state-store.ts +14 -1
- package/src/sync/automatic-sync.ts +11 -3
- package/src/sync/conflict-artifact-ownership.ts +63 -0
- package/src/sync/conflict-artifacts.ts +195 -0
- package/src/sync/conflict-groups.ts +68 -0
- package/src/sync/conflict-preview.ts +56 -0
- package/src/sync/conflict-resolution.ts +93 -0
- package/src/sync/conflict-retention.ts +72 -0
- package/src/sync/conflict-review.ts +95 -0
- package/src/sync/content-conflicts.ts +73 -0
- package/src/sync/field-migration.ts +39 -0
- package/src/sync/file-merge-planner.ts +143 -0
- package/src/sync/json-document.ts +184 -0
- package/src/sync/local-fields.ts +134 -0
- package/src/sync/merge-apply.ts +380 -0
- package/src/sync/merge-journal.ts +219 -0
- package/src/sync/merged-sync.ts +723 -0
- package/src/sync/partial-progress.ts +54 -0
- package/src/sync/remote-snapshot.ts +6 -0
- package/src/sync/session-merge.ts +194 -0
- package/src/sync/settings-conflicts.ts +52 -0
- package/src/sync/settings-merge.ts +57 -0
- package/src/sync/startup-check.ts +90 -5
- package/src/sync/sync-inspection.ts +1 -0
- package/src/sync/sync-local.ts +61 -0
- package/src/sync/sync-mutations.ts +256 -114
- package/src/sync/sync-queries.ts +6 -2
- package/src/sync/sync-state.ts +14 -5
- package/src/sync/text-merge.ts +133 -0
- package/src/sync-extension.ts +8 -0
- package/src/ui/manager-state.ts +12 -1
- package/src/ui/manager-ui.ts +10 -0
- package/src/ui/merge-review.ts +28 -0
- package/src/ui/remote-selection-ui.ts +1 -0
- package/src/ui/settings-ui.ts +183 -2
- package/src/ui/setup/setup-actions.ts +2 -2
- package/src/ui/setup/setup-switcher.ts +2 -1
- package/src/ui/storage-connections-ui.ts +11 -7
- package/src/ui/sync-attention.ts +3 -0
- package/src/ui/sync-format.ts +16 -12
- package/src/ui/sync-setups-ui.ts +1 -0
- package/src/ui/terminal-text.ts +9 -2
- package/dist/chunks/chunk-4AIHXRKE.ts +0 -197
- package/dist/chunks/chunk-4AIHXRKE.ts.map +0 -7
- package/dist/chunks/chunk-4MPIH46N.ts.map +0 -7
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +0 -7
- package/dist/chunks/chunk-EEJPYTWE.ts +0 -348
- package/dist/chunks/chunk-EEJPYTWE.ts.map +0 -7
- package/dist/chunks/chunk-IVXEDPAD.ts +0 -370
- package/dist/chunks/chunk-IVXEDPAD.ts.map +0 -7
- package/dist/chunks/chunk-MSSW6F3R.ts +0 -12
- package/dist/chunks/chunk-MSSW6F3R.ts.map +0 -7
- package/dist/chunks/chunk-OVDTF5CH.ts +0 -90
- package/dist/chunks/chunk-OVDTF5CH.ts.map +0 -7
- package/dist/chunks/chunk-Q32CQWPM.ts.map +0 -7
- package/dist/chunks/chunk-QLSN7AGX.ts.map +0 -7
- package/dist/chunks/chunk-SWUXA3MH.ts +0 -18
- package/dist/chunks/chunk-SWUXA3MH.ts.map +0 -7
- package/dist/chunks/chunk-T56RNRZJ.ts.map +0 -7
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +0 -7
- package/dist/chunks/git-backend-IAANDL3K.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +0 -23
- package/dist/chunks/manager-ui-763NEKJY.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +0 -22
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +0 -7
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +0 -7
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +0 -7
- /package/dist/chunks/{chunk-UA7RXOIT.ts.map → chunk-4SBTAVFL.ts.map} +0 -0
- /package/dist/chunks/{chunk-HGJH56KR.ts.map → chunk-QHSQCK7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-DZUSTGQV.ts.map → chunk-UQ3ALBIB.ts.map} +0 -0
- /package/dist/chunks/{chunk-E42GXD6D.ts.map → chunk-WKPX2SIC.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-OTUMQM26.ts.map → conflict-artifacts-BB2FZMIE.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-TZTJF32Y.ts.map → local-fields-6DGYOOIK.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-5G2VNJHL.ts.map → manager-result-dispatcher-VIDT3QBU.ts.map} +0 -0
- /package/dist/chunks/{snapshot-JL5DFSXI.ts.map → merge-baseline-store-EVQAAML6.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-P3ENJLNF.ts.map → remote-selection-ui-SMF5CLFO.ts.map} +0 -0
- /package/dist/chunks/{sync-state-VHJUZ4YG.ts.map → setup-switch-KTUWL6DE.ts.map} +0 -0
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
2
|
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
-
import {
|
|
4
|
-
formatRemoteSelectionStatus,
|
|
5
|
-
readRemoteSnapshot,
|
|
6
|
-
readSnapshotForHead,
|
|
7
|
-
requireCompatibleRemoteSelection
|
|
8
|
-
} from "./chunk-Q32CQWPM.ts";
|
|
9
3
|
import {
|
|
10
4
|
inspectSync
|
|
11
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7D6RDCVC.ts";
|
|
12
6
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
applySnapshot,
|
|
8
|
+
countPreservedRemoteFiles,
|
|
9
|
+
createSyncDecision,
|
|
10
|
+
formatDiff,
|
|
11
|
+
formatPullSummary,
|
|
12
|
+
formatPushSummary,
|
|
13
|
+
formatRollbackSummary,
|
|
14
|
+
formatSnapshotOnlyDiff,
|
|
15
|
+
mergeSync,
|
|
16
|
+
publicationCapabilityDescription
|
|
17
|
+
} from "./chunk-2GUXI2XT.ts";
|
|
18
|
+
import {
|
|
19
|
+
readMergeJournal,
|
|
20
|
+
requireNoMergeJournal,
|
|
21
|
+
retireMergeJournal
|
|
22
|
+
} from "./chunk-ORQRHEEO.ts";
|
|
15
23
|
import {
|
|
16
24
|
canPullRemoteSessionsOnFirstSync,
|
|
17
25
|
canPullRemoteSettingsOnFirstSync,
|
|
@@ -24,436 +32,112 @@ import {
|
|
|
24
32
|
snapshotHashesMatchState,
|
|
25
33
|
snapshotsMatch,
|
|
26
34
|
syncPolicyChanged
|
|
27
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-BDB4IDM3.ts";
|
|
28
36
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
formatRemoteSelectionStatus,
|
|
38
|
+
readRemoteSnapshot,
|
|
39
|
+
readSnapshotForHead,
|
|
40
|
+
requireCompatibleRemoteSelection
|
|
41
|
+
} from "./chunk-V6VZ5TLG.ts";
|
|
32
42
|
import {
|
|
43
|
+
createSyncBackend
|
|
44
|
+
} from "./chunk-QHSQCK7L.ts";
|
|
45
|
+
import "./chunk-ANWNQIR7.ts";
|
|
46
|
+
import {
|
|
47
|
+
backupLocal,
|
|
48
|
+
captureMutationOwner,
|
|
33
49
|
inspectLock,
|
|
34
50
|
isLockGuardHeld,
|
|
35
51
|
isStaleLock,
|
|
52
|
+
protectedSessionPaths,
|
|
36
53
|
withLock
|
|
37
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-IJ6VV6FN.ts";
|
|
55
|
+
import {
|
|
56
|
+
throwIfAborted
|
|
57
|
+
} from "./chunk-PWMWOJP4.ts";
|
|
38
58
|
import {
|
|
39
59
|
setSyncStatus
|
|
40
60
|
} from "./chunk-K3RIEL6J.ts";
|
|
41
61
|
import {
|
|
42
|
-
|
|
43
|
-
} from "./chunk-
|
|
62
|
+
safeTerminalText
|
|
63
|
+
} from "./chunk-AMJRUXFT.ts";
|
|
64
|
+
import {
|
|
65
|
+
pruneMergeBaselines
|
|
66
|
+
} from "./chunk-QE7NZZWT.ts";
|
|
67
|
+
import "./chunk-76NB4R7U.ts";
|
|
44
68
|
import {
|
|
45
69
|
readStateForConfig,
|
|
46
70
|
writeStateForConfig
|
|
47
|
-
} from "./chunk-
|
|
48
|
-
import
|
|
49
|
-
safeTerminalText
|
|
50
|
-
} from "./chunk-MSSW6F3R.ts";
|
|
71
|
+
} from "./chunk-4QVPTIZB.ts";
|
|
72
|
+
import "./chunk-BVVP33U5.ts";
|
|
51
73
|
import {
|
|
52
74
|
loadConfig,
|
|
53
|
-
|
|
75
|
+
syncCheckConfigFingerprint,
|
|
54
76
|
syncSessionsWarnings
|
|
55
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-3B4Z2SKV.ts";
|
|
56
78
|
import "./chunk-2JZFHNLM.ts";
|
|
57
|
-
import {
|
|
58
|
-
encodeSnapshot
|
|
59
|
-
} from "./chunk-6NT3YFGY.ts";
|
|
60
|
-
import {
|
|
61
|
-
stateDir
|
|
62
|
-
} from "./chunk-BVVP33U5.ts";
|
|
63
79
|
import "./chunk-T2SJD3N7.ts";
|
|
64
80
|
import {
|
|
65
81
|
expectedRemoteHead
|
|
66
82
|
} from "./chunk-UETBKTDI.ts";
|
|
83
|
+
import "./chunk-ZX2Q6THB.ts";
|
|
67
84
|
import {
|
|
68
|
-
SyncDecisionRequiredError,
|
|
69
85
|
errorMessage
|
|
70
86
|
} from "./chunk-7D5SF7P3.ts";
|
|
87
|
+
import {
|
|
88
|
+
overlayLocalFields,
|
|
89
|
+
portableSnapshot,
|
|
90
|
+
sameLocalFields,
|
|
91
|
+
validatePortableSnapshot
|
|
92
|
+
} from "./chunk-A6HU4ERM.ts";
|
|
71
93
|
import {
|
|
72
94
|
createSnapshot,
|
|
73
95
|
filterSnapshotForConfigPolicy,
|
|
74
|
-
|
|
75
|
-
isSessionPath,
|
|
96
|
+
inspectRemoteSelection,
|
|
76
97
|
mergeRemotePreservedFiles,
|
|
77
98
|
regenerateSnapshotIdentity,
|
|
78
99
|
scanSnapshot,
|
|
79
|
-
|
|
100
|
+
sessionDirForApply,
|
|
80
101
|
snapshotIncludesSessions,
|
|
81
|
-
|
|
102
|
+
snapshotOptionsForContext,
|
|
82
103
|
snapshotWithoutSessions
|
|
83
|
-
} from "./chunk-
|
|
84
|
-
import
|
|
85
|
-
|
|
86
|
-
} from "./chunk-SWUXA3MH.ts";
|
|
87
|
-
import {
|
|
88
|
-
agentDir,
|
|
89
|
-
sessionDirForApply,
|
|
90
|
-
sessionDirFromContext,
|
|
91
|
-
snapshotOptionsForContext
|
|
92
|
-
} from "./chunk-OVDTF5CH.ts";
|
|
93
|
-
import {
|
|
94
|
-
assertWithinRoot,
|
|
95
|
-
includeFromSelectionConfig,
|
|
96
|
-
inspectRemoteSelection,
|
|
97
|
-
isDeniedPath,
|
|
98
|
-
isPathInside,
|
|
99
|
-
parentPaths,
|
|
100
|
-
safeJoin,
|
|
101
|
-
toPosix
|
|
102
|
-
} from "./chunk-IVXEDPAD.ts";
|
|
103
|
-
|
|
104
|
-
// src/sync/sync-mutations.ts
|
|
105
|
-
import fs2 from "node:fs/promises";
|
|
106
|
-
import path2 from "node:path";
|
|
104
|
+
} from "./chunk-QOFMXPM5.ts";
|
|
105
|
+
import "./chunk-ZW5ZLGBI.ts";
|
|
106
|
+
import "./chunk-HUZ5CR4W.ts";
|
|
107
107
|
|
|
108
|
-
// src/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const plan = await addTopLevelCaseVariantDeletes(
|
|
128
|
-
root,
|
|
129
|
-
protectSnapshotApplyPlan(
|
|
130
|
-
root,
|
|
131
|
-
preflightSnapshotApply(root, snapshot, current, { sessionDir }),
|
|
132
|
-
protectedRelativePaths,
|
|
133
|
-
sessionDir
|
|
134
|
-
),
|
|
135
|
-
snapshot
|
|
136
|
-
);
|
|
137
|
-
await preflightSnapshotMutations(root, plan, sessionDir);
|
|
138
|
-
await applySnapshotTransaction(plan, { sessionDir });
|
|
139
|
-
return appliedFileHashMap(snapshot, current, protectedRelativePaths);
|
|
140
|
-
}
|
|
141
|
-
function preflightSnapshotApply(root, snapshot, current, options = {}) {
|
|
142
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
143
|
-
const remotePaths = /* @__PURE__ */ new Set();
|
|
144
|
-
const writes = [];
|
|
145
|
-
const deletes = [];
|
|
146
|
-
for (const file of snapshot.files) {
|
|
147
|
-
const normalized = toPosix(file.path);
|
|
148
|
-
if (!isSafeSnapshotPath(file.path)) {
|
|
149
|
-
throw new Error(`Unsafe path in snapshot: ${file.path}`);
|
|
150
|
-
}
|
|
151
|
-
if (isSessionPath(normalized) && !isSessionFilePath(normalized)) {
|
|
152
|
-
throw new Error(`Unsafe session path in snapshot: ${file.path}`);
|
|
153
|
-
}
|
|
154
|
-
if (seenPaths.has(normalized)) throw new Error(`Duplicate path in snapshot: ${normalized}`);
|
|
155
|
-
seenPaths.add(normalized);
|
|
156
|
-
remotePaths.add(normalized);
|
|
157
|
-
const target = snapshotTarget(root, normalized, options.sessionDir);
|
|
158
|
-
const content = decodeBase64Strict(file.contentBase64, normalized);
|
|
159
|
-
if (sha256(content) !== file.sha256) throw new Error(`Checksum mismatch in snapshot file: ${normalized}`);
|
|
160
|
-
writes.push({ target, content });
|
|
161
|
-
}
|
|
162
|
-
const deletePaths = /* @__PURE__ */ new Set();
|
|
163
|
-
for (const file of current.files) {
|
|
164
|
-
const normalized = toPosix(file.path);
|
|
165
|
-
if (!remotePaths.has(normalized)) {
|
|
166
|
-
deletePaths.add(snapshotTarget(root, normalized, options.sessionDir));
|
|
167
|
-
}
|
|
168
|
-
for (const remotePath of parentPaths(normalized)) {
|
|
169
|
-
if (remotePaths.has(remotePath)) {
|
|
170
|
-
deletePaths.add(snapshotTarget(root, remotePath, options.sessionDir));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
deletes.push(...deletePaths);
|
|
175
|
-
return { writes, deletes };
|
|
176
|
-
}
|
|
177
|
-
function protectSnapshotApplyPlan(root, plan, protectedRelativePaths, sessionDir) {
|
|
178
|
-
if (protectedRelativePaths.size === 0) return plan;
|
|
179
|
-
const protectedTargets = new Set(
|
|
180
|
-
[...protectedRelativePaths].map((relativePath) => snapshotTarget(root, relativePath, sessionDir))
|
|
108
|
+
// src/sync/field-migration.ts
|
|
109
|
+
async function confirmFieldMigration(ctx, config, state, remote, force, signal) {
|
|
110
|
+
if (remote?.localFields !== void 0 && config.localFields === void 0)
|
|
111
|
+
throw new Error(
|
|
112
|
+
"Portable snapshots require settings version 4 and explicit localFields rules on this machine; older policy must not replace local-only values."
|
|
113
|
+
);
|
|
114
|
+
const changed = Boolean(state.lastAppliedSnapshot) && !sameLocalFields(state.localFields, config.localFields) || remote !== void 0 && !sameLocalFields(remote.localFields, config.localFields);
|
|
115
|
+
if (!changed) return true;
|
|
116
|
+
if (!force)
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Local-field policy changed; review an explicit push --force or pull --force migration. Old remote history is not erased."
|
|
119
|
+
);
|
|
120
|
+
const validate = captureMutationOwner(ctx, signal);
|
|
121
|
+
if (!ctx.hasUI) throw new Error("Local-field migration requires observable TUI/RPC confirmation.");
|
|
122
|
+
validate();
|
|
123
|
+
const confirmed = await ctx.ui.confirm(
|
|
124
|
+
"Migrate portable settings policy?",
|
|
125
|
+
`New machine-local fields: ${(config.localFields ?? []).map(safeTerminalText).join(", ") || "none"}. Additions remove fields from future snapshots, not old history. Removals make local-only values portable again and a pull may replace them. The selected direction is authoritative; inspect /sync diff first. This confirmation is required even with --yes.`,
|
|
126
|
+
{ signal }
|
|
181
127
|
);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
deletes: plan.deletes.filter((target) => !protectedTargets.has(target))
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
async function addTopLevelCaseVariantDeletes(root, plan, snapshot) {
|
|
188
|
-
const topLevelPaths = /* @__PURE__ */ new Map();
|
|
189
|
-
for (const file of snapshot.files) {
|
|
190
|
-
const normalized = toPosix(file.path);
|
|
191
|
-
if (!normalized.includes("/") && isSafeSnapshotPath(file.path)) {
|
|
192
|
-
topLevelPaths.set(normalized.toLowerCase(), normalized);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (topLevelPaths.size === 0) return plan;
|
|
196
|
-
let entries;
|
|
197
|
-
try {
|
|
198
|
-
entries = await fs.readdir(root, { withFileTypes: true });
|
|
199
|
-
} catch (error) {
|
|
200
|
-
if (error.code === "ENOENT") return plan;
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
203
|
-
const deletes = new Set(plan.deletes);
|
|
204
|
-
for (const entry of entries) {
|
|
205
|
-
const canonicalPath = topLevelPaths.get(entry.name.toLowerCase());
|
|
206
|
-
if (canonicalPath && entry.name !== canonicalPath && (entry.isFile() || entry.isSymbolicLink())) {
|
|
207
|
-
deletes.add(safeJoin(root, entry.name));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return { ...plan, deletes: [...deletes] };
|
|
211
|
-
}
|
|
212
|
-
function appliedFileHashMap(snapshot, current, protectedRelativePaths) {
|
|
213
|
-
const hashes = fileHashMap2(snapshot);
|
|
214
|
-
if (protectedRelativePaths.size === 0) return hashes;
|
|
215
|
-
const currentHashes = fileHashMap2(current);
|
|
216
|
-
for (const relativePath of protectedRelativePaths) {
|
|
217
|
-
const normalized = toPosix(relativePath);
|
|
218
|
-
if (currentHashes[normalized]) {
|
|
219
|
-
hashes[normalized] = currentHashes[normalized];
|
|
220
|
-
} else {
|
|
221
|
-
delete hashes[normalized];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return hashes;
|
|
225
|
-
}
|
|
226
|
-
function isSafeSnapshotPath(relativePath) {
|
|
227
|
-
if (relativePath.includes("\\")) return false;
|
|
228
|
-
const normalized = toPosix(relativePath);
|
|
229
|
-
return Boolean(normalized) && normalized !== "." && normalized !== ".." && !normalized.startsWith("../") && !path.posix.isAbsolute(normalized) && path.posix.normalize(normalized) === normalized && !isDeniedPath(normalized);
|
|
230
|
-
}
|
|
231
|
-
function decodeBase64Strict(value, filePath) {
|
|
232
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
233
|
-
throw new Error(`Invalid base64 content in snapshot file: ${filePath}`);
|
|
234
|
-
}
|
|
235
|
-
return Buffer.from(value, "base64");
|
|
236
|
-
}
|
|
237
|
-
async function preflightSnapshotMutations(root, plan, sessionDir) {
|
|
238
|
-
const deletePaths = new Set(plan.deletes);
|
|
239
|
-
for (const target of plan.deletes) {
|
|
240
|
-
await assertNoSymlinkParents(rootForTarget(root, target, sessionDir), target);
|
|
241
|
-
}
|
|
242
|
-
for (const item of plan.writes) {
|
|
243
|
-
await prepareSnapshotWrite(rootForTarget(root, item.target, sessionDir), item.target, deletePaths);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
function rootForTarget(root, target, sessionDir) {
|
|
247
|
-
const sessionRoot = sessionDir ? sessionStorageRoot(root, sessionDir) : void 0;
|
|
248
|
-
if (sessionRoot && isPathInside(sessionRoot, target)) return sessionRoot;
|
|
249
|
-
return root;
|
|
250
|
-
}
|
|
251
|
-
async function prepareSnapshotWrite(root, target, deletePaths) {
|
|
252
|
-
const parentWillBeReplaced = await ensureSafeDirectory(root, path.dirname(target), deletePaths);
|
|
253
|
-
if (parentWillBeReplaced) return;
|
|
254
|
-
try {
|
|
255
|
-
const stat = await fs.lstat(target);
|
|
256
|
-
if (stat.isSymbolicLink()) throw new Error(`Refusing to overwrite symlink during snapshot apply: ${target}`);
|
|
257
|
-
if (stat.isDirectory() && !deletePaths.has(target)) {
|
|
258
|
-
throw new Error(`Refusing to overwrite directory during snapshot apply: ${target}`);
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
if (error.code !== "ENOENT") throw error;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
async function ensureSafeDirectory(root, directory, deletePaths) {
|
|
265
|
-
assertWithinRoot(root, directory);
|
|
266
|
-
const rootPath = path.resolve(root);
|
|
267
|
-
const relative = path.relative(rootPath, path.resolve(directory));
|
|
268
|
-
let current = rootPath;
|
|
269
|
-
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
270
|
-
current = path.join(current, part);
|
|
271
|
-
try {
|
|
272
|
-
const stat = await fs.lstat(current);
|
|
273
|
-
if (stat.isSymbolicLink()) throw new Error(`Refusing to follow symlink during snapshot apply: ${current}`);
|
|
274
|
-
if (!stat.isDirectory()) {
|
|
275
|
-
if (deletePaths.has(current)) return true;
|
|
276
|
-
throw new Error(`Snapshot path parent is not a directory: ${current}`);
|
|
277
|
-
}
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (error.code !== "ENOENT") throw error;
|
|
280
|
-
await fs.mkdir(current);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
async function assertNoSymlinkParents(root, target) {
|
|
286
|
-
assertWithinRoot(root, target);
|
|
287
|
-
const rootPath = path.resolve(root);
|
|
288
|
-
const relative = path.relative(rootPath, path.resolve(target));
|
|
289
|
-
let current = rootPath;
|
|
290
|
-
const parts = relative.split(path.sep).filter(Boolean);
|
|
291
|
-
for (const part of parts.slice(0, -1)) {
|
|
292
|
-
current = path.join(current, part);
|
|
293
|
-
try {
|
|
294
|
-
const stat = await fs.lstat(current);
|
|
295
|
-
if (stat.isSymbolicLink()) throw new Error(`Refusing to follow symlink during snapshot apply: ${current}`);
|
|
296
|
-
if (!stat.isDirectory()) throw new Error(`Snapshot path parent is not a directory: ${current}`);
|
|
297
|
-
} catch (error) {
|
|
298
|
-
if (error.code === "ENOENT") return;
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// src/ui/sync-format.ts
|
|
305
|
-
function formatDiff(local, remote) {
|
|
306
|
-
const localMap = fileHashMap(local);
|
|
307
|
-
const remoteMap = fileHashMap(remote);
|
|
308
|
-
const allPaths = [.../* @__PURE__ */ new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
309
|
-
const lines = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
|
|
310
|
-
let changed = 0;
|
|
311
|
-
for (const filePath of allPaths) {
|
|
312
|
-
if (!localMap[filePath]) {
|
|
313
|
-
lines.push(`Remote only: ${filePath}`);
|
|
314
|
-
changed += 1;
|
|
315
|
-
} else if (!remoteMap[filePath]) {
|
|
316
|
-
lines.push(`Local only: ${filePath}`);
|
|
317
|
-
changed += 1;
|
|
318
|
-
} else if (localMap[filePath] !== remoteMap[filePath]) {
|
|
319
|
-
lines.push(`Different: ${filePath}`);
|
|
320
|
-
changed += 1;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (changed === 0) lines.push("No file differences.");
|
|
324
|
-
return lines.join("\n");
|
|
325
|
-
}
|
|
326
|
-
function formatSnapshotOnlyDiff(title, snapshot) {
|
|
327
|
-
return [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${file.path}`)].join("\n");
|
|
328
|
-
}
|
|
329
|
-
function formatPushSummary(config, destination, upload, head, preservedRemoteFileCount = 0, remote) {
|
|
330
|
-
return [
|
|
331
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
332
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
333
|
-
`Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
|
|
334
|
-
`Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
335
|
-
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
336
|
-
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
337
|
-
...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
|
|
338
|
-
formatPublicationPreview(remote, upload),
|
|
339
|
-
preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
|
|
340
|
-
].join("\n");
|
|
341
|
-
}
|
|
342
|
-
function formatApplyPreview(local, remote) {
|
|
343
|
-
return formatDirectionalChanges(local, remote, {
|
|
344
|
-
add: "Add locally",
|
|
345
|
-
update: "Update locally",
|
|
346
|
-
remove: "Remove locally"
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
function formatPullSummary(config, destination, local, remote, protectedSessionCount) {
|
|
350
|
-
return [
|
|
351
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
352
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
353
|
-
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
354
|
-
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
355
|
-
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
356
|
-
formatApplyPreview(local, remote),
|
|
357
|
-
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
|
|
358
|
-
].join("\n");
|
|
359
|
-
}
|
|
360
|
-
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedSessionCount) {
|
|
361
|
-
return [
|
|
362
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
363
|
-
`Storage location: ${safeTerminalText(destination)}`,
|
|
364
|
-
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
365
|
-
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
366
|
-
`Protected live sessions: ${protectedSessionCount || "none"}`,
|
|
367
|
-
formatApplyPreview(local, remote),
|
|
368
|
-
"A local backup is created before applying; the backend's active head will change."
|
|
369
|
-
].join("\n");
|
|
370
|
-
}
|
|
371
|
-
function formatPublicationPreview(remote, upload) {
|
|
372
|
-
if (!remote) {
|
|
373
|
-
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${file.path}`)].join("\n");
|
|
374
|
-
}
|
|
375
|
-
return formatDirectionalChanges(remote, upload, {
|
|
376
|
-
add: "Add remotely",
|
|
377
|
-
update: "Update remotely",
|
|
378
|
-
remove: "Remove remotely"
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
function formatDirectionalChanges(before, after, labels) {
|
|
382
|
-
const beforeMap = fileHashMap(before);
|
|
383
|
-
const afterMap = fileHashMap(after);
|
|
384
|
-
const paths = [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
|
|
385
|
-
const lines = [];
|
|
386
|
-
for (const filePath of paths) {
|
|
387
|
-
if (!beforeMap[filePath]) lines.push(`${labels.add}: ${filePath}`);
|
|
388
|
-
else if (!afterMap[filePath]) lines.push(`${labels.remove}: ${filePath}`);
|
|
389
|
-
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${filePath}`);
|
|
390
|
-
}
|
|
391
|
-
if (lines.length === 0) lines.push("No file changes.");
|
|
392
|
-
return lines.join("\n");
|
|
393
|
-
}
|
|
394
|
-
function countPreservedRemoteFiles(local, upload) {
|
|
395
|
-
const localPaths = new Set(local.files.map((file) => file.path));
|
|
396
|
-
return upload.files.filter((file) => !localPaths.has(file.path)).length;
|
|
397
|
-
}
|
|
398
|
-
function publicationCapabilityDescription(capability) {
|
|
399
|
-
switch (capability) {
|
|
400
|
-
case "lease-protected":
|
|
401
|
-
return "lease-protected (exact expected-ref update)";
|
|
402
|
-
case "atomic-conditional":
|
|
403
|
-
return "atomic-conditional (verified atomic precondition)";
|
|
404
|
-
case "conditional-required":
|
|
405
|
-
return "conditional-required (read-only until atomic preconditions are verified)";
|
|
406
|
-
case "read-check-write-verify":
|
|
407
|
-
return "read-check-write-verify (visible races rejected; simultaneous unseen races remain possible)";
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// src/sync/sync-decision.ts
|
|
412
|
-
function createSyncDecision(options) {
|
|
413
|
-
const { config, state, local, remote, kind } = options;
|
|
414
|
-
const policyChanged = Boolean(state.lastAppliedSnapshot) && syncPolicyChanged(state, config);
|
|
415
|
-
const previousInclude = includeFromSelectionConfig(state);
|
|
416
|
-
const currentInclude = [...config.include];
|
|
417
|
-
const causes = {
|
|
418
|
-
localChanged: options.localChanged,
|
|
419
|
-
remoteChanged: options.remoteChanged,
|
|
420
|
-
policyChanged
|
|
421
|
-
};
|
|
422
|
-
const causeLines = kind === "first-sync-settings-diverged" ? ["This machine and the remote have different Pi settings on first sync."] : kind === "first-sync-sessions-diverged" ? ["Pi settings match, but local and remote sessions differ on first sync."] : kind === "remote-empty" ? ["The remote storage location is empty."] : [
|
|
423
|
-
...causes.localChanged ? ["Local content changed since the last sync."] : [],
|
|
424
|
-
...causes.remoteChanged ? ["Remote content changed since the last sync."] : [],
|
|
425
|
-
...causes.policyChanged ? ["Included content changed since the last sync."] : []
|
|
426
|
-
];
|
|
427
|
-
const comparison = remote ? formatDiff(local, remote) : formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local);
|
|
428
|
-
const review = [
|
|
429
|
-
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
430
|
-
"",
|
|
431
|
-
"Why a decision is required:",
|
|
432
|
-
...causeLines,
|
|
433
|
-
...policyChanged ? ["", `Previously included: ${safeList(previousInclude)}`, `Currently included: ${safeList(currentInclude)}`] : [],
|
|
434
|
-
"",
|
|
435
|
-
"Observed differences:",
|
|
436
|
-
...comparison.split("\n").map(safeTerminalText)
|
|
437
|
-
].join("\n");
|
|
438
|
-
return new SyncDecisionRequiredError({
|
|
439
|
-
kind,
|
|
440
|
-
setupName: config.setupName,
|
|
441
|
-
configIdentity: syncConfigReviewIdentity(config),
|
|
442
|
-
causes,
|
|
443
|
-
...policyChanged ? { previousInclude: [...previousInclude] } : {},
|
|
444
|
-
currentInclude,
|
|
445
|
-
review,
|
|
446
|
-
directions: kind === "remote-empty" ? ["push"] : ["push", "pull"],
|
|
447
|
-
directMessage: options.directMessage
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
function safeList(values) {
|
|
451
|
-
return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
|
|
128
|
+
validate();
|
|
129
|
+
return confirmed;
|
|
452
130
|
}
|
|
453
131
|
|
|
454
132
|
// src/sync/sync-mutations.ts
|
|
455
133
|
var VERSION = 1;
|
|
456
134
|
var POST_LOCAL_COMMIT_TIMEOUT_MS = 3e4;
|
|
135
|
+
function requireResolvedDirectionalProgress(state, options, allowForce = true) {
|
|
136
|
+
if (state.unresolved?.length && (options.auto || !options.force || !allowForce))
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Unresolved partial conflicts remain. Review /sync conflicts or explicitly choose push --force or pull --force; automatic transfer and rollback cannot accept unresolved groups."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
457
141
|
var PublicationStatePersistenceError = class extends Error {
|
|
458
142
|
head;
|
|
459
143
|
backupPath;
|
|
@@ -478,13 +162,19 @@ var RollbackPublicationError = class extends Error {
|
|
|
478
162
|
}
|
|
479
163
|
};
|
|
480
164
|
async function push(ctx, options, input, factory = createSyncBackend) {
|
|
165
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
166
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
481
167
|
const config = input?.config ?? await loadConfig(options.setup);
|
|
482
168
|
throwIfAborted(options.signal);
|
|
169
|
+
await requireNoMergeJournal(config, options);
|
|
170
|
+
const state = input?.state ?? await readStateForConfig(config);
|
|
171
|
+
validateMutation();
|
|
172
|
+
requireResolvedDirectionalProgress(state, options);
|
|
483
173
|
setSyncStatus(ctx, `pushing ${config.setupName}`);
|
|
484
174
|
const backend = input?.backend ?? await factory(config);
|
|
485
|
-
const state = input?.state ?? await readStateForConfig(config);
|
|
486
175
|
throwIfAborted(options.signal);
|
|
487
|
-
const
|
|
176
|
+
const localRaw = input?.local ?? await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
177
|
+
const local = portableSnapshot(localRaw, config.localFields);
|
|
488
178
|
throwIfAborted(options.signal);
|
|
489
179
|
let head = await backend.readHead(options.signal);
|
|
490
180
|
let remoteForUpload = await readRemoteSnapshotForUpload(backend, config, head, state, options.signal);
|
|
@@ -509,6 +199,13 @@ async function push(ctx, options, input, factory = createSyncBackend) {
|
|
|
509
199
|
});
|
|
510
200
|
}
|
|
511
201
|
}
|
|
202
|
+
if (!remoteForUpload && head && config.localFields !== void 0)
|
|
203
|
+
remoteForUpload = await readSnapshotForHead(backend, head, options.signal);
|
|
204
|
+
if (!await confirmFieldMigration(ctx, config, state, remoteForUpload, options.force, options.signal)) {
|
|
205
|
+
setSyncStatus(ctx, void 0);
|
|
206
|
+
return "cancelled";
|
|
207
|
+
}
|
|
208
|
+
const configIdentity = syncCheckConfigFingerprint(config);
|
|
512
209
|
let upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
513
210
|
if (!config.skipSecretScan) {
|
|
514
211
|
const secrets = scanSnapshot(local);
|
|
@@ -524,7 +221,13 @@ ${secrets.map((s) => `- ${s}`).join("\n")}`);
|
|
|
524
221
|
const refreshedHead = await backend.readHead(options.signal);
|
|
525
222
|
if (!sameRemoteHead(backend, head, refreshedHead)) {
|
|
526
223
|
head = refreshedHead;
|
|
527
|
-
remoteForUpload = head ? await backend
|
|
224
|
+
remoteForUpload = head ? await readSnapshotForHead(backend, head, options.signal) : void 0;
|
|
225
|
+
validateMutation();
|
|
226
|
+
if (!await confirmFieldMigration(ctx, config, state, remoteForUpload, options.force, options.signal)) {
|
|
227
|
+
setSyncStatus(ctx, void 0);
|
|
228
|
+
return "cancelled";
|
|
229
|
+
}
|
|
230
|
+
validateMutation();
|
|
528
231
|
upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
529
232
|
if (!await confirmPush(
|
|
530
233
|
ctx,
|
|
@@ -541,22 +244,42 @@ ${secrets.map((s) => `- ${s}`).join("\n")}`);
|
|
|
541
244
|
}
|
|
542
245
|
}
|
|
543
246
|
}
|
|
247
|
+
validateMutation();
|
|
248
|
+
if (configIdentity !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
249
|
+
throw new Error("Settings changed during push review.");
|
|
250
|
+
validateMutation();
|
|
251
|
+
if (config.localFields !== void 0 || await readMergeJournal(config)) {
|
|
252
|
+
const current = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
253
|
+
if (!sameHashes(fileHashMap(current), fileHashMap(localRaw)))
|
|
254
|
+
throw new Error("Local content changed during recovery review; retry from current bytes.");
|
|
255
|
+
}
|
|
256
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options);
|
|
257
|
+
validateMutation();
|
|
544
258
|
const result = await backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
545
259
|
signal: options.signal,
|
|
546
260
|
onCommit: options.onCommit
|
|
547
261
|
});
|
|
548
262
|
try {
|
|
549
|
-
await writeStateForConfig(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
263
|
+
await writeStateForConfig(
|
|
264
|
+
config,
|
|
265
|
+
{
|
|
266
|
+
version: VERSION,
|
|
267
|
+
profile: config.snapshotIdentity,
|
|
268
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
269
|
+
lastRemoteRevision: result.head.revision,
|
|
270
|
+
lastFileHashes: fileHashMap(local),
|
|
271
|
+
include: [...config.include],
|
|
272
|
+
...config.localFields !== void 0 ? { localFields: config.localFields } : {}
|
|
273
|
+
},
|
|
274
|
+
local,
|
|
275
|
+
validateCommittedOwner
|
|
276
|
+
);
|
|
557
277
|
} catch (error) {
|
|
558
278
|
throw new PublicationStatePersistenceError(result.head, error);
|
|
559
279
|
}
|
|
280
|
+
await retireMergeJournal(config);
|
|
281
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
282
|
+
validateCommittedOwner();
|
|
560
283
|
if (options.signal?.aborted) return;
|
|
561
284
|
setSyncStatus(ctx, void 0);
|
|
562
285
|
if (!options.silent) {
|
|
@@ -571,12 +294,16 @@ ${secrets.map((s) => `- ${s}`).join("\n")}`);
|
|
|
571
294
|
return "applied";
|
|
572
295
|
}
|
|
573
296
|
async function pull(ctx, options, factory = createSyncBackend) {
|
|
297
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
298
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
574
299
|
const config = await loadConfig(options.setup);
|
|
575
300
|
throwIfAborted(options.signal);
|
|
301
|
+
await requireNoMergeJournal(config, options);
|
|
576
302
|
setSyncStatus(ctx, `pulling ${config.setupName}`);
|
|
577
303
|
const backend = await factory(config);
|
|
578
304
|
const state = await readStateForConfig(config);
|
|
579
|
-
|
|
305
|
+
validateMutation();
|
|
306
|
+
requireResolvedDirectionalProgress(state, options);
|
|
580
307
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
581
308
|
throwIfAborted(options.signal);
|
|
582
309
|
const { head, snapshot: remote } = await readRemoteSnapshot(backend, config, options.signal);
|
|
@@ -593,6 +320,19 @@ async function pull(ctx, options, factory = createSyncBackend) {
|
|
|
593
320
|
directMessage: "Remote is empty. Run /sync push from a configured machine first."
|
|
594
321
|
});
|
|
595
322
|
}
|
|
323
|
+
if (!sameLocalFields(config.localFields, remote.localFields)) {
|
|
324
|
+
setSyncStatus(ctx, void 0);
|
|
325
|
+
throw new Error(
|
|
326
|
+
"Pull requires the configured local-field policy to match the remote. To change the remote policy, review push --force from the authoritative machine first, then pull using matching rules; old history is not erased."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (!await confirmFieldMigration(ctx, config, state, remote, options.force, options.signal)) {
|
|
330
|
+
setSyncStatus(ctx, void 0);
|
|
331
|
+
return "cancelled";
|
|
332
|
+
}
|
|
333
|
+
const configIdentity = syncCheckConfigFingerprint(config);
|
|
334
|
+
const logical = portableSnapshot(remote, config.localFields);
|
|
335
|
+
const physical = overlayLocalFields(logical, local, config.localFields);
|
|
596
336
|
const remoteChanged = hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx));
|
|
597
337
|
if (localChanged && remoteChanged && state.lastAppliedSnapshot && !options.force) {
|
|
598
338
|
throw createSyncDecision({
|
|
@@ -608,29 +348,65 @@ async function pull(ctx, options, factory = createSyncBackend) {
|
|
|
608
348
|
}
|
|
609
349
|
if (!options.yes && !await ctx.ui.confirm(
|
|
610
350
|
snapshotIncludesSessions(remote) ? "Pull pi settings and sessions?" : "Pull pi settings?",
|
|
611
|
-
formatPullSummary(config, backend.destination, local,
|
|
351
|
+
formatPullSummary(config, backend.destination, local, physical, protectedSessionPaths(ctx).size)
|
|
612
352
|
)) {
|
|
613
353
|
setSyncStatus(ctx, void 0);
|
|
614
354
|
ctx.ui.notify("Pull cancelled.", "info");
|
|
615
355
|
return "cancelled";
|
|
616
356
|
}
|
|
617
357
|
throwIfAborted(options.signal);
|
|
358
|
+
const recoveringMerge = Boolean(await readMergeJournal(config));
|
|
359
|
+
const requireFreshRecovery = async () => {
|
|
360
|
+
validateMutation();
|
|
361
|
+
const currentHead = await backend.readHead(options.signal);
|
|
362
|
+
validateMutation();
|
|
363
|
+
const current = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
364
|
+
validateMutation();
|
|
365
|
+
if (!sameHashes(fileHashMap(current), fileHashMap(local)) || !sameRemoteHead(backend, head, currentHead)) {
|
|
366
|
+
throw new Error("Local or remote content changed during recovery review; retry from a fresh diff.");
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
if (recoveringMerge) await requireFreshRecovery();
|
|
370
|
+
if (configIdentity !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
371
|
+
throw new Error("Settings changed during pull review.");
|
|
372
|
+
validateMutation();
|
|
373
|
+
const refreshed = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
374
|
+
if (!sameHashes(fileHashMap(local), fileHashMap(refreshed)))
|
|
375
|
+
throw new Error("Local bytes changed during pull review.");
|
|
376
|
+
if (!sameRemoteHead(backend, head, await backend.readHead(options.signal)))
|
|
377
|
+
throw new Error("Remote changed during pull review.");
|
|
378
|
+
validateMutation();
|
|
618
379
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
619
|
-
const applySessionDir = await sessionDirForApply(ctx,
|
|
620
|
-
|
|
380
|
+
const applySessionDir = await sessionDirForApply(ctx, physical);
|
|
381
|
+
if (recoveringMerge) await requireFreshRecovery();
|
|
382
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options);
|
|
383
|
+
validateMutation();
|
|
621
384
|
options.onCommit?.();
|
|
622
|
-
const lastFileHashes = await applySnapshot(
|
|
385
|
+
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
623
386
|
include: config.include,
|
|
624
|
-
sessionDir: applySessionDir
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
profile: config.snapshotIdentity,
|
|
629
|
-
lastAppliedSnapshot: remote.id,
|
|
630
|
-
lastRemoteRevision: head?.revision,
|
|
631
|
-
lastFileHashes,
|
|
632
|
-
include: [...config.include]
|
|
387
|
+
sessionDir: applySessionDir,
|
|
388
|
+
signal: options.signal,
|
|
389
|
+
validateMutation,
|
|
390
|
+
expectedFileHashes: config.localFields?.length ? { "settings.json": fileHashMap(local)["settings.json"] ?? null } : void 0
|
|
633
391
|
});
|
|
392
|
+
validateCommittedOwner();
|
|
393
|
+
await writeStateForConfig(
|
|
394
|
+
config,
|
|
395
|
+
{
|
|
396
|
+
version: VERSION,
|
|
397
|
+
profile: config.snapshotIdentity,
|
|
398
|
+
lastAppliedSnapshot: remote.id,
|
|
399
|
+
lastRemoteRevision: head?.revision,
|
|
400
|
+
lastFileHashes: config.localFields?.length ? { ...lastFileHashes, "settings.json": fileHashMap(logical)["settings.json"] } : lastFileHashes,
|
|
401
|
+
include: [...config.include],
|
|
402
|
+
...config.localFields !== void 0 ? { localFields: config.localFields } : {}
|
|
403
|
+
},
|
|
404
|
+
logical,
|
|
405
|
+
validateCommittedOwner
|
|
406
|
+
);
|
|
407
|
+
await retireMergeJournal(config);
|
|
408
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
409
|
+
validateCommittedOwner();
|
|
634
410
|
if (options.signal?.aborted) return "applied";
|
|
635
411
|
setSyncStatus(ctx, void 0);
|
|
636
412
|
if (!options.silent) {
|
|
@@ -641,15 +417,20 @@ async function pull(ctx, options, factory = createSyncBackend) {
|
|
|
641
417
|
"warning"
|
|
642
418
|
);
|
|
643
419
|
}
|
|
644
|
-
if (options.reload) await maybeReload(ctx, options.signal);
|
|
420
|
+
if (options.reload) await maybeReload(ctx, options.signal, validateMutation);
|
|
645
421
|
return "applied";
|
|
646
422
|
}
|
|
647
423
|
async function syncBoth(ctx, options, factory = createSyncBackend) {
|
|
424
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
648
425
|
const config = await loadConfig(options.setup);
|
|
649
426
|
throwIfAborted(options.signal);
|
|
650
|
-
|
|
427
|
+
if (config.include.length > 0 && options.auto && config.automaticTransfer) return mergeSync(ctx, options, factory);
|
|
428
|
+
if (!options.auto && config.include.length > 0 && await readMergeJournal(config))
|
|
429
|
+
return mergeSync(ctx, options, factory);
|
|
651
430
|
const state = await readStateForConfig(config);
|
|
652
431
|
throwIfAborted(options.signal);
|
|
432
|
+
if (!options.auto && config.include.length > 0 && state.lastAppliedSnapshot) return mergeSync(ctx, options, factory);
|
|
433
|
+
const backend = await factory(config);
|
|
653
434
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
654
435
|
throwIfAborted(options.signal);
|
|
655
436
|
if (config.include.length === 0) {
|
|
@@ -666,8 +447,34 @@ async function syncBoth(ctx, options, factory = createSyncBackend) {
|
|
|
666
447
|
const localChanged = hasLocalChanges(local, state, config);
|
|
667
448
|
const remoteChanged = remote ? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx)) : false;
|
|
668
449
|
const firstSync = !state.lastAppliedSnapshot;
|
|
450
|
+
if (options.auto) throw new Error("Automatic transfer is not authorized by the current settings.");
|
|
451
|
+
const portableLocal = portableSnapshot(local, config.localFields);
|
|
452
|
+
const acceptMatchingState = async (snapshot) => {
|
|
453
|
+
requireCompatibleRemoteSelection(config, snapshot);
|
|
454
|
+
const accepted = portableSnapshot(snapshot, config.localFields);
|
|
455
|
+
if (state.lastAppliedSnapshot && !sameLocalFields(state.localFields, config.localFields))
|
|
456
|
+
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
457
|
+
await writeStateForConfig(
|
|
458
|
+
config,
|
|
459
|
+
{
|
|
460
|
+
version: VERSION,
|
|
461
|
+
profile: config.snapshotIdentity,
|
|
462
|
+
lastAppliedSnapshot: snapshot.id,
|
|
463
|
+
lastRemoteRevision: head?.revision,
|
|
464
|
+
lastFileHashes: fileHashMap(accepted),
|
|
465
|
+
include: [...config.include],
|
|
466
|
+
...config.localFields !== void 0 ? { localFields: config.localFields } : {}
|
|
467
|
+
},
|
|
468
|
+
accepted,
|
|
469
|
+
validateMutation
|
|
470
|
+
);
|
|
471
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateMutation);
|
|
472
|
+
validateMutation();
|
|
473
|
+
};
|
|
669
474
|
if (firstSync && remote && remote.files.length > 0 && local.files.length > 0) {
|
|
670
|
-
|
|
475
|
+
requireCompatibleRemoteSelection(config, remote);
|
|
476
|
+
const portableRemote = portableSnapshot(remote, config.localFields);
|
|
477
|
+
if (!canPullRemoteSettingsOnFirstSync(portableLocal, portableRemote)) {
|
|
671
478
|
throw createSyncDecision({
|
|
672
479
|
kind: "first-sync-settings-diverged",
|
|
673
480
|
config,
|
|
@@ -679,8 +486,8 @@ async function syncBoth(ctx, options, factory = createSyncBackend) {
|
|
|
679
486
|
directMessage: "Remote settings exist and this machine has different local Pi settings. Run /sync diff, then manually choose /sync pull or /sync push."
|
|
680
487
|
});
|
|
681
488
|
}
|
|
682
|
-
if (!sameHashes(fileHashMap(
|
|
683
|
-
if (!canPullRemoteSessionsOnFirstSync(
|
|
489
|
+
if (!sameHashes(fileHashMap(portableLocal), fileHashMap(portableRemote))) {
|
|
490
|
+
if (!canPullRemoteSessionsOnFirstSync(portableLocal, portableRemote)) {
|
|
684
491
|
throw createSyncDecision({
|
|
685
492
|
kind: "first-sync-sessions-diverged",
|
|
686
493
|
config,
|
|
@@ -695,26 +502,13 @@ async function syncBoth(ctx, options, factory = createSyncBackend) {
|
|
|
695
502
|
await pull(ctx, options, factory);
|
|
696
503
|
return;
|
|
697
504
|
}
|
|
698
|
-
await
|
|
699
|
-
version: VERSION,
|
|
700
|
-
profile: config.snapshotIdentity,
|
|
701
|
-
lastAppliedSnapshot: remote.id,
|
|
702
|
-
lastRemoteRevision: head?.revision,
|
|
703
|
-
lastFileHashes: fileHashMap(remote),
|
|
704
|
-
include: [...config.include]
|
|
705
|
-
});
|
|
505
|
+
await acceptMatchingState(remote);
|
|
706
506
|
if (!options.silent) ctx.ui.notify("pi-sync state initialized; local settings already match remote.", "info");
|
|
707
507
|
return;
|
|
708
508
|
}
|
|
709
|
-
if (localChanged && remoteChanged && remote && snapshotsMatch(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
profile: config.snapshotIdentity,
|
|
713
|
-
lastAppliedSnapshot: remote.id,
|
|
714
|
-
lastRemoteRevision: head?.revision,
|
|
715
|
-
lastFileHashes: fileHashMap(remote),
|
|
716
|
-
include: [...config.include]
|
|
717
|
-
});
|
|
509
|
+
if (localChanged && remoteChanged && remote && snapshotsMatch(portableLocal, portableSnapshot(remote, remote.localFields))) {
|
|
510
|
+
requireCompatibleRemoteSelection(config, remote);
|
|
511
|
+
await acceptMatchingState(remote);
|
|
718
512
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
719
513
|
return;
|
|
720
514
|
}
|
|
@@ -739,18 +533,13 @@ async function syncBoth(ctx, options, factory = createSyncBackend) {
|
|
|
739
533
|
return;
|
|
740
534
|
}
|
|
741
535
|
if (shouldRefreshSyncedState(remote, head, state, config, (left, right) => backend.sameRevision(left, right))) {
|
|
742
|
-
await
|
|
743
|
-
version: VERSION,
|
|
744
|
-
profile: config.snapshotIdentity,
|
|
745
|
-
lastAppliedSnapshot: remote.id,
|
|
746
|
-
lastRemoteRevision: head?.revision,
|
|
747
|
-
lastFileHashes: fileHashMap(remote),
|
|
748
|
-
include: [...config.include]
|
|
749
|
-
});
|
|
536
|
+
await acceptMatchingState(remote);
|
|
750
537
|
}
|
|
751
538
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
752
539
|
}
|
|
753
540
|
async function rollback(ctx, options, factory = createSyncBackend, expectedSelection) {
|
|
541
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
542
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
754
543
|
const target = options.args[0];
|
|
755
544
|
if (!target) throw new Error("Usage: /sync rollback <snapshot-id> [--yes]");
|
|
756
545
|
const config = await loadConfig(options.setup);
|
|
@@ -759,37 +548,63 @@ async function rollback(ctx, options, factory = createSyncBackend, expectedSelec
|
|
|
759
548
|
if (expectedSelection && (backend.identity !== expectedSelection.backendIdentity || config.setupName !== expectedSelection.setup)) {
|
|
760
549
|
throw new Error("Sync setup or storage location changed while history was open; reopen history and retry.");
|
|
761
550
|
}
|
|
551
|
+
await requireNoMergeJournal(config);
|
|
552
|
+
const state = await readStateForConfig(config);
|
|
553
|
+
validateMutation();
|
|
554
|
+
requireResolvedDirectionalProgress(state, options, false);
|
|
762
555
|
const decoded = await backend.readSnapshot(target, options.signal);
|
|
556
|
+
validatePortableSnapshot(decoded);
|
|
557
|
+
if (!sameLocalFields(decoded.localFields, config.localFields))
|
|
558
|
+
throw new Error(
|
|
559
|
+
"Rollback requires the historical snapshot local-field policy to match the configured rules. Review push --force from current local bytes to migrate policy first; choose matching history afterward."
|
|
560
|
+
);
|
|
763
561
|
const selected = filterSnapshotForConfigPolicy(
|
|
764
562
|
config.include.includes("sessions") ? decoded : snapshotWithoutSessions(decoded),
|
|
765
563
|
config
|
|
766
564
|
);
|
|
767
|
-
const remote = regenerateSnapshotIdentity(selected);
|
|
565
|
+
const remote = portableSnapshot(regenerateSnapshotIdentity(selected), config.localFields);
|
|
768
566
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
769
567
|
const expectedHead = await backend.readHead(options.signal);
|
|
770
|
-
|
|
568
|
+
const currentRemote = expectedHead ? await readSnapshotForHead(backend, expectedHead, options.signal) : void 0;
|
|
569
|
+
validateMutation();
|
|
570
|
+
if (!await confirmFieldMigration(ctx, config, state, currentRemote, true, options.signal)) return;
|
|
571
|
+
validateMutation();
|
|
572
|
+
const physical = overlayLocalFields(remote, local, config.localFields);
|
|
771
573
|
if (!options.yes && !await ctx.ui.confirm(
|
|
772
574
|
snapshotIncludesSessions(remote) ? "Rollback pi settings and sessions?" : "Rollback pi settings?",
|
|
773
|
-
formatRollbackSummary(config, backend.destination, local,
|
|
575
|
+
formatRollbackSummary(config, backend.destination, local, physical, target, protectedSessionPaths(ctx).size)
|
|
774
576
|
)) {
|
|
775
577
|
ctx.ui.notify("Rollback cancelled.", "info");
|
|
776
578
|
return;
|
|
777
579
|
}
|
|
580
|
+
validateMutation();
|
|
581
|
+
const currentLocal = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
582
|
+
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(local)))
|
|
583
|
+
throw new Error("Local bytes changed during rollback review.");
|
|
584
|
+
if (syncCheckConfigFingerprint(config) !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
585
|
+
throw new Error("Settings changed during rollback review.");
|
|
586
|
+
if (!sameRemoteHead(backend, expectedHead, await backend.readHead(options.signal)))
|
|
587
|
+
throw new Error("Remote changed during rollback review.");
|
|
588
|
+
validateMutation();
|
|
778
589
|
throwIfAborted(options.signal);
|
|
779
590
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
780
591
|
const applySessionDir = await sessionDirForApply(ctx, remote);
|
|
781
|
-
|
|
592
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options, false);
|
|
593
|
+
validateMutation();
|
|
782
594
|
options.onCommit?.();
|
|
783
|
-
const lastFileHashes = await applySnapshot(
|
|
595
|
+
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
784
596
|
include: config.include,
|
|
785
|
-
sessionDir: applySessionDir
|
|
597
|
+
sessionDir: applySessionDir,
|
|
598
|
+
signal: options.signal,
|
|
599
|
+
validateMutation,
|
|
600
|
+
expectedFileHashes: config.localFields?.length ? { "settings.json": fileHashMap(local)["settings.json"] ?? null } : void 0
|
|
786
601
|
});
|
|
602
|
+
validateCommittedOwner();
|
|
787
603
|
let result;
|
|
788
604
|
try {
|
|
789
605
|
const completionSignal = AbortSignal.timeout(POST_LOCAL_COMMIT_TIMEOUT_MS);
|
|
790
|
-
const upload = await snapshotForUpload(backend, config, remote, expectedHead,
|
|
791
|
-
|
|
792
|
-
});
|
|
606
|
+
const upload = await snapshotForUpload(backend, config, remote, expectedHead, currentRemote, completionSignal);
|
|
607
|
+
validateCommittedOwner();
|
|
793
608
|
result = await backend.publishSnapshot(upload, expectedRemoteHead(expectedHead), {
|
|
794
609
|
signal: completionSignal
|
|
795
610
|
});
|
|
@@ -797,17 +612,26 @@ async function rollback(ctx, options, factory = createSyncBackend, expectedSelec
|
|
|
797
612
|
throw new RollbackPublicationError(backup, error);
|
|
798
613
|
}
|
|
799
614
|
try {
|
|
800
|
-
await writeStateForConfig(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
615
|
+
await writeStateForConfig(
|
|
616
|
+
config,
|
|
617
|
+
{
|
|
618
|
+
version: VERSION,
|
|
619
|
+
profile: config.snapshotIdentity,
|
|
620
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
621
|
+
lastRemoteRevision: result.head.revision,
|
|
622
|
+
lastFileHashes: config.localFields?.length ? { ...lastFileHashes, "settings.json": fileHashMap(remote)["settings.json"] } : lastFileHashes,
|
|
623
|
+
include: [...config.include],
|
|
624
|
+
...config.localFields !== void 0 ? { localFields: config.localFields } : {}
|
|
625
|
+
},
|
|
626
|
+
remote,
|
|
627
|
+
validateCommittedOwner
|
|
628
|
+
);
|
|
808
629
|
} catch (error) {
|
|
809
630
|
throw new PublicationStatePersistenceError(result.head, error, backup);
|
|
810
631
|
}
|
|
632
|
+
await retireMergeJournal(config);
|
|
633
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
634
|
+
validateCommittedOwner();
|
|
811
635
|
if (options.signal?.aborted) return;
|
|
812
636
|
ctx.ui.notify(
|
|
813
637
|
[
|
|
@@ -816,22 +640,17 @@ async function rollback(ctx, options, factory = createSyncBackend, expectedSelec
|
|
|
816
640
|
].filter(Boolean).join("\n"),
|
|
817
641
|
result.warnings.length > 0 ? "warning" : "info"
|
|
818
642
|
);
|
|
819
|
-
await maybeReload(ctx, options.signal);
|
|
820
|
-
}
|
|
821
|
-
function protectedSessionPaths(ctx) {
|
|
822
|
-
const getSessionFile = ctx.sessionManager.getSessionFile;
|
|
823
|
-
if (typeof getSessionFile !== "function") return /* @__PURE__ */ new Set();
|
|
824
|
-
const sessionFile = getSessionFile.call(ctx.sessionManager);
|
|
825
|
-
const snapshotPath = sessionFile ? sessionSnapshotPathFromAbsolute(sessionFile, sessionDirFromContext(ctx)) : void 0;
|
|
826
|
-
return snapshotPath ? /* @__PURE__ */ new Set([snapshotPath]) : /* @__PURE__ */ new Set();
|
|
643
|
+
await maybeReload(ctx, options.signal, validateMutation);
|
|
827
644
|
}
|
|
828
|
-
async function maybeReload(ctx, signal) {
|
|
645
|
+
async function maybeReload(ctx, signal, validateMutation = captureMutationOwner(ctx, signal)) {
|
|
829
646
|
if (signal?.aborted || !("reload" in ctx)) return;
|
|
647
|
+
validateMutation();
|
|
830
648
|
if (ctx.hasUI && await ctx.ui.confirm(
|
|
831
649
|
"Reload Pi resources now?",
|
|
832
650
|
"This reloads extensions, skills, prompts, themes, and context files."
|
|
833
651
|
)) {
|
|
834
652
|
if (signal?.aborted) return;
|
|
653
|
+
validateMutation();
|
|
835
654
|
await ctx.reload();
|
|
836
655
|
}
|
|
837
656
|
}
|
|
@@ -839,19 +658,11 @@ async function readRemoteSnapshotForUpload(backend, config, head, state, signal)
|
|
|
839
658
|
if (!head || head.snapshotId === state.lastAppliedSnapshot && !syncPolicyChanged(state, config) && (!state.lastRemoteRevision || backend.sameRevision(head.revision, state.lastRemoteRevision))) {
|
|
840
659
|
return void 0;
|
|
841
660
|
}
|
|
842
|
-
return backend
|
|
661
|
+
return readSnapshotForHead(backend, head, signal);
|
|
843
662
|
}
|
|
844
|
-
async function snapshotForUpload(backend, config, local, head, remote, signal
|
|
663
|
+
async function snapshotForUpload(backend, config, local, head, remote, signal) {
|
|
845
664
|
if (!head) return local;
|
|
846
|
-
|
|
847
|
-
if (!snapshot) {
|
|
848
|
-
try {
|
|
849
|
-
snapshot = await backend.readSnapshot(head.snapshotRef, signal);
|
|
850
|
-
} catch (error) {
|
|
851
|
-
if (options.ignoreUnreadableRemote) return local;
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
665
|
+
const snapshot = remote ?? await readSnapshotForHead(backend, head, signal);
|
|
855
666
|
return mergeRemotePreservedFiles(local, snapshot, config);
|
|
856
667
|
}
|
|
857
668
|
async function confirmPush(ctx, options, config, backend, local, upload, head, remote, title = snapshotIncludesSessions(upload) ? "Push pi settings and sessions?" : "Push pi settings?") {
|
|
@@ -871,19 +682,6 @@ function sameRemoteHead(backend, left, right) {
|
|
|
871
682
|
if (!left || !right) return left === right;
|
|
872
683
|
return backend.sameRevision(left.revision, right.revision);
|
|
873
684
|
}
|
|
874
|
-
async function backupLocal(profile, options = {}, signal) {
|
|
875
|
-
throwIfAborted(signal);
|
|
876
|
-
const snapshot = await createSnapshot(profile, options);
|
|
877
|
-
throwIfAborted(signal);
|
|
878
|
-
const backupDirectory = path2.join(stateDir(), "backups");
|
|
879
|
-
await fs2.mkdir(backupDirectory, { recursive: true });
|
|
880
|
-
throwIfAborted(signal);
|
|
881
|
-
const backupPath = path2.join(backupDirectory, `${snapshot.id}.json.gz`);
|
|
882
|
-
const encoded = await encodeSnapshot(snapshot);
|
|
883
|
-
throwIfAborted(signal);
|
|
884
|
-
await fs2.writeFile(backupPath, encoded, { signal });
|
|
885
|
-
return backupPath;
|
|
886
|
-
}
|
|
887
685
|
|
|
888
686
|
// src/sync/sync-queries.ts
|
|
889
687
|
var DEFAULT_PROFILE = "default";
|
|
@@ -924,7 +722,10 @@ async function diff(ctx, options, factory = createSyncBackend) {
|
|
|
924
722
|
throwIfAborted(options.signal);
|
|
925
723
|
setSyncStatus(ctx, `checking ${config.setupName}`);
|
|
926
724
|
const backend = await factory(config);
|
|
927
|
-
const local =
|
|
725
|
+
const local = portableSnapshot(
|
|
726
|
+
await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config)),
|
|
727
|
+
config.localFields
|
|
728
|
+
);
|
|
928
729
|
throwIfAborted(options.signal);
|
|
929
730
|
const { snapshot: remote, selectionState } = await readRemoteSnapshot(backend, config, options.signal, {
|
|
930
731
|
allowSelectionDifference: true
|
|
@@ -950,7 +751,7 @@ ${formatSnapshotOnlyDiff("Remote is empty. Local push would upload", local)}`, l
|
|
|
950
751
|
}
|
|
951
752
|
ctx.ui.notify(`${header}
|
|
952
753
|
|
|
953
|
-
${formatDiff(local, remote)}`, level);
|
|
754
|
+
${formatDiff(local, portableSnapshot(remote, remote.localFields))}`, level);
|
|
954
755
|
}
|
|
955
756
|
async function doctor(ctx, options, factory = createSyncBackend) {
|
|
956
757
|
const messages = [];
|
|
@@ -1075,4 +876,4 @@ export {
|
|
|
1075
876
|
status,
|
|
1076
877
|
syncBoth
|
|
1077
878
|
};
|
|
1078
|
-
//# sourceMappingURL=sync-operations-
|
|
879
|
+
//# sourceMappingURL=sync-operations-27VQUDH2.ts.map
|