@narumitw/pi-sync 0.54.2 → 0.56.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 +40 -15
- package/dist/chunks/agent-candidate-3JN6DJKT.ts +26 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts +99 -0
- package/dist/chunks/agent-plan-QCUJWBP4.ts.map +7 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts +182 -0
- package/dist/chunks/agent-runner-E2JU2VZ6.ts.map +7 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts +152 -0
- package/dist/chunks/agent-store-A3F6IYWZ.ts.map +7 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts +128 -0
- package/dist/chunks/chunk-5Q5SSKNG.ts.map +7 -0
- package/dist/chunks/chunk-5XZOC7FD.ts +215 -0
- package/dist/chunks/chunk-5XZOC7FD.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-AAEEHR7H.ts} +31 -87
- package/dist/chunks/chunk-AAEEHR7H.ts.map +7 -0
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-B4QZBDBT.ts} +2 -2
- package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-CRKTAPSL.ts} +8 -8
- package/dist/chunks/chunk-DT7M4VQS.ts +69 -0
- package/dist/chunks/chunk-DT7M4VQS.ts.map +7 -0
- package/dist/chunks/chunk-ED3TYIVW.ts +229 -0
- package/dist/chunks/chunk-ED3TYIVW.ts.map +7 -0
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-EUMINX7L.ts} +6 -6
- package/dist/chunks/chunk-FQOSHGJ7.ts +215 -0
- package/dist/chunks/chunk-FQOSHGJ7.ts.map +7 -0
- package/dist/chunks/chunk-GNH73BJ5.ts +153 -0
- package/dist/chunks/chunk-GNH73BJ5.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-GP5DU62N.ts} +8 -9
- package/dist/chunks/chunk-GP5DU62N.ts.map +7 -0
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-GSNPASKB.ts} +2 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-IWZH5KYS.ts} +23 -7
- package/dist/chunks/chunk-IWZH5KYS.ts.map +7 -0
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-JJTARUSO.ts} +15 -14
- package/dist/chunks/chunk-JJTARUSO.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-NLWS55B6.ts} +15 -2
- package/dist/chunks/chunk-NLWS55B6.ts.map +7 -0
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-OEWQVWLC.ts} +4 -4
- package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-OTZRCKYP.ts} +380 -510
- package/dist/chunks/chunk-OTZRCKYP.ts.map +7 -0
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-OWBAZA6J.ts} +24 -12
- package/dist/chunks/chunk-OWBAZA6J.ts.map +7 -0
- package/dist/chunks/chunk-PRUEIIK7.ts +24 -0
- package/dist/chunks/chunk-PRUEIIK7.ts.map +7 -0
- package/dist/chunks/chunk-QGK445K7.ts +21 -0
- package/dist/chunks/chunk-QGK445K7.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-QO333NM4.ts} +49 -21
- package/dist/chunks/chunk-QO333NM4.ts.map +7 -0
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-QSHMOL3I.ts} +22 -24
- package/dist/chunks/chunk-QSHMOL3I.ts.map +7 -0
- package/dist/chunks/chunk-S2FMH5FT.ts +97 -0
- package/dist/chunks/chunk-S2FMH5FT.ts.map +7 -0
- package/dist/chunks/{chunk-BVVP33U5.ts → chunk-SWRVXVFD.ts} +12 -9
- package/dist/chunks/chunk-SWRVXVFD.ts.map +7 -0
- package/dist/chunks/{chunk-ZX2Q6THB.ts → chunk-UBV24CG4.ts} +4 -5
- package/dist/chunks/chunk-UBV24CG4.ts.map +7 -0
- package/dist/chunks/chunk-UOHR7LRO.ts +70 -0
- package/dist/chunks/chunk-UOHR7LRO.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-UPNVIUBN.ts} +3 -3
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/chunk-VJN7KU4J.ts +350 -0
- package/dist/chunks/chunk-VJN7KU4J.ts.map +7 -0
- package/dist/chunks/chunk-YHT4YTYG.ts +31 -0
- package/dist/chunks/chunk-YHT4YTYG.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-YK52VDXG.ts} +6 -5
- package/dist/chunks/chunk-YK52VDXG.ts.map +7 -0
- package/dist/chunks/config-KGIYTTXQ.ts +37 -0
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-FXFIIBCB.ts} +8 -7
- package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-6WIEQLXW.ts} +31 -24
- package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-6WIEQLXW.ts.map} +1 -1
- package/dist/chunks/continuous-record-LFRVXQBD.ts +30 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts +545 -0
- package/dist/chunks/continuous-sync-AWAAOXK2.ts.map +7 -0
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-3JJYOVNR.ts} +5 -5
- package/dist/chunks/{git-backend-FD7AVSK6.ts → git-backend-KYDZ673X.ts} +28 -32
- package/dist/chunks/git-backend-KYDZ673X.ts.map +7 -0
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-AGBBVDDS.ts} +10 -9
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-KRAW3NE2.ts} +464 -164
- package/dist/chunks/manager-ui-KRAW3NE2.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-RFCAH7KL.ts} +7 -5
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-5725JDNC.ts} +9 -8
- package/dist/chunks/remote-selection-ui-5725JDNC.ts.map +7 -0
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y7WSCLJI.ts} +38 -38
- package/dist/chunks/s3-backend-Y7WSCLJI.ts.map +7 -0
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-E7PHKI6J.ts} +7 -6
- package/dist/chunks/setup-switch-E7PHKI6J.ts.map +7 -0
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-65DROCIJ.ts} +14 -13
- package/dist/chunks/setup-wizard-65DROCIJ.ts.map +7 -0
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-XFJOAMJD.ts} +9 -8
- package/dist/chunks/sync-inspection-XFJOAMJD.ts.map +7 -0
- package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-HDUDT5PD.ts} +188 -73
- package/dist/chunks/sync-operations-HDUDT5PD.ts.map +7 -0
- package/dist/chunks/{webdav-backend-TBILVEQK.ts → webdav-backend-6K26F6LT.ts} +39 -48
- package/dist/chunks/webdav-backend-6K26F6LT.ts.map +7 -0
- package/dist/index.ts +246 -131
- package/dist/index.ts.map +3 -3
- package/docs/agent-mode-verification.md +99 -0
- package/docs/continuous-sync-verification.md +87 -0
- package/docs/settings.md +131 -11
- package/docs/tui-contract.md +61 -0
- package/package.json +3 -2
- package/src/backends/git/git-backend.ts +1 -4
- package/src/backends/git/git-runner.ts +1 -5
- package/src/backends/s3/s3-backend.ts +3 -25
- package/src/backends/s3/s3-client.ts +14 -2
- package/src/backends/snapshot-pointer.ts +27 -0
- package/src/backends/webdav/webdav-backend.ts +3 -29
- package/src/backends/webdav/webdav-client.ts +11 -7
- package/src/commands/command-execution.ts +7 -3
- package/src/commands/command-handler.ts +15 -3
- package/src/commands/command-types.ts +10 -0
- package/src/commands/command.ts +4 -1
- package/src/settings/config.ts +20 -10
- package/src/settings/settings-store.ts +1 -34
- package/src/settings/settings-types.ts +10 -0
- package/src/settings/settings-validation.ts +4 -1
- package/src/snapshot/snapshot-codec.ts +1 -5
- package/src/state/lock.ts +13 -10
- package/src/state/merge-baseline-store.ts +19 -1
- package/src/state/state-directory.ts +8 -1
- package/src/sync/agent-candidate.ts +251 -0
- package/src/sync/agent-plan.ts +87 -0
- package/src/sync/agent-policy.ts +13 -0
- package/src/sync/agent-request.ts +17 -0
- package/src/sync/agent-runner.ts +188 -0
- package/src/sync/agent-store.ts +151 -0
- package/src/sync/agent-sync.ts +91 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +46 -33
- package/src/sync/continuous-errors.ts +85 -0
- package/src/sync/continuous-lease.ts +221 -0
- package/src/sync/continuous-record.ts +179 -0
- package/src/sync/continuous-storage.ts +55 -0
- package/src/sync/continuous-sync.ts +516 -0
- package/src/sync/continuous-watch.ts +59 -0
- package/src/sync/merged-sync.ts +268 -112
- package/src/sync/remote-snapshot.ts +2 -3
- package/src/sync/settings-merge.ts +19 -3
- package/src/sync/setup-switch.ts +1 -5
- package/src/sync/startup-check.ts +51 -29
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +16 -5
- package/src/sync/sync-mutations.ts +55 -28
- package/src/sync/sync-queries.ts +19 -3
- package/src/sync/text-merge.ts +3 -3
- package/src/sync-extension.ts +75 -19
- package/src/ui/agent-mode-setting.ts +91 -0
- package/src/ui/manager-state.ts +12 -6
- package/src/ui/manager-ui.ts +57 -7
- package/src/ui/merge-review.ts +3 -3
- package/src/ui/merged-sync-summary.ts +2 -18
- package/src/ui/remote-selection-ui.ts +5 -5
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +223 -43
- package/src/ui/setup/s3-credentials-ui.ts +1 -5
- package/src/ui/setup/setup-name-ui.ts +2 -5
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/setup/setup-switcher.ts +2 -0
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/sync-setups-ui.ts +3 -5
- package/src/ui/transfer-changes.ts +33 -0
- package/src/ui/transfer-review.ts +27 -0
- package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
- package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
- package/dist/chunks/chunk-5ZWQ33XL.ts.map +0 -7
- package/dist/chunks/chunk-BVVP33U5.ts.map +0 -7
- package/dist/chunks/chunk-IJ6VV6FN.ts.map +0 -7
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
- package/dist/chunks/chunk-ORQRHEEO.ts +0 -362
- package/dist/chunks/chunk-ORQRHEEO.ts.map +0 -7
- package/dist/chunks/chunk-QE7NZZWT.ts.map +0 -7
- package/dist/chunks/chunk-UQ3ALBIB.ts.map +0 -7
- package/dist/chunks/chunk-V6VZ5TLG.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/chunk-ZX2Q6THB.ts.map +0 -7
- package/dist/chunks/git-backend-FD7AVSK6.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/s3-backend-AQGV6LFT.ts.map +0 -7
- package/dist/chunks/setup-wizard-GSDPDFC4.ts.map +0 -7
- package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
- package/dist/chunks/webdav-backend-TBILVEQK.ts.map +0 -7
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → agent-candidate-3JN6DJKT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-B4QZBDBT.ts.map} +0 -0
- /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-CRKTAPSL.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-EUMINX7L.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-GSNPASKB.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-OEWQVWLC.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-UPNVIUBN.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → config-KGIYTTXQ.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → conflict-artifacts-FXFIIBCB.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → continuous-record-LFRVXQBD.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-3JJYOVNR.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → manager-result-dispatcher-AGBBVDDS.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → merge-baseline-store-RFCAH7KL.ts.map} +0 -0
|
@@ -1,16 +1,29 @@
|
|
|
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
|
-
|
|
4
|
+
confirmMergeReview
|
|
5
|
+
} from "./chunk-5XZOC7FD.ts";
|
|
6
|
+
import {
|
|
7
|
+
formatRemoteSelectionStatus,
|
|
8
|
+
readSnapshotForHead,
|
|
9
|
+
requireCompatibleRemoteSelection
|
|
10
|
+
} from "./chunk-YK52VDXG.ts";
|
|
11
|
+
import {
|
|
12
|
+
createSyncBackend
|
|
13
|
+
} from "./chunk-OEWQVWLC.ts";
|
|
14
|
+
import {
|
|
5
15
|
conflictArtifactFingerprint,
|
|
6
16
|
conflictDirectory,
|
|
7
17
|
conflictGroups,
|
|
8
|
-
mergeJournalIdentity,
|
|
9
18
|
readConflictArtifact,
|
|
19
|
+
saveConflictArtifact
|
|
20
|
+
} from "./chunk-ED3TYIVW.ts";
|
|
21
|
+
import {
|
|
22
|
+
clearMergeJournal,
|
|
23
|
+
mergeJournalIdentity,
|
|
10
24
|
readMergeJournal,
|
|
11
|
-
saveConflictArtifact,
|
|
12
25
|
writeMergeJournal
|
|
13
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-GNH73BJ5.ts";
|
|
14
27
|
import {
|
|
15
28
|
fileHashMap,
|
|
16
29
|
hasLocalChanges,
|
|
@@ -18,65 +31,44 @@ import {
|
|
|
18
31
|
sameHashes,
|
|
19
32
|
syncPolicyChanged
|
|
20
33
|
} from "./chunk-BDB4IDM3.ts";
|
|
21
|
-
import {
|
|
22
|
-
formatRemoteSelectionStatus,
|
|
23
|
-
readSnapshotForHead,
|
|
24
|
-
requireCompatibleRemoteSelection
|
|
25
|
-
} from "./chunk-V6VZ5TLG.ts";
|
|
26
|
-
import {
|
|
27
|
-
createSyncBackend
|
|
28
|
-
} from "./chunk-SDZCTCYQ.ts";
|
|
29
34
|
import {
|
|
30
35
|
applySnapshotTransaction,
|
|
31
36
|
recoverPendingSnapshotTransactions,
|
|
32
37
|
syncMutationParents
|
|
33
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-CRKTAPSL.ts";
|
|
34
39
|
import {
|
|
35
40
|
backupLocal,
|
|
36
41
|
captureMutationOwner,
|
|
37
42
|
protectedSessionPaths
|
|
38
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-UOHR7LRO.ts";
|
|
39
44
|
import {
|
|
40
45
|
safeTerminalText,
|
|
41
46
|
snapshotPathLabel
|
|
42
47
|
} from "./chunk-AMJRUXFT.ts";
|
|
43
48
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
mergeSession,
|
|
47
|
-
mergeText,
|
|
48
|
-
pruneMergeBaselines,
|
|
49
|
-
readMergeAncestor,
|
|
50
|
-
readMergeAncestors,
|
|
51
|
-
stageMergeBaseline
|
|
52
|
-
} from "./chunk-QE7NZZWT.ts";
|
|
49
|
+
createStartupTimings
|
|
50
|
+
} from "./chunk-UZPMDLTL.ts";
|
|
53
51
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from "./chunk-76NB4R7U.ts";
|
|
57
|
-
import {
|
|
58
|
-
readStateForConfig,
|
|
59
|
-
statePathForConfig,
|
|
60
|
-
syncStateFingerprint,
|
|
61
|
-
writeStateForConfig
|
|
62
|
-
} from "./chunk-4QVPTIZB.ts";
|
|
52
|
+
SyncDecisionRequiredError
|
|
53
|
+
} from "./chunk-7D5SF7P3.ts";
|
|
63
54
|
import {
|
|
64
55
|
loadConfig,
|
|
65
56
|
syncCheckConfigFingerprint,
|
|
66
57
|
syncConfigReviewIdentity
|
|
67
|
-
} from "./chunk-
|
|
68
|
-
import {
|
|
69
|
-
SyncBackendConflictError,
|
|
70
|
-
expectedRemoteHead
|
|
71
|
-
} from "./chunk-YDVQYUHE.ts";
|
|
72
|
-
import {
|
|
73
|
-
SyncDecisionRequiredError
|
|
74
|
-
} from "./chunk-7D5SF7P3.ts";
|
|
58
|
+
} from "./chunk-QO333NM4.ts";
|
|
75
59
|
import {
|
|
76
60
|
overlayLocalFields,
|
|
77
61
|
portableSnapshot,
|
|
78
62
|
sameLocalFields
|
|
79
63
|
} from "./chunk-A6HU4ERM.ts";
|
|
64
|
+
import {
|
|
65
|
+
SyncBackendConflictError,
|
|
66
|
+
expectedRemoteHead
|
|
67
|
+
} from "./chunk-YDVQYUHE.ts";
|
|
68
|
+
import {
|
|
69
|
+
mergeSettingsJson,
|
|
70
|
+
resolveContentConflicts
|
|
71
|
+
} from "./chunk-5Q5SSKNG.ts";
|
|
80
72
|
import {
|
|
81
73
|
agentDir,
|
|
82
74
|
createSnapshot,
|
|
@@ -96,10 +88,26 @@ import {
|
|
|
96
88
|
snapshotTarget
|
|
97
89
|
} from "./chunk-QOFMXPM5.ts";
|
|
98
90
|
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
TEXT_MERGE_WORK_LIMIT,
|
|
92
|
+
hasCurrentMergeBaseline,
|
|
93
|
+
pruneMergeBaselines,
|
|
94
|
+
readMergeAncestor,
|
|
95
|
+
readMergeAncestors,
|
|
96
|
+
stageMergeBaseline
|
|
97
|
+
} from "./chunk-IWZH5KYS.ts";
|
|
98
|
+
import {
|
|
99
|
+
mergePathIdentity,
|
|
100
|
+
planFileMerge
|
|
101
|
+
} from "./chunk-76NB4R7U.ts";
|
|
102
|
+
import {
|
|
103
|
+
startupTransferEnabled
|
|
104
|
+
} from "./chunk-PRUEIIK7.ts";
|
|
105
|
+
import {
|
|
106
|
+
readStateForConfig,
|
|
107
|
+
statePathForConfig,
|
|
108
|
+
syncStateFingerprint,
|
|
109
|
+
writeStateForConfig
|
|
110
|
+
} from "./chunk-UPNVIUBN.ts";
|
|
103
111
|
import {
|
|
104
112
|
assertWithinRoot,
|
|
105
113
|
isDeniedPath,
|
|
@@ -110,216 +118,32 @@ import {
|
|
|
110
118
|
toPosix
|
|
111
119
|
} from "./chunk-HUZ5CR4W.ts";
|
|
112
120
|
|
|
113
|
-
// src/ui/
|
|
114
|
-
import { truncateToWidth
|
|
115
|
-
import { defineMenu, runCustomInteraction, runDocumentReview, runMenu } from "@narumitw/pi-tui-kit";
|
|
121
|
+
// src/ui/merged-sync-summary.ts
|
|
122
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
116
123
|
|
|
117
|
-
// src/ui/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
[
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{ value: "details", label: "View details" }
|
|
128
|
-
],
|
|
129
|
-
3,
|
|
130
|
-
{
|
|
131
|
-
selectedPrefix: (text) => theme.fg("accent", text),
|
|
132
|
-
selectedText: (text) => theme.fg("accent", text),
|
|
133
|
-
description: (text) => theme.fg("muted", text),
|
|
134
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
135
|
-
noMatch: (text) => theme.fg("muted", text)
|
|
136
|
-
}
|
|
137
|
-
);
|
|
138
|
-
const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
|
|
139
|
-
let selected = 0;
|
|
140
|
-
let offset = 0;
|
|
141
|
-
let maximumOffset = 0;
|
|
142
|
-
let lastWidth = 0;
|
|
143
|
-
let lastRows = 0;
|
|
144
|
-
let seen = /* @__PURE__ */ new Set();
|
|
145
|
-
let summary = [];
|
|
146
|
-
let reviewed = false;
|
|
147
|
-
let disposed = false;
|
|
148
|
-
let finished = false;
|
|
149
|
-
let summaryStart = 0;
|
|
150
|
-
let summaryEnd = 0;
|
|
151
|
-
let actionsStart = 0;
|
|
152
|
-
const finish = (choice) => {
|
|
153
|
-
if (disposed || finished) return;
|
|
154
|
-
finished = true;
|
|
155
|
-
options.complete(choice);
|
|
156
|
-
};
|
|
157
|
-
const select = (index) => {
|
|
158
|
-
selected = (index + choices.length) % choices.length;
|
|
159
|
-
list.setSelectedIndex(selected);
|
|
160
|
-
};
|
|
161
|
-
list.onSelectionChange = (item) => {
|
|
162
|
-
selected = Math.max(0, choices.indexOf(item.value));
|
|
163
|
-
};
|
|
164
|
-
list.onSelect = (item) => {
|
|
165
|
-
const choice = choices[selected] ?? "cancel";
|
|
166
|
-
if (item.value === choice && (choice !== "approve" || reviewed)) finish(choice);
|
|
167
|
-
};
|
|
168
|
-
const component = {
|
|
169
|
-
render(width, expose = true) {
|
|
170
|
-
if (!Number.isFinite(width) || width <= 0) {
|
|
171
|
-
reviewed = false;
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
174
|
-
const cells = Math.floor(width);
|
|
175
|
-
const rows = Math.max(1, Math.floor(tui.terminal.rows) - 3);
|
|
176
|
-
if (cells !== lastWidth || rows !== lastRows) {
|
|
177
|
-
summary = options.lines.flatMap((line) => hardWrapTerminalDocument(line, cells));
|
|
178
|
-
seen = /* @__PURE__ */ new Set();
|
|
179
|
-
offset = 0;
|
|
180
|
-
select(0);
|
|
181
|
-
lastWidth = cells;
|
|
182
|
-
lastRows = rows;
|
|
183
|
-
}
|
|
184
|
-
const framed = rows >= 9;
|
|
185
|
-
const actions = list.render(cells);
|
|
186
|
-
const viewport = Math.max(0, rows - actions.length - 3 - (framed ? 2 : 0));
|
|
187
|
-
maximumOffset = Math.max(0, summary.length - viewport);
|
|
188
|
-
offset = Math.min(offset, maximumOffset);
|
|
189
|
-
const visible = summary.slice(offset, offset + viewport);
|
|
190
|
-
if (expose) for (let index = offset; index < offset + visible.length; index++) seen.add(index);
|
|
191
|
-
reviewed = viewport > 0 && seen.size === summary.length;
|
|
192
|
-
const hint = reviewed ? "Select an action." : "Navigation keys scroll the summary before selecting Yes.";
|
|
193
|
-
const body = [
|
|
194
|
-
theme.fg("accent", theme.bold(hardWrapTerminalDocument(options.title, cells).join(" "))),
|
|
195
|
-
...visible.map((line) => theme.fg("text", line)),
|
|
196
|
-
theme.fg(
|
|
197
|
-
"muted",
|
|
198
|
-
`Summary ${visible.length ? offset + 1 : 0}-${offset + visible.length}/${summary.length}${reviewed ? "" : " \u2014 review before Yes"}`
|
|
199
|
-
),
|
|
200
|
-
...actions,
|
|
201
|
-
theme.fg("dim", hint)
|
|
202
|
-
];
|
|
203
|
-
summaryStart = Number(framed) + 1;
|
|
204
|
-
summaryEnd = summaryStart + visible.length;
|
|
205
|
-
actionsStart = summaryEnd + 1;
|
|
206
|
-
const frame = framed ? [...rule.render(cells), ...body, ...rule.render(cells)] : body;
|
|
207
|
-
return frame.slice(0, rows).map((line) => truncateToWidth(line, cells, ""));
|
|
208
|
-
},
|
|
209
|
-
invalidate() {
|
|
210
|
-
list.invalidate();
|
|
211
|
-
rule.invalidate();
|
|
212
|
-
},
|
|
213
|
-
handleInput(data) {
|
|
214
|
-
if (disposed || finished) return;
|
|
215
|
-
if (matchesKey(data, Key.ctrl("c")) || keybindings.matches(data, "tui.select.cancel")) {
|
|
216
|
-
finish("cancel");
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
component.render(tui.terminal.columns ?? lastWidth, false);
|
|
220
|
-
if (keybindings.matches(data, "tui.select.up")) {
|
|
221
|
-
if (!reviewed || selected === 0 && offset > 0) offset = Math.max(0, offset - 1);
|
|
222
|
-
else select(selected - 1);
|
|
223
|
-
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
224
|
-
if (!reviewed || selected === 0 && offset < maximumOffset) offset = Math.min(maximumOffset, offset + 1);
|
|
225
|
-
else select(selected + 1);
|
|
226
|
-
} else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
227
|
-
const choice = choices[selected] ?? "cancel";
|
|
228
|
-
if (choice !== "approve" || reviewed) finish(choice);
|
|
229
|
-
}
|
|
230
|
-
tui.requestRender();
|
|
231
|
-
},
|
|
232
|
-
handleMouse(event) {
|
|
233
|
-
if (disposed || finished || event.width !== lastWidth) return void 0;
|
|
234
|
-
component.render(event.width, false);
|
|
235
|
-
if (event.y >= summaryStart && event.y < summaryEnd && event.type === "wheel" && event.wheelDelta) {
|
|
236
|
-
offset = Math.max(0, Math.min(maximumOffset, offset + (event.wheelDelta < 0 ? -1 : 1)));
|
|
237
|
-
select(0);
|
|
238
|
-
tui.requestRender();
|
|
239
|
-
return { handled: true, render: true };
|
|
240
|
-
}
|
|
241
|
-
if (event.y < actionsStart || event.y >= actionsStart + choices.length || event.y >= lastRows) return void 0;
|
|
242
|
-
return list.handleMouse({ ...event, y: event.y - actionsStart, height: choices.length });
|
|
243
|
-
},
|
|
244
|
-
dispose() {
|
|
245
|
-
disposed = true;
|
|
246
|
-
seen.clear();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
return component;
|
|
124
|
+
// src/ui/transfer-changes.ts
|
|
125
|
+
function transferChanges(before, after, protectedPaths = /* @__PURE__ */ new Set()) {
|
|
126
|
+
const beforeMap = before ? fileHashMap(before) : {};
|
|
127
|
+
const afterMap = fileHashMap(after);
|
|
128
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
|
|
129
|
+
if (protectedPaths.has(filePath)) return [];
|
|
130
|
+
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
131
|
+
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
132
|
+
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
133
|
+
});
|
|
250
134
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
];
|
|
258
|
-
while (isCurrent() && !signal?.aborted) {
|
|
259
|
-
const choice = { value: "cancel" };
|
|
260
|
-
const menu = defineMenu({
|
|
261
|
-
start: "confirm",
|
|
262
|
-
screens: {
|
|
263
|
-
confirm: () => ({
|
|
264
|
-
kind: "choice",
|
|
265
|
-
title,
|
|
266
|
-
lines,
|
|
267
|
-
initialItemId: "cancel",
|
|
268
|
-
items: [
|
|
269
|
-
{ id: "cancel", label: "No, cancel" },
|
|
270
|
-
{ id: "approve", label: `Yes, ${confirmationLabel}` },
|
|
271
|
-
{ id: "details", label: "View details" }
|
|
272
|
-
],
|
|
273
|
-
action: "choose",
|
|
274
|
-
hint: "close"
|
|
275
|
-
})
|
|
276
|
-
},
|
|
277
|
-
actions: {
|
|
278
|
-
choose: ({ itemId }) => {
|
|
279
|
-
if (itemId === "approve" || itemId === "details") choice.value = itemId;
|
|
280
|
-
return { kind: "close" };
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
const result = ctx.mode === "tui" && ctx.hasUI ? await runCustomInteraction(ctx, {
|
|
285
|
-
create: ({ tui, theme, keybindings, complete }) => createSyncConfirmation({ title, lines, confirmationLabel, tui, theme, keybindings, complete }),
|
|
286
|
-
signal,
|
|
287
|
-
isCurrent,
|
|
288
|
-
onError: () => {
|
|
289
|
-
}
|
|
290
|
-
}) : await runMenu(ctx, menu, { getState: () => void 0, signal, isCurrent, onError: () => {
|
|
291
|
-
} });
|
|
292
|
-
if (!isCurrent() || signal?.aborted) return false;
|
|
293
|
-
if (result.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
|
|
294
|
-
if (result.kind === "unsupported")
|
|
295
|
-
throw new Error("Merge review requires observable TUI or RPC; review the plan before using --yes.");
|
|
296
|
-
if (result.kind === "completed") choice.value = result.value;
|
|
297
|
-
else if (result.kind !== "closed") return false;
|
|
298
|
-
if (choice.value === "approve") return true;
|
|
299
|
-
if (choice.value !== "details") return false;
|
|
300
|
-
const detail = await runDocumentReview(ctx, {
|
|
301
|
-
title: "Sync change details",
|
|
302
|
-
content,
|
|
303
|
-
format: { kind: "text" },
|
|
304
|
-
viewportSize: "adaptive",
|
|
305
|
-
hint: "back",
|
|
306
|
-
signal,
|
|
307
|
-
isCurrent,
|
|
308
|
-
onError: () => {
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
if (!isCurrent() || signal?.aborted) return false;
|
|
312
|
-
if (detail.kind === "error") throw new Error("Merge review failed; no transfer was performed.");
|
|
313
|
-
if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
|
|
314
|
-
}
|
|
315
|
-
return false;
|
|
135
|
+
function transferCounts(changes) {
|
|
136
|
+
return {
|
|
137
|
+
add: changes.filter((change) => change.action === "Add").length,
|
|
138
|
+
update: changes.filter((change) => change.action === "Update").length,
|
|
139
|
+
remove: changes.filter((change) => change.action === "Delete").length
|
|
140
|
+
};
|
|
316
141
|
}
|
|
317
142
|
|
|
318
143
|
// src/ui/merged-sync-summary.ts
|
|
319
|
-
import { truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
320
144
|
function mergedSyncSummary(input) {
|
|
321
|
-
const local =
|
|
322
|
-
const remote =
|
|
145
|
+
const local = transferChanges(input.localBefore, input.localAfter);
|
|
146
|
+
const remote = transferChanges(input.remoteBefore, input.remoteAfter);
|
|
323
147
|
const all = [...local, ...remote];
|
|
324
148
|
const updatesOnly = all.length > 0 && all.every((change) => change.action === "Update");
|
|
325
149
|
const verb = updatesOnly ? "Update" : "Apply changes to";
|
|
@@ -343,7 +167,7 @@ function mergedSyncSummary(input) {
|
|
|
343
167
|
return [
|
|
344
168
|
`${heading}: ${counts.join(", ")}`,
|
|
345
169
|
...(compact ? items.slice(0, 3) : items).map(
|
|
346
|
-
(item) => ` ${compact ?
|
|
170
|
+
(item) => ` ${compact ? truncateToWidth(label(item, side), 80) : label(item, side)}`
|
|
347
171
|
),
|
|
348
172
|
...compact && items.length > 3 ? [` ${items.length - 3} more files \u2014 View details`] : []
|
|
349
173
|
];
|
|
@@ -351,6 +175,7 @@ function mergedSyncSummary(input) {
|
|
|
351
175
|
const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
|
|
352
176
|
const safety = [
|
|
353
177
|
`Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
|
|
178
|
+
"Remote removals affect the active snapshot; retained history is not erased.",
|
|
354
179
|
`Sessions: ${input.sessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
355
180
|
input.groups.length ? `Unresolved groups: ${input.groups.length} \u2014 these paths stay unchanged on each side` : "Conflicts: none",
|
|
356
181
|
...input.publish && input.capability === "read-check-write-verify" ? ["Warning: simultaneous remote writes can still race; visible changes are rejected."] : [],
|
|
@@ -362,7 +187,7 @@ function mergedSyncSummary(input) {
|
|
|
362
187
|
title,
|
|
363
188
|
confirmationLabel,
|
|
364
189
|
lines: [
|
|
365
|
-
...context.map((line) =>
|
|
190
|
+
...context.map((line) => truncateToWidth(line, 100)),
|
|
366
191
|
...section(local, "local", true),
|
|
367
192
|
...section(remote, "remote", true),
|
|
368
193
|
...safety
|
|
@@ -381,15 +206,6 @@ function mergedSyncSummary(input) {
|
|
|
381
206
|
].join("\n")
|
|
382
207
|
};
|
|
383
208
|
}
|
|
384
|
-
function changes(before, after) {
|
|
385
|
-
const beforeMap = fileHashMap(before);
|
|
386
|
-
const afterMap = fileHashMap(after);
|
|
387
|
-
return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
|
|
388
|
-
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
389
|
-
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
390
|
-
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
209
|
|
|
394
210
|
// src/sync/conflict-artifact-ownership.ts
|
|
395
211
|
import fs from "node:fs/promises";
|
|
@@ -534,76 +350,18 @@ async function pruneCompletedConflicts(config, backend, state, local, remote, va
|
|
|
534
350
|
}
|
|
535
351
|
}
|
|
536
352
|
|
|
537
|
-
// src/sync/content-conflicts.ts
|
|
538
|
-
import { createHash } from "node:crypto";
|
|
539
|
-
function snapshotFile(filePath, bytes) {
|
|
540
|
-
return {
|
|
541
|
-
path: filePath,
|
|
542
|
-
contentBase64: bytes.toString("base64"),
|
|
543
|
-
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
async function resolveContentConflicts(config, state, local, remote, plan, protectedPaths, options = {}) {
|
|
547
|
-
if (plan.kind !== "planned" || !config.mergeContent) return plan;
|
|
548
|
-
options.signal?.throwIfAborted();
|
|
549
|
-
const budget = options.textBudget ?? { remaining: TEXT_MERGE_WORK_LIMIT };
|
|
550
|
-
const decisions = [...plan.decisions];
|
|
551
|
-
let ancestors = [];
|
|
552
|
-
try {
|
|
553
|
-
ancestors = await readMergeAncestors(config, state) ?? [];
|
|
554
|
-
} catch {
|
|
555
|
-
options.signal?.throwIfAborted();
|
|
556
|
-
return plan;
|
|
557
|
-
}
|
|
558
|
-
options.signal?.throwIfAborted();
|
|
559
|
-
const oursByPath = new Map(local.files.map((file) => [file.path, file]));
|
|
560
|
-
const theirsByPath = new Map(remote.files.map((file) => [file.path, file]));
|
|
561
|
-
const ancestorsByPath = new Map(ancestors.map((file) => [file.path, file]));
|
|
562
|
-
const decisionIndex = new Map(decisions.map((decision, index) => [decision.path, index]));
|
|
563
|
-
const protectedKeys = new Set([...protectedPaths].map(mergePathIdentity));
|
|
564
|
-
for (const conflict of plan.conflicts) {
|
|
565
|
-
options.signal?.throwIfAborted();
|
|
566
|
-
if (conflict.reason !== "both-changed" || protectedKeys.has(mergePathIdentity(conflict.path))) continue;
|
|
567
|
-
const session = conflict.path.startsWith("sessions/") && conflict.path.endsWith(".jsonl");
|
|
568
|
-
if (!session && (!isMergeTextPath(conflict.path) || budget.remaining === 0)) continue;
|
|
569
|
-
const ours = oursByPath.get(conflict.path);
|
|
570
|
-
const theirs = theirsByPath.get(conflict.path);
|
|
571
|
-
if (!ours || !theirs) continue;
|
|
572
|
-
const ancestor = ancestorsByPath.get(conflict.path);
|
|
573
|
-
const base = ancestor ? Buffer.from(ancestor.contentBase64, "base64") : void 0;
|
|
574
|
-
if (!base) continue;
|
|
575
|
-
const localBytes = Buffer.from(ours.contentBase64, "base64");
|
|
576
|
-
const remoteBytes = Buffer.from(theirs.contentBase64, "base64");
|
|
577
|
-
const bytes = session ? mergeSession(base, localBytes, remoteBytes) : mergeText(base, localBytes, remoteBytes, { budget, signal: options.signal });
|
|
578
|
-
if (!bytes) continue;
|
|
579
|
-
const index = decisionIndex.get(conflict.path);
|
|
580
|
-
if (index === void 0) continue;
|
|
581
|
-
decisions[index] = {
|
|
582
|
-
kind: "accepted",
|
|
583
|
-
path: conflict.path,
|
|
584
|
-
source: "merged",
|
|
585
|
-
file: snapshotFile(conflict.path, bytes)
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
return {
|
|
589
|
-
kind: "planned",
|
|
590
|
-
decisions,
|
|
591
|
-
conflicts: decisions.filter((item) => item.kind === "conflict")
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
353
|
// src/sync/merge-apply.ts
|
|
596
|
-
import { createHash as
|
|
354
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
597
355
|
import fs4 from "node:fs/promises";
|
|
598
356
|
import path4 from "node:path";
|
|
599
357
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
600
358
|
|
|
601
359
|
// src/snapshot/snapshot-apply.ts
|
|
602
|
-
import { createHash
|
|
360
|
+
import { createHash } from "node:crypto";
|
|
603
361
|
import fs3 from "node:fs/promises";
|
|
604
362
|
import path3 from "node:path";
|
|
605
363
|
function sha256(value) {
|
|
606
|
-
return
|
|
364
|
+
return createHash("sha256").update(value).digest("hex");
|
|
607
365
|
}
|
|
608
366
|
function fileHashMap2(snapshot) {
|
|
609
367
|
return Object.fromEntries(snapshot.files.map((file) => [file.path, file.sha256]));
|
|
@@ -912,7 +670,7 @@ async function assertReviewedTree(directory, relative, beforeHashes) {
|
|
|
912
670
|
const stat = await fs4.lstat(child);
|
|
913
671
|
if (stat.isDirectory()) {
|
|
914
672
|
await assertReviewedTree(child, childRelative, beforeHashes);
|
|
915
|
-
} else if (stat.isFile() && stat.nlink === 1 && beforeHashes[childRelative] ===
|
|
673
|
+
} else if (stat.isFile() && stat.nlink === 1 && beforeHashes[childRelative] === createHash2("sha256").update(await fs4.readFile(child)).digest("hex")) {
|
|
916
674
|
continue;
|
|
917
675
|
} else {
|
|
918
676
|
throw new Error("Reviewed directory contains unknown or changed files; publication refused.");
|
|
@@ -1082,7 +840,7 @@ async function applyMergedSnapshot(before, after, protectedPaths, options, valid
|
|
|
1082
840
|
} else {
|
|
1083
841
|
if (!stat.isFile() || stat.nlink > 1)
|
|
1084
842
|
throw new Error("Merged transfer target is no longer an independent regular file.");
|
|
1085
|
-
hash =
|
|
843
|
+
hash = createHash2("sha256").update(await fs4.readFile(target2)).digest("hex");
|
|
1086
844
|
}
|
|
1087
845
|
} catch (error) {
|
|
1088
846
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -1260,53 +1018,7 @@ function progressState(config, head, snapshot, progress) {
|
|
|
1260
1018
|
}
|
|
1261
1019
|
|
|
1262
1020
|
// src/sync/settings-conflicts.ts
|
|
1263
|
-
import { createHash as
|
|
1264
|
-
|
|
1265
|
-
// src/sync/settings-merge.ts
|
|
1266
|
-
var COUPLED_FIELDS = [
|
|
1267
|
-
["defaultProvider", "defaultModel"],
|
|
1268
|
-
["queueMode", "steeringMode"],
|
|
1269
|
-
["websockets", "transport"],
|
|
1270
|
-
["skills", "enableSkillCommands"],
|
|
1271
|
-
["enableAnalytics", "trackingId"]
|
|
1272
|
-
];
|
|
1273
|
-
function mergeSettingsJson(ancestor, local, remote) {
|
|
1274
|
-
let documents;
|
|
1275
|
-
try {
|
|
1276
|
-
documents = [ancestor, local, remote].map(parseSettingsDocument);
|
|
1277
|
-
} catch {
|
|
1278
|
-
return { kind: "review", reason: "unsupported-format", fields: [] };
|
|
1279
|
-
}
|
|
1280
|
-
const [base, left, right] = documents;
|
|
1281
|
-
const selected = /* @__PURE__ */ new Map();
|
|
1282
|
-
const fields = new Set(documents.flatMap((document) => [...document.root.members.keys()]));
|
|
1283
|
-
const units = COUPLED_FIELDS.map((group) => [...group]);
|
|
1284
|
-
const coupled = new Set(units.flat());
|
|
1285
|
-
units.push(...[...fields].filter((field) => !coupled.has(field)).map((field) => [field]));
|
|
1286
|
-
const conflicts = [];
|
|
1287
|
-
for (const unit of units) {
|
|
1288
|
-
const equal = (first, second) => unit.every(
|
|
1289
|
-
(field) => jsonEqual(first.root.members.get(field)?.node.value, second.root.members.get(field)?.node.value)
|
|
1290
|
-
);
|
|
1291
|
-
const source = equal(left, right) ? left : equal(left, base) ? right : equal(right, base) ? left : void 0;
|
|
1292
|
-
if (!source) {
|
|
1293
|
-
conflicts.push(...unit.filter((field) => fields.has(field)));
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
for (const field of unit) {
|
|
1297
|
-
const member = source.root.members.get(field);
|
|
1298
|
-
if (member) selected.set(field, { document: source, node: member.node });
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
if (conflicts.length) return { kind: "review", reason: "field-conflict", fields: conflicts };
|
|
1302
|
-
try {
|
|
1303
|
-
return { kind: "merged", content: renderSettingsMembers(left, selected) };
|
|
1304
|
-
} catch {
|
|
1305
|
-
return { kind: "review", reason: "unsupported-format", fields: [] };
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// src/sync/settings-conflicts.ts
|
|
1021
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1310
1022
|
async function resolveSettingsConflicts(config, state, local, remote, plan) {
|
|
1311
1023
|
if (plan.kind !== "planned") return { plan, fields: [] };
|
|
1312
1024
|
const fields = [];
|
|
@@ -1336,7 +1048,7 @@ async function resolveSettingsConflicts(config, state, local, remote, plan) {
|
|
|
1336
1048
|
file: {
|
|
1337
1049
|
path: decision.path,
|
|
1338
1050
|
contentBase64: merged.content.toString("base64"),
|
|
1339
|
-
sha256:
|
|
1051
|
+
sha256: createHash3("sha256").update(merged.content).digest("hex")
|
|
1340
1052
|
}
|
|
1341
1053
|
};
|
|
1342
1054
|
}
|
|
@@ -1351,7 +1063,11 @@ function formatDiff(local, remote) {
|
|
|
1351
1063
|
const localMap = fileHashMap(local);
|
|
1352
1064
|
const remoteMap = fileHashMap(remote);
|
|
1353
1065
|
const allPaths = [.../* @__PURE__ */ new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
1354
|
-
const lines = [
|
|
1066
|
+
const lines = [
|
|
1067
|
+
`local: ${local.files.length} files`,
|
|
1068
|
+
`remote: ${safeTerminalText(remote.id)} (${remote.files.length} files)`,
|
|
1069
|
+
""
|
|
1070
|
+
];
|
|
1355
1071
|
let changed = 0;
|
|
1356
1072
|
for (const filePath of allPaths) {
|
|
1357
1073
|
if (!Object.hasOwn(localMap, filePath)) {
|
|
@@ -1369,76 +1085,87 @@ function formatDiff(local, remote) {
|
|
|
1369
1085
|
return lines.join("\n");
|
|
1370
1086
|
}
|
|
1371
1087
|
function formatSnapshotOnlyDiff(title, snapshot) {
|
|
1372
|
-
return [
|
|
1373
|
-
|
|
1374
|
-
|
|
1088
|
+
return [
|
|
1089
|
+
`${safeTerminalText(title)}: ${safeTerminalText(snapshot.id)}`,
|
|
1090
|
+
...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)
|
|
1091
|
+
].join("\n");
|
|
1375
1092
|
}
|
|
1376
1093
|
function formatPushSummary(config, destination, upload, head, preservedRemoteFileCount = 0, remote) {
|
|
1377
1094
|
return [
|
|
1378
1095
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1379
1096
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1380
|
-
`
|
|
1097
|
+
`Direction: Local \u2192 Remote. Source: ${safeTerminalText(agentDir())}.`,
|
|
1098
|
+
`Publish a snapshot containing ${upload.files.length} files; unchanged and preserved files are not counted as changes.`,
|
|
1381
1099
|
`Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1382
|
-
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
1383
|
-
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
1100
|
+
head ? `Remote latest: ${safeTerminalText(head.snapshotId)}` : "Remote latest: empty",
|
|
1101
|
+
"Publication effect: the backend's active head will reference the new immutable snapshot. Removed paths leave the active snapshot; retained history is not erased.",
|
|
1384
1102
|
...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
|
|
1385
1103
|
formatPublicationPreview(remote, upload),
|
|
1386
|
-
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."
|
|
1104
|
+
config.skipSecretScan ? "Warning: possible-secret scanning is disabled for this push." : 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."
|
|
1387
1105
|
].join("\n");
|
|
1388
1106
|
}
|
|
1389
|
-
function formatApplyPreview(local, remote) {
|
|
1390
|
-
return formatDirectionalChanges(
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1107
|
+
function formatApplyPreview(local, remote, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
|
|
1108
|
+
return formatDirectionalChanges(
|
|
1109
|
+
local,
|
|
1110
|
+
remote,
|
|
1111
|
+
{
|
|
1112
|
+
add: "Add locally",
|
|
1113
|
+
update: "Update locally",
|
|
1114
|
+
remove: "Remove locally"
|
|
1115
|
+
},
|
|
1116
|
+
protectedPaths,
|
|
1117
|
+
additionalRemovals
|
|
1118
|
+
);
|
|
1395
1119
|
}
|
|
1396
|
-
function formatPullSummary(config, destination, local, remote,
|
|
1120
|
+
function formatPullSummary(config, destination, local, remote, protectedPaths, additionalRemovals = []) {
|
|
1397
1121
|
return [
|
|
1398
1122
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1399
1123
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1124
|
+
"Direction: Remote \u2192 Local.",
|
|
1400
1125
|
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
1401
1126
|
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1402
|
-
`Protected live sessions: ${
|
|
1403
|
-
formatApplyPreview(local, remote),
|
|
1127
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
1128
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
1129
|
+
...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
|
|
1404
1130
|
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
|
|
1405
1131
|
].join("\n");
|
|
1406
1132
|
}
|
|
1407
|
-
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot,
|
|
1133
|
+
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedPaths, additionalRemovals = []) {
|
|
1408
1134
|
return [
|
|
1409
1135
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1410
1136
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1137
|
+
"Direction: Historical snapshot \u2192 Local and remote active head.",
|
|
1411
1138
|
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
1412
1139
|
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1413
|
-
`Protected live sessions: ${
|
|
1414
|
-
formatApplyPreview(local, remote),
|
|
1140
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
1141
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
1142
|
+
...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
|
|
1415
1143
|
"A local backup is created before applying; the backend's active head will change."
|
|
1416
1144
|
].join("\n");
|
|
1417
1145
|
}
|
|
1418
1146
|
function formatPublicationPreview(remote, upload) {
|
|
1419
|
-
if (!remote) {
|
|
1420
|
-
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
|
|
1421
|
-
"\n"
|
|
1422
|
-
);
|
|
1423
|
-
}
|
|
1424
1147
|
return formatDirectionalChanges(remote, upload, {
|
|
1425
1148
|
add: "Add remotely",
|
|
1426
1149
|
update: "Update remotely",
|
|
1427
1150
|
remove: "Remove remotely"
|
|
1428
1151
|
});
|
|
1429
1152
|
}
|
|
1430
|
-
function formatDirectionalChanges(before, after, labels) {
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
|
|
1153
|
+
function formatDirectionalChanges(before, after, labels, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
|
|
1154
|
+
const changes = transferChanges(before, after, protectedPaths);
|
|
1155
|
+
const known = new Set(changes.map((change) => change.path));
|
|
1156
|
+
for (const filePath of additionalRemovals) {
|
|
1157
|
+
if (!known.has(filePath) && !protectedPaths.has(filePath)) {
|
|
1158
|
+
changes.push({ path: filePath, action: "Delete" });
|
|
1159
|
+
known.add(filePath);
|
|
1160
|
+
}
|
|
1439
1161
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1162
|
+
const counts = transferCounts(changes);
|
|
1163
|
+
const label = { Add: labels.add, Update: labels.update, Delete: labels.remove };
|
|
1164
|
+
return [
|
|
1165
|
+
`Changes: ${counts.add} add \xB7 ${counts.update} update \xB7 ${counts.remove} remove`,
|
|
1166
|
+
...changes.map((change) => `${label[change.action]}: ${snapshotPathLabel(change.path)}`),
|
|
1167
|
+
...changes.length ? [] : ["No file changes."]
|
|
1168
|
+
].join("\n");
|
|
1442
1169
|
}
|
|
1443
1170
|
function countPreservedRemoteFiles(local, upload) {
|
|
1444
1171
|
const localPaths = new Set(local.files.map((file) => file.path));
|
|
@@ -1503,7 +1230,12 @@ function safeList(values) {
|
|
|
1503
1230
|
// src/sync/merged-sync.ts
|
|
1504
1231
|
var MAX_ATTEMPTS = 3;
|
|
1505
1232
|
async function mergeSync(ctx, options, factory = createSyncBackend, resolution) {
|
|
1506
|
-
const
|
|
1233
|
+
const timings = options.startupTimings ?? createStartupTimings();
|
|
1234
|
+
const sessionOwner = captureMutationOwner(ctx, options.signal);
|
|
1235
|
+
const validateOwner = (config2) => {
|
|
1236
|
+
sessionOwner();
|
|
1237
|
+
options.background?.validate(config2);
|
|
1238
|
+
};
|
|
1507
1239
|
const isCurrent = () => {
|
|
1508
1240
|
try {
|
|
1509
1241
|
validateOwner();
|
|
@@ -1513,7 +1245,9 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1513
1245
|
}
|
|
1514
1246
|
};
|
|
1515
1247
|
const config = await loadConfig(options.setup);
|
|
1248
|
+
validateOwner(config);
|
|
1516
1249
|
const configToken = syncCheckConfigFingerprint(config);
|
|
1250
|
+
const modelToken = JSON.stringify(ctx.model);
|
|
1517
1251
|
const sessionRoot = config.include.includes("sessions") ? await effectiveSessionRoot(ctx) : void 0;
|
|
1518
1252
|
validateOwner();
|
|
1519
1253
|
const snapshotOptions = {
|
|
@@ -1521,10 +1255,10 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1521
1255
|
sessionDir: sessionRoot,
|
|
1522
1256
|
signal: options.signal
|
|
1523
1257
|
};
|
|
1524
|
-
const validate = async () => {
|
|
1258
|
+
const validate = () => timings.measure("transfer authorization", async () => {
|
|
1525
1259
|
validateOwner();
|
|
1526
1260
|
const latest = await loadConfig(options.setup);
|
|
1527
|
-
validateOwner();
|
|
1261
|
+
validateOwner(latest);
|
|
1528
1262
|
if (configToken !== syncCheckConfigFingerprint(latest))
|
|
1529
1263
|
throw new Error("Sync setup or settings changed during transfer; reopen sync.");
|
|
1530
1264
|
if (sessionRoot !== void 0) {
|
|
@@ -1533,21 +1267,31 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1533
1267
|
if (currentRoot !== sessionRoot)
|
|
1534
1268
|
throw new Error("Session storage root changed during transfer; evidence retained for review.");
|
|
1535
1269
|
}
|
|
1536
|
-
if (
|
|
1270
|
+
if (config.agentMode && JSON.stringify(ctx.model) !== modelToken)
|
|
1271
|
+
throw new Error("Agent mode paused: selected model changed during transfer.");
|
|
1272
|
+
if (options.auto && (!startupTransferEnabled(latest) || !ctx.isIdle()))
|
|
1537
1273
|
throw new Error("Automatic transfer is no longer authorized at this idle boundary.");
|
|
1538
|
-
};
|
|
1274
|
+
});
|
|
1539
1275
|
await validate();
|
|
1540
|
-
const backend = await factory(config);
|
|
1276
|
+
const backend = await timings.measure("transfer backend initialization", () => factory(config));
|
|
1541
1277
|
await validate();
|
|
1542
|
-
|
|
1543
|
-
throw new Error(
|
|
1544
|
-
"Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Use manual sync."
|
|
1545
|
-
);
|
|
1546
|
-
}
|
|
1547
|
-
const pending = await readMergeJournal(config);
|
|
1278
|
+
const pending = await timings.measure("transfer journal read", () => readMergeJournal(config));
|
|
1548
1279
|
await validate();
|
|
1549
1280
|
if (pending) {
|
|
1550
|
-
const recovered = await
|
|
1281
|
+
const recovered = await timings.measure(
|
|
1282
|
+
"transfer journal recovery",
|
|
1283
|
+
() => completeJournal(
|
|
1284
|
+
ctx,
|
|
1285
|
+
config,
|
|
1286
|
+
backend,
|
|
1287
|
+
pending,
|
|
1288
|
+
validate,
|
|
1289
|
+
timings,
|
|
1290
|
+
options.signal,
|
|
1291
|
+
options.auto,
|
|
1292
|
+
options.background?.validate
|
|
1293
|
+
)
|
|
1294
|
+
);
|
|
1551
1295
|
if (!recovered) {
|
|
1552
1296
|
if ((options.auto || !options.silent) && !options.signal?.aborted)
|
|
1553
1297
|
ctx.ui.notify(
|
|
@@ -1560,24 +1304,51 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1560
1304
|
ctx.ui.notify("Recovered the committed merged transfer. Run sync again to inspect newer changes.", "info");
|
|
1561
1305
|
return "applied";
|
|
1562
1306
|
}
|
|
1307
|
+
if ((options.auto || config.agentMode) && backend.capability === "read-check-write-verify") {
|
|
1308
|
+
throw new Error(
|
|
1309
|
+
"Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Disable Agent mode and use manual sync."
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1563
1312
|
const textBudget = { remaining: TEXT_MERGE_WORK_LIMIT };
|
|
1564
1313
|
const createdArtifacts = [];
|
|
1565
1314
|
const ownedStatePath = statePathForConfig(config);
|
|
1566
1315
|
try {
|
|
1567
1316
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1568
1317
|
await validate();
|
|
1569
|
-
const state = await readStateForConfig(config);
|
|
1318
|
+
const state = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1570
1319
|
if (state.unresolved?.length && !config.partialSync)
|
|
1571
1320
|
throw new Error(
|
|
1572
1321
|
"Partial progress remains unresolved; re-enable partial sync or review an explicit force direction."
|
|
1573
1322
|
);
|
|
1574
|
-
const localRaw = await
|
|
1323
|
+
const localRaw = await timings.measure(
|
|
1324
|
+
"transfer local snapshot",
|
|
1325
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1326
|
+
);
|
|
1575
1327
|
const local = portableSnapshot(localRaw, config.localFields);
|
|
1576
1328
|
if (!sameLocalFields(state.localFields, config.localFields))
|
|
1577
1329
|
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
1578
|
-
const head = await backend.readHead(options.signal);
|
|
1330
|
+
const head = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1579
1331
|
await validate();
|
|
1580
|
-
|
|
1332
|
+
if (options.auto && !resolution && !config.partialSync && head && state.lastAppliedSnapshot && state.lastRemoteRevision && !state.unresolved?.length && head.snapshotId === state.lastAppliedSnapshot && backend.sameRevision(head.revision, state.lastRemoteRevision) && inspectRemoteSelection(config.include, { selection: head.selection, files: [] }).kind === "same" && matchesAcceptedState(state, config, head, local)) {
|
|
1333
|
+
const unchanged = await timings.measure("transfer unchanged verification", async () => {
|
|
1334
|
+
if (!await hasCurrentMergeBaseline(config, local, state)) return false;
|
|
1335
|
+
await validate();
|
|
1336
|
+
const currentLocal = await timings.measure(
|
|
1337
|
+
"transfer local snapshot",
|
|
1338
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1339
|
+
);
|
|
1340
|
+
await validate();
|
|
1341
|
+
const currentState = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1342
|
+
await validate();
|
|
1343
|
+
return sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) && syncStateFingerprint(currentState) === syncStateFingerprint(state);
|
|
1344
|
+
});
|
|
1345
|
+
await validate();
|
|
1346
|
+
if (unchanged) {
|
|
1347
|
+
timings.markUnchanged();
|
|
1348
|
+
return "applied";
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const rawRemote = head ? await timings.measure("transfer snapshot download", () => readSnapshotForHead(backend, head, options.signal)) : void 0;
|
|
1581
1352
|
await validate();
|
|
1582
1353
|
if (rawRemote) {
|
|
1583
1354
|
const files = [...local.files, ...rawRemote.files];
|
|
@@ -1590,12 +1361,16 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1590
1361
|
const remote = rawRemote ? portableSnapshot(filterSnapshotForConfigPolicy(rawRemote, config), rawRemote.localFields) : void 0;
|
|
1591
1362
|
if (rawRemote) {
|
|
1592
1363
|
requireCompatibleRemoteSelection(config, rawRemote);
|
|
1593
|
-
const validation =
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1364
|
+
const validation = await timings.measure(
|
|
1365
|
+
"transfer merge planning",
|
|
1366
|
+
() => planFileMerge({
|
|
1367
|
+
baseline: state.lastFileHashes,
|
|
1368
|
+
local: local.files,
|
|
1369
|
+
remote: rawRemote.files,
|
|
1370
|
+
selectionCompatible: true
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
await validate();
|
|
1599
1374
|
const managedPaths = /* @__PURE__ */ new Set([
|
|
1600
1375
|
...Object.keys(state.lastFileHashes),
|
|
1601
1376
|
...[...local.files, ...remote?.files ?? []].map((file) => file.path)
|
|
@@ -1622,30 +1397,35 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1622
1397
|
throw review("The established remote is missing; choose an explicit recovery direction.", "remote-empty");
|
|
1623
1398
|
const selectionState = inspectRemoteSelection(config.include, rawRemote);
|
|
1624
1399
|
const selectionCompatible = selectionState.kind === "same";
|
|
1625
|
-
let plan =
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1400
|
+
let plan = await timings.measure(
|
|
1401
|
+
"transfer merge planning",
|
|
1402
|
+
() => planFileMerge({
|
|
1403
|
+
baseline: state.lastFileHashes,
|
|
1404
|
+
local: local.files,
|
|
1405
|
+
remote: remote.files,
|
|
1406
|
+
selectionCompatible,
|
|
1407
|
+
protectedPaths: protectedSessionPaths(ctx, sessionRoot)
|
|
1408
|
+
})
|
|
1409
|
+
);
|
|
1410
|
+
await validate();
|
|
1632
1411
|
let resolved = { plan, fields: [] };
|
|
1633
1412
|
if (config.mergeSettings) {
|
|
1634
1413
|
try {
|
|
1635
|
-
resolved = await
|
|
1414
|
+
resolved = await timings.measure(
|
|
1415
|
+
"transfer settings merge",
|
|
1416
|
+
() => resolveSettingsConflicts(config, state, local, remote, plan)
|
|
1417
|
+
);
|
|
1636
1418
|
} catch (error) {
|
|
1637
1419
|
if (!config.partialSync) throw error;
|
|
1638
1420
|
}
|
|
1639
1421
|
}
|
|
1640
1422
|
await validate();
|
|
1641
|
-
plan = await
|
|
1642
|
-
|
|
1643
|
-
state,
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
protectedSessionPaths(ctx, sessionRoot),
|
|
1648
|
-
{ textBudget, signal: options.signal }
|
|
1423
|
+
plan = await timings.measure(
|
|
1424
|
+
"transfer content merge",
|
|
1425
|
+
() => resolveContentConflicts(config, state, local, remote, resolved.plan, protectedSessionPaths(ctx, sessionRoot), {
|
|
1426
|
+
textBudget,
|
|
1427
|
+
signal: options.signal
|
|
1428
|
+
})
|
|
1649
1429
|
);
|
|
1650
1430
|
await validate();
|
|
1651
1431
|
if (resolution) {
|
|
@@ -1662,7 +1442,30 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1662
1442
|
);
|
|
1663
1443
|
await validate();
|
|
1664
1444
|
}
|
|
1665
|
-
if (
|
|
1445
|
+
if (config.agentMode && !resolution) {
|
|
1446
|
+
const { resolveAgentPlan } = await import("./agent-plan-QCUJWBP4.ts");
|
|
1447
|
+
await validate();
|
|
1448
|
+
plan = await resolveAgentPlan(
|
|
1449
|
+
ctx,
|
|
1450
|
+
options,
|
|
1451
|
+
config,
|
|
1452
|
+
backend,
|
|
1453
|
+
state,
|
|
1454
|
+
local,
|
|
1455
|
+
localRaw,
|
|
1456
|
+
remote,
|
|
1457
|
+
head,
|
|
1458
|
+
plan,
|
|
1459
|
+
snapshotOptions,
|
|
1460
|
+
validate
|
|
1461
|
+
);
|
|
1462
|
+
await validate();
|
|
1463
|
+
if (plan.kind !== "planned" || plan.conflicts.length && !config.partialSync)
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
"Agent mode paused: unsupported or refused conflict groups remain; no transfer was performed."
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
if (plan.kind !== "planned" || plan.conflicts.length && (!config.partialSync || options.auto && !config.agentMode)) {
|
|
1666
1469
|
const decision = review(
|
|
1667
1470
|
selectionCompatible ? `Conflicting or protected paths require review; no merged transfer was performed. Use /sync diff and an explicit direction.${resolved.fields.length ? ` Settings fields: ${resolved.fields.map(safeTerminalText).join(", ")}` : ""}` : "Remote selection metadata is unavailable; review the included content and choose an explicit direction before merging.",
|
|
1668
1471
|
selectionCompatible ? "both-changed" : "remote-or-policy-changed"
|
|
@@ -1752,37 +1555,49 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1752
1555
|
const publish = !sameHashes(fileHashMap(publication), fileHashMap(remote)) || Boolean(config.partialSync && rawRemote.version !== 3);
|
|
1753
1556
|
const apply = !sameHashes(fileHashMap(localRaw), fileHashMap(after));
|
|
1754
1557
|
if (!publish && !apply) {
|
|
1755
|
-
const beforeEvidence = await backend.readHead(options.signal);
|
|
1558
|
+
const beforeEvidence = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1756
1559
|
await validate();
|
|
1757
1560
|
if (!beforeEvidence || !backend.sameRevision(beforeEvidence.revision, head.revision))
|
|
1758
1561
|
throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
|
|
1759
1562
|
progress = await persistProgress();
|
|
1760
1563
|
await validate();
|
|
1761
|
-
const currentLocal = await
|
|
1564
|
+
const currentLocal = await timings.measure(
|
|
1565
|
+
"transfer local snapshot",
|
|
1566
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1567
|
+
);
|
|
1762
1568
|
await validate();
|
|
1763
|
-
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
|
|
1569
|
+
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
|
|
1764
1570
|
throw new Error(
|
|
1765
1571
|
"Local content or baseline changed before baseline acceptance; retry from a fresh observation."
|
|
1766
1572
|
);
|
|
1767
1573
|
await validate();
|
|
1768
|
-
const currentHead = await backend.readHead(options.signal);
|
|
1574
|
+
const currentHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1769
1575
|
await validate();
|
|
1770
1576
|
if (!currentHead || !backend.sameRevision(currentHead.revision, head.revision))
|
|
1771
1577
|
throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
|
|
1772
|
-
await
|
|
1773
|
-
|
|
1774
|
-
config,
|
|
1775
|
-
|
|
1776
|
-
|
|
1578
|
+
await timings.measure(
|
|
1579
|
+
"transfer baseline write",
|
|
1580
|
+
() => writeAcceptedState(config, head, accepted, validate, progress)
|
|
1581
|
+
);
|
|
1582
|
+
await timings.measure(
|
|
1583
|
+
"transfer cleanup",
|
|
1584
|
+
() => pruneMergeBaselines(
|
|
1585
|
+
config,
|
|
1586
|
+
acceptedState(config, head, accepted, progress),
|
|
1587
|
+
captureMutationOwner(ctx, options.signal)
|
|
1588
|
+
)
|
|
1777
1589
|
);
|
|
1778
1590
|
await validate();
|
|
1779
|
-
await
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1591
|
+
await timings.measure(
|
|
1592
|
+
"transfer cleanup",
|
|
1593
|
+
() => pruneCompletedConflicts(
|
|
1594
|
+
config,
|
|
1595
|
+
backend.identity,
|
|
1596
|
+
acceptedState(config, head, accepted, progress),
|
|
1597
|
+
accepted,
|
|
1598
|
+
remote,
|
|
1599
|
+
captureMutationOwner(ctx, options.signal)
|
|
1600
|
+
)
|
|
1786
1601
|
);
|
|
1787
1602
|
await validate();
|
|
1788
1603
|
if (!options.silent)
|
|
@@ -1792,11 +1607,14 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1792
1607
|
);
|
|
1793
1608
|
return "applied";
|
|
1794
1609
|
}
|
|
1795
|
-
await
|
|
1610
|
+
await timings.measure(
|
|
1611
|
+
"transfer preflight",
|
|
1612
|
+
() => preflightMergedTargets(localRaw, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
|
|
1613
|
+
);
|
|
1796
1614
|
await validate();
|
|
1797
1615
|
if (!config.skipSecretScan && scanSnapshot(upload).length)
|
|
1798
1616
|
throw new Error("Refusing to merge possible secrets. Review managed content before syncing.");
|
|
1799
|
-
if (!options.yes) {
|
|
1617
|
+
if (!options.yes && !config.agentMode) {
|
|
1800
1618
|
const summary = mergedSyncSummary({
|
|
1801
1619
|
setupName: config.setupName,
|
|
1802
1620
|
destination: backend.destination,
|
|
@@ -1822,19 +1640,25 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1822
1640
|
return "cancelled";
|
|
1823
1641
|
}
|
|
1824
1642
|
await validate();
|
|
1825
|
-
const refreshed = await
|
|
1643
|
+
const refreshed = await timings.measure(
|
|
1644
|
+
"transfer local snapshot",
|
|
1645
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1646
|
+
);
|
|
1826
1647
|
if (!sameHashes(fileHashMap(refreshed), fileHashMap(localRaw)))
|
|
1827
1648
|
throw new Error("Local content changed during review; no merged transfer was performed.");
|
|
1828
|
-
if (syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
|
|
1649
|
+
if (syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
|
|
1829
1650
|
throw new Error("Sync baseline changed during review; retry from a fresh observation.");
|
|
1830
1651
|
await validate();
|
|
1831
|
-
const refreshedHead = await backend.readHead(options.signal);
|
|
1652
|
+
const refreshedHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1832
1653
|
await validate();
|
|
1833
1654
|
if (!refreshedHead || !backend.sameRevision(refreshedHead.revision, head.revision))
|
|
1834
1655
|
throw new Error("Remote changed during review; no merged transfer was performed.");
|
|
1835
1656
|
progress = await persistProgress();
|
|
1836
1657
|
await validate();
|
|
1837
|
-
const backup = await
|
|
1658
|
+
const backup = await timings.measure(
|
|
1659
|
+
"transfer backup",
|
|
1660
|
+
() => backupLocal(config.snapshotIdentity, snapshotOptions, options.signal)
|
|
1661
|
+
);
|
|
1838
1662
|
await validate();
|
|
1839
1663
|
const journal = {
|
|
1840
1664
|
version: 1,
|
|
@@ -1850,10 +1674,15 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1850
1674
|
...sessionRoot !== void 0 ? { sessionRoot } : {},
|
|
1851
1675
|
...!publish ? { committedHead: head, applyOnly: true } : {}
|
|
1852
1676
|
};
|
|
1853
|
-
await writeMergeJournal(config, journal);
|
|
1854
|
-
const atCommitState = syncStateFingerprint(
|
|
1677
|
+
await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
|
|
1678
|
+
const atCommitState = syncStateFingerprint(
|
|
1679
|
+
await timings.measure("transfer state read", () => readStateForConfig(config))
|
|
1680
|
+
);
|
|
1855
1681
|
await validate();
|
|
1856
|
-
const atCommit = await
|
|
1682
|
+
const atCommit = await timings.measure(
|
|
1683
|
+
"transfer local snapshot",
|
|
1684
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1685
|
+
);
|
|
1857
1686
|
await validate();
|
|
1858
1687
|
if (!sameHashes(fileHashMap(atCommit), fileHashMap(localRaw)) || atCommitState !== journal.stateIdentity) {
|
|
1859
1688
|
await validate();
|
|
@@ -1863,7 +1692,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1863
1692
|
);
|
|
1864
1693
|
}
|
|
1865
1694
|
try {
|
|
1866
|
-
await
|
|
1695
|
+
await timings.measure(
|
|
1696
|
+
"transfer preflight",
|
|
1697
|
+
() => preflightMergedTargets(atCommit, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
|
|
1698
|
+
);
|
|
1867
1699
|
await validate();
|
|
1868
1700
|
} catch (error) {
|
|
1869
1701
|
await validate();
|
|
@@ -1872,13 +1704,16 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1872
1704
|
}
|
|
1873
1705
|
try {
|
|
1874
1706
|
if (publish) {
|
|
1875
|
-
const result = await
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1707
|
+
const result = await timings.measure(
|
|
1708
|
+
"transfer remote publication",
|
|
1709
|
+
() => backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
1710
|
+
signal: options.signal,
|
|
1711
|
+
onCommit: options.onCommit
|
|
1712
|
+
})
|
|
1713
|
+
);
|
|
1879
1714
|
journal.committedHead = result.head;
|
|
1880
1715
|
journal.warnings = result.warnings;
|
|
1881
|
-
await writeMergeJournal(config, journal);
|
|
1716
|
+
await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
|
|
1882
1717
|
} else options.onCommit?.();
|
|
1883
1718
|
} catch (error) {
|
|
1884
1719
|
if (error instanceof SyncBackendConflictError && error.phase === "before-commit" && !error.candidateMayHaveBeenActive) {
|
|
@@ -1888,7 +1723,20 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1888
1723
|
}
|
|
1889
1724
|
throw mergeTransferError(error, config.backend);
|
|
1890
1725
|
}
|
|
1891
|
-
const completed = await
|
|
1726
|
+
const completed = await timings.measure(
|
|
1727
|
+
"transfer journal recovery",
|
|
1728
|
+
() => completeJournal(
|
|
1729
|
+
ctx,
|
|
1730
|
+
config,
|
|
1731
|
+
backend,
|
|
1732
|
+
journal,
|
|
1733
|
+
validate,
|
|
1734
|
+
timings,
|
|
1735
|
+
options.signal,
|
|
1736
|
+
options.auto,
|
|
1737
|
+
options.background?.validate
|
|
1738
|
+
)
|
|
1739
|
+
);
|
|
1892
1740
|
if (!completed) {
|
|
1893
1741
|
if (!options.signal?.aborted && (options.auto || !options.silent))
|
|
1894
1742
|
ctx.ui.notify(
|
|
@@ -1911,10 +1759,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1911
1759
|
return "applied";
|
|
1912
1760
|
}
|
|
1913
1761
|
} finally {
|
|
1914
|
-
await releaseConflictArtifacts(config, ownedStatePath, createdArtifacts);
|
|
1762
|
+
await timings.measure("transfer cleanup", () => releaseConflictArtifacts(config, ownedStatePath, createdArtifacts));
|
|
1915
1763
|
}
|
|
1916
1764
|
}
|
|
1917
|
-
async function completeJournal(ctx, config, backend, journal, validate, signal, auto = false) {
|
|
1765
|
+
async function completeJournal(ctx, config, backend, journal, validate, timings, signal, auto = false, validateBackground) {
|
|
1918
1766
|
if (journal.identity !== mergeJournalIdentity(config, backend.identity) || !sameLocalFields(journal.upload.localFields, config.localFields))
|
|
1919
1767
|
throw new Error(
|
|
1920
1768
|
"Merge journal belongs to a different setup, selection or local-field policy; preserve it for reviewed recovery."
|
|
@@ -1928,11 +1776,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1928
1776
|
);
|
|
1929
1777
|
}
|
|
1930
1778
|
const snapshotOptions = { ...snapshotOptionsForContext(ctx, config), sessionDir: sessionRoot, signal };
|
|
1931
|
-
const head = await backend.readHead(signal);
|
|
1779
|
+
const head = await timings.measure("transfer remote head", () => backend.readHead(signal));
|
|
1932
1780
|
if (!journal.committedHead && head && (backend.sameRevision(head.revision, journal.expectedHead.revision) || backend.matchesUncommittedRecoveryHead?.(head, journal.expectedHead))) {
|
|
1933
1781
|
await validate();
|
|
1934
|
-
const local = await
|
|
1935
|
-
|
|
1782
|
+
const local = await timings.measure(
|
|
1783
|
+
"transfer local snapshot",
|
|
1784
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1785
|
+
);
|
|
1786
|
+
if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== journal.stateIdentity) {
|
|
1936
1787
|
throw new Error(
|
|
1937
1788
|
"Uncertain publication is not active, but local content or baseline changed; keep the journal for reviewed recovery."
|
|
1938
1789
|
);
|
|
@@ -1942,7 +1793,7 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1942
1793
|
}
|
|
1943
1794
|
await validate();
|
|
1944
1795
|
if (journal.committedHead && (!head || !backend.sameRevision(head.revision, journal.committedHead.revision))) {
|
|
1945
|
-
const state2 = await readStateForConfig(config);
|
|
1796
|
+
const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1946
1797
|
await validate();
|
|
1947
1798
|
if (matchesAcceptedState(state2, config, journal.committedHead, journal.accepted ?? journal.after, journal.progress)) {
|
|
1948
1799
|
await clearMergeJournal(config);
|
|
@@ -1953,8 +1804,11 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1953
1804
|
}
|
|
1954
1805
|
}
|
|
1955
1806
|
if (journal.applyOnly && (!head || !backend.sameRevision(head.revision, journal.expectedHead.revision))) {
|
|
1956
|
-
const local = await
|
|
1957
|
-
|
|
1807
|
+
const local = await timings.measure(
|
|
1808
|
+
"transfer local snapshot",
|
|
1809
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1810
|
+
);
|
|
1811
|
+
const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1958
1812
|
await validate();
|
|
1959
1813
|
if (sameHashes(fileHashMap(local), fileHashMap(journal.before)) && syncStateFingerprint(state2) === journal.stateIdentity) {
|
|
1960
1814
|
await clearMergeJournal(config);
|
|
@@ -1967,11 +1821,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1967
1821
|
"Publication outcome cannot be reconciled with the current remote head. Journal and backup retained; do not retry publication or restore old bytes blindly."
|
|
1968
1822
|
);
|
|
1969
1823
|
}
|
|
1970
|
-
const observed = await
|
|
1824
|
+
const observed = await timings.measure(
|
|
1825
|
+
"transfer snapshot download",
|
|
1826
|
+
() => readSnapshotForHead(backend, head, signal)
|
|
1827
|
+
);
|
|
1971
1828
|
requireCompatibleRemoteSelection(config, observed);
|
|
1972
1829
|
if (!sameHashes(fileHashMap(observed), fileHashMap(journal.upload)) || JSON.stringify(observed.selection) !== JSON.stringify(journal.upload.selection))
|
|
1973
1830
|
throw new Error("Remote snapshot does not match the recorded merge publication.");
|
|
1974
|
-
const state = await readStateForConfig(config);
|
|
1831
|
+
const state = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1975
1832
|
if (matchesAcceptedState(state, config, head, journal.accepted ?? journal.after, journal.progress)) {
|
|
1976
1833
|
await validate();
|
|
1977
1834
|
await clearMergeJournal(config);
|
|
@@ -1992,38 +1849,51 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1992
1849
|
const validateOwner = captureMutationOwner(ctx, signal);
|
|
1993
1850
|
const validateMutation = () => {
|
|
1994
1851
|
validateOwner();
|
|
1852
|
+
validateBackground?.();
|
|
1995
1853
|
if (auto && !ctx.isIdle()) throw new Error("Automatic file apply is no longer idle; evidence retained.");
|
|
1996
1854
|
};
|
|
1997
|
-
await
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1855
|
+
await timings.measure(
|
|
1856
|
+
"transfer local apply",
|
|
1857
|
+
() => applyMergedSnapshot(
|
|
1858
|
+
journal.before,
|
|
1859
|
+
journal.after,
|
|
1860
|
+
protectedSessionPaths(ctx, sessionRoot),
|
|
1861
|
+
{ ...snapshotOptions, validateMutation },
|
|
1862
|
+
validate,
|
|
1863
|
+
ctx.sessionManager.getSessionFile?.()
|
|
1864
|
+
)
|
|
2004
1865
|
);
|
|
2005
1866
|
await validate();
|
|
2006
|
-
const headAtAcceptance = await backend.readHead(signal);
|
|
1867
|
+
const headAtAcceptance = await timings.measure("transfer remote head", () => backend.readHead(signal));
|
|
2007
1868
|
await validate();
|
|
2008
1869
|
if (!headAtAcceptance || !backend.sameRevision(headAtAcceptance.revision, head.revision))
|
|
2009
1870
|
throw new Error("Remote changed during merged apply; journal and backup retained for review.");
|
|
2010
|
-
await
|
|
1871
|
+
await timings.measure(
|
|
1872
|
+
"transfer baseline write",
|
|
1873
|
+
() => writeAcceptedState(config, head, journal.accepted ?? journal.after, validate, journal.progress)
|
|
1874
|
+
);
|
|
2011
1875
|
await validate();
|
|
2012
1876
|
await clearMergeJournal(config);
|
|
2013
1877
|
await validate();
|
|
2014
|
-
await
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
1878
|
+
await timings.measure(
|
|
1879
|
+
"transfer cleanup",
|
|
1880
|
+
() => pruneMergeBaselines(
|
|
1881
|
+
config,
|
|
1882
|
+
acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
|
|
1883
|
+
captureMutationOwner(ctx, signal)
|
|
1884
|
+
)
|
|
2018
1885
|
);
|
|
2019
1886
|
await validate();
|
|
2020
|
-
await
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
1887
|
+
await timings.measure(
|
|
1888
|
+
"transfer cleanup",
|
|
1889
|
+
() => pruneCompletedConflicts(
|
|
1890
|
+
config,
|
|
1891
|
+
backend.identity,
|
|
1892
|
+
acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
|
|
1893
|
+
journal.accepted ?? journal.after,
|
|
1894
|
+
journal.upload,
|
|
1895
|
+
captureMutationOwner(ctx, signal)
|
|
1896
|
+
)
|
|
2027
1897
|
);
|
|
2028
1898
|
await validate();
|
|
2029
1899
|
return true;
|
|
@@ -2055,8 +1925,8 @@ async function writeAcceptedState(config, head, snapshot, validate, progress) {
|
|
|
2055
1925
|
}
|
|
2056
1926
|
|
|
2057
1927
|
export {
|
|
2058
|
-
confirmMergeReview,
|
|
2059
1928
|
applySnapshot,
|
|
1929
|
+
addTopLevelCaseVariantDeletes,
|
|
2060
1930
|
formatDiff,
|
|
2061
1931
|
formatSnapshotOnlyDiff,
|
|
2062
1932
|
formatPushSummary,
|
|
@@ -2067,4 +1937,4 @@ export {
|
|
|
2067
1937
|
createSyncDecision,
|
|
2068
1938
|
mergeSync
|
|
2069
1939
|
};
|
|
2070
|
-
//# sourceMappingURL=chunk-
|
|
1940
|
+
//# sourceMappingURL=chunk-OTZRCKYP.ts.map
|