@narumitw/pi-sync 0.51.1 → 0.52.1
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-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-NLS2NV3K.ts +1746 -0
- package/dist/chunks/chunk-NLS2NV3K.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-4WPGCXFB.ts +175 -0
- package/dist/chunks/conflict-review-4WPGCXFB.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-RHNECOOG.ts} +309 -508
- package/dist/chunks/sync-operations-RHNECOOG.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 +428 -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,5 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
2
|
import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
|
|
5
3
|
import {
|
|
@@ -9,23 +7,21 @@ import {
|
|
|
9
7
|
type SyncBackend,
|
|
10
8
|
} from "../backends/sync-backend.js";
|
|
11
9
|
import type { CommandOptions } from "../commands/command-types.js";
|
|
12
|
-
import { loadConfig } from "../settings/config.js";
|
|
10
|
+
import { loadConfig, syncCheckConfigFingerprint } from "../settings/config.js";
|
|
13
11
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
14
|
-
import { sessionDirForApply,
|
|
12
|
+
import { sessionDirForApply, snapshotOptionsForContext } from "../snapshot/session-paths.js";
|
|
15
13
|
import {
|
|
16
14
|
createSnapshot,
|
|
17
15
|
filterSnapshotForConfigPolicy,
|
|
18
16
|
mergeRemotePreservedFiles,
|
|
19
17
|
regenerateSnapshotIdentity,
|
|
20
18
|
scanSnapshot,
|
|
21
|
-
sessionSnapshotPathFromAbsolute,
|
|
22
19
|
snapshotIncludesSessions,
|
|
23
20
|
snapshotWithoutSessions,
|
|
24
21
|
} from "../snapshot/snapshot.js";
|
|
25
22
|
import { applySnapshot } from "../snapshot/snapshot-apply.js";
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
import { stateDir } from "../state/state-directory.js";
|
|
23
|
+
import type { Snapshot } from "../snapshot/snapshot-types.js";
|
|
24
|
+
import { pruneMergeBaselines } from "../state/merge-baseline-store.js";
|
|
29
25
|
import type { SyncState } from "../state/state-types.js";
|
|
30
26
|
import { readStateForConfig, writeStateForConfig } from "../state/sync-state-store.js";
|
|
31
27
|
import {
|
|
@@ -35,10 +31,16 @@ import {
|
|
|
35
31
|
formatRollbackSummary,
|
|
36
32
|
} from "../ui/sync-format.js";
|
|
37
33
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
34
|
+
import { confirmFieldMigration } from "./field-migration.js";
|
|
35
|
+
import { overlayLocalFields, portableSnapshot, sameLocalFields, validatePortableSnapshot } from "./local-fields.js";
|
|
36
|
+
import { readMergeJournal, requireNoMergeJournal, retireMergeJournal } from "./merge-journal.js";
|
|
37
|
+
import { mergeSync } from "./merged-sync.js";
|
|
38
38
|
import { readRemoteSnapshot, readSnapshotForHead, requireCompatibleRemoteSelection } from "./remote-snapshot.js";
|
|
39
39
|
import { throwIfAborted } from "./signals.js";
|
|
40
40
|
import { createSyncDecision } from "./sync-decision.js";
|
|
41
41
|
import { errorMessage } from "./sync-errors.js";
|
|
42
|
+
|
|
43
|
+
import { backupLocal, captureMutationOwner, protectedSessionPaths } from "./sync-local.js";
|
|
42
44
|
import { inspectRemoteSelection } from "./sync-policy.js";
|
|
43
45
|
import {
|
|
44
46
|
canPullRemoteSessionsOnFirstSync,
|
|
@@ -57,6 +59,14 @@ import {
|
|
|
57
59
|
const VERSION = 1;
|
|
58
60
|
const POST_LOCAL_COMMIT_TIMEOUT_MS = 30_000;
|
|
59
61
|
|
|
62
|
+
/** Whole-direction acceptance must never forget withheld state without an explicit recovery choice. */
|
|
63
|
+
function requireResolvedDirectionalProgress(state: SyncState, options: CommandOptions, allowForce = true) {
|
|
64
|
+
if (state.unresolved?.length && (options.auto || !options.force || !allowForce))
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Unresolved partial conflicts remain. Review /sync conflicts or explicitly choose push --force or pull --force; automatic transfer and rollback cannot accept unresolved groups.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
export class PublicationStatePersistenceError extends Error {
|
|
61
71
|
readonly head: RemoteHead;
|
|
62
72
|
readonly backupPath?: string;
|
|
@@ -97,13 +107,20 @@ export async function push(
|
|
|
97
107
|
input?: PushInput,
|
|
98
108
|
factory: SyncBackendFactory = createSyncBackend,
|
|
99
109
|
) {
|
|
110
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
111
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
100
112
|
const config = input?.config ?? (await loadConfig(options.setup));
|
|
101
113
|
throwIfAborted(options.signal);
|
|
114
|
+
await requireNoMergeJournal(config, options);
|
|
115
|
+
const state = input?.state ?? (await readStateForConfig(config));
|
|
116
|
+
validateMutation();
|
|
117
|
+
requireResolvedDirectionalProgress(state, options);
|
|
102
118
|
setSyncStatus(ctx, `pushing ${config.setupName}`);
|
|
103
119
|
const backend = input?.backend ?? (await factory(config));
|
|
104
|
-
const state = input?.state ?? (await readStateForConfig(config));
|
|
105
120
|
throwIfAborted(options.signal);
|
|
106
|
-
const
|
|
121
|
+
const localRaw =
|
|
122
|
+
input?.local ?? (await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config)));
|
|
123
|
+
const local = portableSnapshot(localRaw, config.localFields);
|
|
107
124
|
throwIfAborted(options.signal);
|
|
108
125
|
|
|
109
126
|
let head = await backend.readHead(options.signal);
|
|
@@ -138,6 +155,13 @@ export async function push(
|
|
|
138
155
|
}
|
|
139
156
|
}
|
|
140
157
|
|
|
158
|
+
if (!remoteForUpload && head && config.localFields !== undefined)
|
|
159
|
+
remoteForUpload = await readSnapshotForHead(backend, head, options.signal);
|
|
160
|
+
if (!(await confirmFieldMigration(ctx, config, state, remoteForUpload, options.force, options.signal))) {
|
|
161
|
+
setSyncStatus(ctx, undefined);
|
|
162
|
+
return "cancelled" as const;
|
|
163
|
+
}
|
|
164
|
+
const configIdentity = syncCheckConfigFingerprint(config);
|
|
141
165
|
let upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
142
166
|
if (!config.skipSecretScan) {
|
|
143
167
|
const secrets = scanSnapshot(local);
|
|
@@ -154,7 +178,13 @@ export async function push(
|
|
|
154
178
|
const refreshedHead = await backend.readHead(options.signal);
|
|
155
179
|
if (!sameRemoteHead(backend, head, refreshedHead)) {
|
|
156
180
|
head = refreshedHead;
|
|
157
|
-
remoteForUpload = head ? await backend
|
|
181
|
+
remoteForUpload = head ? await readSnapshotForHead(backend, head, options.signal) : undefined;
|
|
182
|
+
validateMutation();
|
|
183
|
+
if (!(await confirmFieldMigration(ctx, config, state, remoteForUpload, options.force, options.signal))) {
|
|
184
|
+
setSyncStatus(ctx, undefined);
|
|
185
|
+
return "cancelled" as const;
|
|
186
|
+
}
|
|
187
|
+
validateMutation();
|
|
158
188
|
upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
159
189
|
if (
|
|
160
190
|
!(await confirmPush(
|
|
@@ -174,22 +204,42 @@ export async function push(
|
|
|
174
204
|
}
|
|
175
205
|
}
|
|
176
206
|
|
|
207
|
+
validateMutation();
|
|
208
|
+
if (configIdentity !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
209
|
+
throw new Error("Settings changed during push review.");
|
|
210
|
+
validateMutation();
|
|
211
|
+
if (config.localFields !== undefined || (await readMergeJournal(config))) {
|
|
212
|
+
const current = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
213
|
+
if (!sameHashes(fileHashMap(current), fileHashMap(localRaw)))
|
|
214
|
+
throw new Error("Local content changed during recovery review; retry from current bytes.");
|
|
215
|
+
}
|
|
216
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options);
|
|
217
|
+
validateMutation();
|
|
177
218
|
const result = await backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
178
219
|
signal: options.signal,
|
|
179
220
|
onCommit: options.onCommit,
|
|
180
221
|
});
|
|
181
222
|
try {
|
|
182
|
-
await writeStateForConfig(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
223
|
+
await writeStateForConfig(
|
|
224
|
+
config,
|
|
225
|
+
{
|
|
226
|
+
version: VERSION,
|
|
227
|
+
profile: config.snapshotIdentity,
|
|
228
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
229
|
+
lastRemoteRevision: result.head.revision,
|
|
230
|
+
lastFileHashes: fileHashMap(local),
|
|
231
|
+
include: [...config.include],
|
|
232
|
+
...(config.localFields !== undefined ? { localFields: config.localFields } : {}),
|
|
233
|
+
},
|
|
234
|
+
local,
|
|
235
|
+
validateCommittedOwner,
|
|
236
|
+
);
|
|
190
237
|
} catch (error) {
|
|
191
238
|
throw new PublicationStatePersistenceError(result.head, error);
|
|
192
239
|
}
|
|
240
|
+
await retireMergeJournal(config);
|
|
241
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
242
|
+
validateCommittedOwner();
|
|
193
243
|
if (options.signal?.aborted) return;
|
|
194
244
|
setSyncStatus(ctx, undefined);
|
|
195
245
|
if (!options.silent) {
|
|
@@ -211,12 +261,16 @@ export async function pull(
|
|
|
211
261
|
options: CommandOptions,
|
|
212
262
|
factory: SyncBackendFactory = createSyncBackend,
|
|
213
263
|
) {
|
|
264
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
265
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
214
266
|
const config = await loadConfig(options.setup);
|
|
215
267
|
throwIfAborted(options.signal);
|
|
268
|
+
await requireNoMergeJournal(config, options);
|
|
216
269
|
setSyncStatus(ctx, `pulling ${config.setupName}`);
|
|
217
270
|
const backend = await factory(config);
|
|
218
271
|
const state = await readStateForConfig(config);
|
|
219
|
-
|
|
272
|
+
validateMutation();
|
|
273
|
+
requireResolvedDirectionalProgress(state, options);
|
|
220
274
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
221
275
|
throwIfAborted(options.signal);
|
|
222
276
|
const { head, snapshot: remote } = await readRemoteSnapshot(backend, config, options.signal);
|
|
@@ -234,6 +288,19 @@ export async function pull(
|
|
|
234
288
|
});
|
|
235
289
|
}
|
|
236
290
|
|
|
291
|
+
if (!sameLocalFields(config.localFields, remote.localFields)) {
|
|
292
|
+
setSyncStatus(ctx, undefined);
|
|
293
|
+
throw new Error(
|
|
294
|
+
"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.",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (!(await confirmFieldMigration(ctx, config, state, remote, options.force, options.signal))) {
|
|
298
|
+
setSyncStatus(ctx, undefined);
|
|
299
|
+
return "cancelled" as const;
|
|
300
|
+
}
|
|
301
|
+
const configIdentity = syncCheckConfigFingerprint(config);
|
|
302
|
+
const logical = portableSnapshot(remote, config.localFields);
|
|
303
|
+
const physical = overlayLocalFields(logical, local, config.localFields);
|
|
237
304
|
const remoteChanged = hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx));
|
|
238
305
|
if (localChanged && remoteChanged && state.lastAppliedSnapshot && !options.force) {
|
|
239
306
|
throw createSyncDecision({
|
|
@@ -253,7 +320,7 @@ export async function pull(
|
|
|
253
320
|
!options.yes &&
|
|
254
321
|
!(await ctx.ui.confirm(
|
|
255
322
|
snapshotIncludesSessions(remote) ? "Pull pi settings and sessions?" : "Pull pi settings?",
|
|
256
|
-
formatPullSummary(config, backend.destination, local,
|
|
323
|
+
formatPullSummary(config, backend.destination, local, physical, protectedSessionPaths(ctx).size),
|
|
257
324
|
))
|
|
258
325
|
) {
|
|
259
326
|
setSyncStatus(ctx, undefined);
|
|
@@ -262,22 +329,63 @@ export async function pull(
|
|
|
262
329
|
}
|
|
263
330
|
|
|
264
331
|
throwIfAborted(options.signal);
|
|
332
|
+
const recoveringMerge = Boolean(await readMergeJournal(config));
|
|
333
|
+
const requireFreshRecovery = async () => {
|
|
334
|
+
validateMutation();
|
|
335
|
+
const currentHead = await backend.readHead(options.signal);
|
|
336
|
+
validateMutation();
|
|
337
|
+
const current = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
338
|
+
validateMutation();
|
|
339
|
+
if (!sameHashes(fileHashMap(current), fileHashMap(local)) || !sameRemoteHead(backend, head, currentHead)) {
|
|
340
|
+
throw new Error("Local or remote content changed during recovery review; retry from a fresh diff.");
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
if (recoveringMerge) await requireFreshRecovery();
|
|
344
|
+
if (configIdentity !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
345
|
+
throw new Error("Settings changed during pull review.");
|
|
346
|
+
validateMutation();
|
|
347
|
+
const refreshed = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
348
|
+
if (!sameHashes(fileHashMap(local), fileHashMap(refreshed)))
|
|
349
|
+
throw new Error("Local bytes changed during pull review.");
|
|
350
|
+
if (!sameRemoteHead(backend, head, await backend.readHead(options.signal)))
|
|
351
|
+
throw new Error("Remote changed during pull review.");
|
|
352
|
+
validateMutation();
|
|
265
353
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
266
|
-
const applySessionDir = await sessionDirForApply(ctx,
|
|
267
|
-
|
|
354
|
+
const applySessionDir = await sessionDirForApply(ctx, physical);
|
|
355
|
+
if (recoveringMerge) await requireFreshRecovery();
|
|
356
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options);
|
|
357
|
+
validateMutation();
|
|
268
358
|
options.onCommit?.();
|
|
269
|
-
const lastFileHashes = await applySnapshot(
|
|
359
|
+
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
270
360
|
include: config.include,
|
|
271
361
|
sessionDir: applySessionDir,
|
|
362
|
+
signal: options.signal,
|
|
363
|
+
validateMutation,
|
|
364
|
+
expectedFileHashes: config.localFields?.length
|
|
365
|
+
? { "settings.json": fileHashMap(local)["settings.json"] ?? null }
|
|
366
|
+
: undefined,
|
|
272
367
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
368
|
+
// Local installation is complete; user cancellation must not leave the baseline stale.
|
|
369
|
+
validateCommittedOwner();
|
|
370
|
+
await writeStateForConfig(
|
|
371
|
+
config,
|
|
372
|
+
{
|
|
373
|
+
version: VERSION,
|
|
374
|
+
profile: config.snapshotIdentity,
|
|
375
|
+
lastAppliedSnapshot: remote.id,
|
|
376
|
+
lastRemoteRevision: head?.revision,
|
|
377
|
+
lastFileHashes: config.localFields?.length
|
|
378
|
+
? { ...lastFileHashes, "settings.json": fileHashMap(logical)["settings.json"] }
|
|
379
|
+
: lastFileHashes,
|
|
380
|
+
include: [...config.include],
|
|
381
|
+
...(config.localFields !== undefined ? { localFields: config.localFields } : {}),
|
|
382
|
+
},
|
|
383
|
+
logical,
|
|
384
|
+
validateCommittedOwner,
|
|
385
|
+
);
|
|
386
|
+
await retireMergeJournal(config);
|
|
387
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
388
|
+
validateCommittedOwner();
|
|
281
389
|
if (options.signal?.aborted) return "applied" as const;
|
|
282
390
|
setSyncStatus(ctx, undefined);
|
|
283
391
|
if (!options.silent) {
|
|
@@ -288,7 +396,7 @@ export async function pull(
|
|
|
288
396
|
"warning",
|
|
289
397
|
);
|
|
290
398
|
}
|
|
291
|
-
if (options.reload) await maybeReload(ctx, options.signal);
|
|
399
|
+
if (options.reload) await maybeReload(ctx, options.signal, validateMutation);
|
|
292
400
|
return "applied" as const;
|
|
293
401
|
}
|
|
294
402
|
|
|
@@ -297,11 +405,16 @@ export async function syncBoth(
|
|
|
297
405
|
options: CommandOptions,
|
|
298
406
|
factory: SyncBackendFactory = createSyncBackend,
|
|
299
407
|
) {
|
|
408
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
300
409
|
const config = await loadConfig(options.setup);
|
|
301
410
|
throwIfAborted(options.signal);
|
|
302
|
-
|
|
411
|
+
if (config.include.length > 0 && options.auto && config.automaticTransfer) return mergeSync(ctx, options, factory);
|
|
412
|
+
if (!options.auto && config.include.length > 0 && (await readMergeJournal(config)))
|
|
413
|
+
return mergeSync(ctx, options, factory);
|
|
303
414
|
const state = await readStateForConfig(config);
|
|
304
415
|
throwIfAborted(options.signal);
|
|
416
|
+
if (!options.auto && config.include.length > 0 && state.lastAppliedSnapshot) return mergeSync(ctx, options, factory);
|
|
417
|
+
const backend = await factory(config);
|
|
305
418
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
306
419
|
throwIfAborted(options.signal);
|
|
307
420
|
if (config.include.length === 0) {
|
|
@@ -318,9 +431,35 @@ export async function syncBoth(
|
|
|
318
431
|
const localChanged = hasLocalChanges(local, state, config);
|
|
319
432
|
const remoteChanged = remote ? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx)) : false;
|
|
320
433
|
const firstSync = !state.lastAppliedSnapshot;
|
|
434
|
+
if (options.auto) throw new Error("Automatic transfer is not authorized by the current settings.");
|
|
321
435
|
|
|
436
|
+
const portableLocal = portableSnapshot(local, config.localFields);
|
|
437
|
+
const acceptMatchingState = async (snapshot: Snapshot) => {
|
|
438
|
+
requireCompatibleRemoteSelection(config, snapshot);
|
|
439
|
+
const accepted = portableSnapshot(snapshot, config.localFields);
|
|
440
|
+
if (state.lastAppliedSnapshot && !sameLocalFields(state.localFields, config.localFields))
|
|
441
|
+
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
442
|
+
await writeStateForConfig(
|
|
443
|
+
config,
|
|
444
|
+
{
|
|
445
|
+
version: VERSION,
|
|
446
|
+
profile: config.snapshotIdentity,
|
|
447
|
+
lastAppliedSnapshot: snapshot.id,
|
|
448
|
+
lastRemoteRevision: head?.revision,
|
|
449
|
+
lastFileHashes: fileHashMap(accepted),
|
|
450
|
+
include: [...config.include],
|
|
451
|
+
...(config.localFields !== undefined ? { localFields: config.localFields } : {}),
|
|
452
|
+
},
|
|
453
|
+
accepted,
|
|
454
|
+
validateMutation,
|
|
455
|
+
);
|
|
456
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateMutation);
|
|
457
|
+
validateMutation();
|
|
458
|
+
};
|
|
322
459
|
if (firstSync && remote && remote.files.length > 0 && local.files.length > 0) {
|
|
323
|
-
|
|
460
|
+
requireCompatibleRemoteSelection(config, remote);
|
|
461
|
+
const portableRemote = portableSnapshot(remote, config.localFields);
|
|
462
|
+
if (!canPullRemoteSettingsOnFirstSync(portableLocal, portableRemote)) {
|
|
324
463
|
throw createSyncDecision({
|
|
325
464
|
kind: "first-sync-settings-diverged",
|
|
326
465
|
config,
|
|
@@ -333,8 +472,8 @@ export async function syncBoth(
|
|
|
333
472
|
"Remote settings exist and this machine has different local Pi settings. Run /sync diff, then manually choose /sync pull or /sync push.",
|
|
334
473
|
});
|
|
335
474
|
}
|
|
336
|
-
if (!sameHashes(fileHashMap(
|
|
337
|
-
if (!canPullRemoteSessionsOnFirstSync(
|
|
475
|
+
if (!sameHashes(fileHashMap(portableLocal), fileHashMap(portableRemote))) {
|
|
476
|
+
if (!canPullRemoteSessionsOnFirstSync(portableLocal, portableRemote)) {
|
|
338
477
|
throw createSyncDecision({
|
|
339
478
|
kind: "first-sync-sessions-diverged",
|
|
340
479
|
config,
|
|
@@ -350,26 +489,18 @@ export async function syncBoth(
|
|
|
350
489
|
await pull(ctx, options, factory);
|
|
351
490
|
return;
|
|
352
491
|
}
|
|
353
|
-
await
|
|
354
|
-
version: VERSION,
|
|
355
|
-
profile: config.snapshotIdentity,
|
|
356
|
-
lastAppliedSnapshot: remote.id,
|
|
357
|
-
lastRemoteRevision: head?.revision,
|
|
358
|
-
lastFileHashes: fileHashMap(remote),
|
|
359
|
-
include: [...config.include],
|
|
360
|
-
});
|
|
492
|
+
await acceptMatchingState(remote);
|
|
361
493
|
if (!options.silent) ctx.ui.notify("pi-sync state initialized; local settings already match remote.", "info");
|
|
362
494
|
return;
|
|
363
495
|
}
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
});
|
|
496
|
+
if (
|
|
497
|
+
localChanged &&
|
|
498
|
+
remoteChanged &&
|
|
499
|
+
remote &&
|
|
500
|
+
snapshotsMatch(portableLocal, portableSnapshot(remote, remote.localFields))
|
|
501
|
+
) {
|
|
502
|
+
requireCompatibleRemoteSelection(config, remote);
|
|
503
|
+
await acceptMatchingState(remote);
|
|
373
504
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
374
505
|
return;
|
|
375
506
|
}
|
|
@@ -394,14 +525,7 @@ export async function syncBoth(
|
|
|
394
525
|
return;
|
|
395
526
|
}
|
|
396
527
|
if (shouldRefreshSyncedState(remote, head, state, config, (left, right) => backend.sameRevision(left, right))) {
|
|
397
|
-
await
|
|
398
|
-
version: VERSION,
|
|
399
|
-
profile: config.snapshotIdentity,
|
|
400
|
-
lastAppliedSnapshot: remote.id,
|
|
401
|
-
lastRemoteRevision: head?.revision,
|
|
402
|
-
lastFileHashes: fileHashMap(remote),
|
|
403
|
-
include: [...config.include],
|
|
404
|
-
});
|
|
528
|
+
await acceptMatchingState(remote);
|
|
405
529
|
}
|
|
406
530
|
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
407
531
|
}
|
|
@@ -412,6 +536,8 @@ export async function rollback(
|
|
|
412
536
|
factory: SyncBackendFactory = createSyncBackend,
|
|
413
537
|
expectedSelection?: { backendIdentity: string; setup?: string },
|
|
414
538
|
) {
|
|
539
|
+
const validateMutation = captureMutationOwner(ctx, options.signal);
|
|
540
|
+
const validateCommittedOwner = captureMutationOwner(ctx);
|
|
415
541
|
const target = options.args[0];
|
|
416
542
|
if (!target) throw new Error("Usage: /sync rollback <snapshot-id> [--yes]");
|
|
417
543
|
|
|
@@ -424,42 +550,72 @@ export async function rollback(
|
|
|
424
550
|
) {
|
|
425
551
|
throw new Error("Sync setup or storage location changed while history was open; reopen history and retry.");
|
|
426
552
|
}
|
|
553
|
+
// --force is only a rollback compatibility flag, not a reviewed merge-recovery direction.
|
|
554
|
+
await requireNoMergeJournal(config);
|
|
555
|
+
const state = await readStateForConfig(config);
|
|
556
|
+
validateMutation();
|
|
557
|
+
requireResolvedDirectionalProgress(state, options, false);
|
|
427
558
|
const decoded = await backend.readSnapshot(target, options.signal);
|
|
559
|
+
validatePortableSnapshot(decoded);
|
|
560
|
+
if (!sameLocalFields(decoded.localFields, config.localFields))
|
|
561
|
+
throw new Error(
|
|
562
|
+
"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.",
|
|
563
|
+
);
|
|
428
564
|
const selected = filterSnapshotForConfigPolicy(
|
|
429
565
|
config.include.includes("sessions") ? decoded : snapshotWithoutSessions(decoded),
|
|
430
566
|
config,
|
|
431
567
|
);
|
|
432
|
-
const remote = regenerateSnapshotIdentity(selected);
|
|
568
|
+
const remote = portableSnapshot(regenerateSnapshotIdentity(selected), config.localFields);
|
|
433
569
|
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
434
570
|
const expectedHead = await backend.readHead(options.signal);
|
|
435
|
-
|
|
571
|
+
const currentRemote = expectedHead ? await readSnapshotForHead(backend, expectedHead, options.signal) : undefined;
|
|
572
|
+
validateMutation();
|
|
573
|
+
if (!(await confirmFieldMigration(ctx, config, state, currentRemote, true, options.signal))) return;
|
|
574
|
+
validateMutation();
|
|
575
|
+
const physical = overlayLocalFields(remote, local, config.localFields);
|
|
436
576
|
|
|
437
577
|
if (
|
|
438
578
|
!options.yes &&
|
|
439
579
|
!(await ctx.ui.confirm(
|
|
440
580
|
snapshotIncludesSessions(remote) ? "Rollback pi settings and sessions?" : "Rollback pi settings?",
|
|
441
|
-
formatRollbackSummary(config, backend.destination, local,
|
|
581
|
+
formatRollbackSummary(config, backend.destination, local, physical, target, protectedSessionPaths(ctx).size),
|
|
442
582
|
))
|
|
443
583
|
) {
|
|
444
584
|
ctx.ui.notify("Rollback cancelled.", "info");
|
|
445
585
|
return;
|
|
446
586
|
}
|
|
447
587
|
|
|
588
|
+
validateMutation();
|
|
589
|
+
const currentLocal = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
590
|
+
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(local)))
|
|
591
|
+
throw new Error("Local bytes changed during rollback review.");
|
|
592
|
+
if (syncCheckConfigFingerprint(config) !== syncCheckConfigFingerprint(await loadConfig(options.setup)))
|
|
593
|
+
throw new Error("Settings changed during rollback review.");
|
|
594
|
+
if (!sameRemoteHead(backend, expectedHead, await backend.readHead(options.signal)))
|
|
595
|
+
throw new Error("Remote changed during rollback review.");
|
|
596
|
+
validateMutation();
|
|
448
597
|
throwIfAborted(options.signal);
|
|
449
598
|
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
450
599
|
const applySessionDir = await sessionDirForApply(ctx, remote);
|
|
451
|
-
|
|
600
|
+
requireResolvedDirectionalProgress(await readStateForConfig(config), options, false);
|
|
601
|
+
validateMutation();
|
|
452
602
|
options.onCommit?.();
|
|
453
|
-
const lastFileHashes = await applySnapshot(
|
|
603
|
+
const lastFileHashes = await applySnapshot(physical, protectedSessionPaths(ctx), {
|
|
454
604
|
include: config.include,
|
|
455
605
|
sessionDir: applySessionDir,
|
|
606
|
+
signal: options.signal,
|
|
607
|
+
validateMutation,
|
|
608
|
+
expectedFileHashes: config.localFields?.length
|
|
609
|
+
? { "settings.json": fileHashMap(local)["settings.json"] ?? null }
|
|
610
|
+
: undefined,
|
|
456
611
|
});
|
|
612
|
+
// The local transaction is retired; finish publication despite caller cancellation.
|
|
613
|
+
validateCommittedOwner();
|
|
457
614
|
let result: PublishSnapshotResult;
|
|
458
615
|
try {
|
|
459
616
|
const completionSignal = AbortSignal.timeout(POST_LOCAL_COMMIT_TIMEOUT_MS);
|
|
460
|
-
const upload = await snapshotForUpload(backend, config, remote, expectedHead,
|
|
461
|
-
|
|
462
|
-
});
|
|
617
|
+
const upload = await snapshotForUpload(backend, config, remote, expectedHead, currentRemote, completionSignal);
|
|
618
|
+
validateCommittedOwner();
|
|
463
619
|
result = await backend.publishSnapshot(upload, expectedRemoteHead(expectedHead), {
|
|
464
620
|
signal: completionSignal,
|
|
465
621
|
});
|
|
@@ -467,17 +623,28 @@ export async function rollback(
|
|
|
467
623
|
throw new RollbackPublicationError(backup, error);
|
|
468
624
|
}
|
|
469
625
|
try {
|
|
470
|
-
await writeStateForConfig(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
626
|
+
await writeStateForConfig(
|
|
627
|
+
config,
|
|
628
|
+
{
|
|
629
|
+
version: VERSION,
|
|
630
|
+
profile: config.snapshotIdentity,
|
|
631
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
632
|
+
lastRemoteRevision: result.head.revision,
|
|
633
|
+
lastFileHashes: config.localFields?.length
|
|
634
|
+
? { ...lastFileHashes, "settings.json": fileHashMap(remote)["settings.json"] }
|
|
635
|
+
: lastFileHashes,
|
|
636
|
+
include: [...config.include],
|
|
637
|
+
...(config.localFields !== undefined ? { localFields: config.localFields } : {}),
|
|
638
|
+
},
|
|
639
|
+
remote,
|
|
640
|
+
validateCommittedOwner,
|
|
641
|
+
);
|
|
478
642
|
} catch (error) {
|
|
479
643
|
throw new PublicationStatePersistenceError(result.head, error, backup);
|
|
480
644
|
}
|
|
645
|
+
await retireMergeJournal(config);
|
|
646
|
+
await pruneMergeBaselines(config, await readStateForConfig(config), validateCommittedOwner);
|
|
647
|
+
validateCommittedOwner();
|
|
481
648
|
if (options.signal?.aborted) return;
|
|
482
649
|
ctx.ui.notify(
|
|
483
650
|
[
|
|
@@ -488,21 +655,16 @@ export async function rollback(
|
|
|
488
655
|
.join("\n"),
|
|
489
656
|
result.warnings.length > 0 ? "warning" : "info",
|
|
490
657
|
);
|
|
491
|
-
await maybeReload(ctx, options.signal);
|
|
658
|
+
await maybeReload(ctx, options.signal, validateMutation);
|
|
492
659
|
}
|
|
493
660
|
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
? sessionSnapshotPathFromAbsolute(sessionFile, sessionDirFromContext(ctx))
|
|
500
|
-
: undefined;
|
|
501
|
-
return snapshotPath ? new Set([snapshotPath]) : new Set<string>();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
async function maybeReload(ctx: ExtensionCommandContext | ExtensionContext, signal?: AbortSignal) {
|
|
661
|
+
async function maybeReload(
|
|
662
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
663
|
+
signal?: AbortSignal,
|
|
664
|
+
validateMutation = captureMutationOwner(ctx, signal),
|
|
665
|
+
) {
|
|
505
666
|
if (signal?.aborted || !("reload" in ctx)) return;
|
|
667
|
+
validateMutation();
|
|
506
668
|
if (
|
|
507
669
|
ctx.hasUI &&
|
|
508
670
|
(await ctx.ui.confirm(
|
|
@@ -511,6 +673,7 @@ async function maybeReload(ctx: ExtensionCommandContext | ExtensionContext, sign
|
|
|
511
673
|
))
|
|
512
674
|
) {
|
|
513
675
|
if (signal?.aborted) return;
|
|
676
|
+
validateMutation();
|
|
514
677
|
await ctx.reload();
|
|
515
678
|
}
|
|
516
679
|
}
|
|
@@ -530,7 +693,7 @@ async function readRemoteSnapshotForUpload(
|
|
|
530
693
|
) {
|
|
531
694
|
return undefined;
|
|
532
695
|
}
|
|
533
|
-
return backend
|
|
696
|
+
return readSnapshotForHead(backend, head, signal);
|
|
534
697
|
}
|
|
535
698
|
|
|
536
699
|
async function snapshotForUpload(
|
|
@@ -540,18 +703,9 @@ async function snapshotForUpload(
|
|
|
540
703
|
head: RemoteHead | undefined,
|
|
541
704
|
remote?: Snapshot,
|
|
542
705
|
signal?: AbortSignal,
|
|
543
|
-
options: { ignoreUnreadableRemote?: boolean } = {},
|
|
544
706
|
) {
|
|
545
707
|
if (!head) return local;
|
|
546
|
-
|
|
547
|
-
if (!snapshot) {
|
|
548
|
-
try {
|
|
549
|
-
snapshot = await backend.readSnapshot(head.snapshotRef, signal);
|
|
550
|
-
} catch (error) {
|
|
551
|
-
if (options.ignoreUnreadableRemote) return local;
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
708
|
+
const snapshot = remote ?? (await readSnapshotForHead(backend, head, signal));
|
|
555
709
|
return mergeRemotePreservedFiles(local, snapshot, config);
|
|
556
710
|
}
|
|
557
711
|
|
|
@@ -584,16 +738,4 @@ function sameRemoteHead(backend: SyncBackend, left: RemoteHead | undefined, righ
|
|
|
584
738
|
return backend.sameRevision(left.revision, right.revision);
|
|
585
739
|
}
|
|
586
740
|
|
|
587
|
-
export
|
|
588
|
-
throwIfAborted(signal);
|
|
589
|
-
const snapshot = await createSnapshot(profile, options);
|
|
590
|
-
throwIfAborted(signal);
|
|
591
|
-
const backupDirectory = path.join(stateDir(), "backups");
|
|
592
|
-
await fs.mkdir(backupDirectory, { recursive: true });
|
|
593
|
-
throwIfAborted(signal);
|
|
594
|
-
const backupPath = path.join(backupDirectory, `${snapshot.id}.json.gz`);
|
|
595
|
-
const encoded = await encodeSnapshot(snapshot);
|
|
596
|
-
throwIfAborted(signal);
|
|
597
|
-
await fs.writeFile(backupPath, encoded, { signal });
|
|
598
|
-
return backupPath;
|
|
599
|
-
}
|
|
741
|
+
export { backupLocal } from "./sync-local.js";
|
package/src/sync/sync-queries.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { inspectLock, isLockGuardHeld, isStaleLock, withLock } from "../state/lo
|
|
|
10
10
|
import { formatDiff, formatSnapshotOnlyDiff, publicationCapabilityDescription } from "../ui/sync-format.js";
|
|
11
11
|
import { setSyncStatus } from "../ui/sync-status.js";
|
|
12
12
|
import { safeTerminalText } from "../ui/terminal-text.js";
|
|
13
|
+
import { portableSnapshot } from "./local-fields.js";
|
|
13
14
|
import { formatRemoteSelectionStatus, readRemoteSnapshot } from "./remote-snapshot.js";
|
|
14
15
|
import { throwIfAborted } from "./signals.js";
|
|
15
16
|
import { errorMessage } from "./sync-errors.js";
|
|
@@ -65,7 +66,10 @@ export async function diff(
|
|
|
65
66
|
throwIfAborted(options.signal);
|
|
66
67
|
setSyncStatus(ctx, `checking ${config.setupName}`);
|
|
67
68
|
const backend = await factory(config);
|
|
68
|
-
const local =
|
|
69
|
+
const local = portableSnapshot(
|
|
70
|
+
await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config)),
|
|
71
|
+
config.localFields,
|
|
72
|
+
);
|
|
69
73
|
throwIfAborted(options.signal);
|
|
70
74
|
const { snapshot: remote, selectionState } = await readRemoteSnapshot(backend, config, options.signal, {
|
|
71
75
|
allowSelectionDifference: true,
|
|
@@ -89,7 +93,7 @@ export async function diff(
|
|
|
89
93
|
return;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
ctx.ui.notify(`${header}\n\n${formatDiff(local, remote)}`, level);
|
|
96
|
+
ctx.ui.notify(`${header}\n\n${formatDiff(local, portableSnapshot(remote, remote.localFields))}`, level);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
export async function doctor(
|