@narumitw/pi-sync 0.50.3 → 0.51.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 +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +89 -101
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +197 -205
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
- package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
- package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
|
@@ -3,28 +3,24 @@ import path from "node:path";
|
|
|
3
3
|
import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
expectedRemoteHead,
|
|
7
|
+
type PublishSnapshotResult,
|
|
8
|
+
type RemoteHead,
|
|
9
|
+
type SyncBackend,
|
|
10
10
|
} from "../backends/sync-backend.js";
|
|
11
11
|
import type { CommandOptions } from "../commands/command-types.js";
|
|
12
12
|
import { loadConfig } from "../settings/config.js";
|
|
13
13
|
import type { AnySyncConfig } from "../settings/settings-types.js";
|
|
14
|
+
import { sessionDirForApply, sessionDirFromContext, snapshotOptionsForContext } from "../snapshot/session-paths.js";
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
regenerateSnapshotIdentity,
|
|
24
|
-
scanSnapshot,
|
|
25
|
-
sessionSnapshotPathFromAbsolute,
|
|
26
|
-
snapshotIncludesSessions,
|
|
27
|
-
snapshotWithoutSessions,
|
|
16
|
+
createSnapshot,
|
|
17
|
+
filterSnapshotForConfigPolicy,
|
|
18
|
+
mergeRemotePreservedFiles,
|
|
19
|
+
regenerateSnapshotIdentity,
|
|
20
|
+
scanSnapshot,
|
|
21
|
+
sessionSnapshotPathFromAbsolute,
|
|
22
|
+
snapshotIncludesSessions,
|
|
23
|
+
snapshotWithoutSessions,
|
|
28
24
|
} from "../snapshot/snapshot.js";
|
|
29
25
|
import { applySnapshot } from "../snapshot/snapshot-apply.js";
|
|
30
26
|
import { encodeSnapshot } from "../snapshot/snapshot-codec.js";
|
|
@@ -33,679 +29,571 @@ import { stateDir } from "../state/state-directory.js";
|
|
|
33
29
|
import type { SyncState } from "../state/state-types.js";
|
|
34
30
|
import { readStateForConfig, writeStateForConfig } from "../state/sync-state-store.js";
|
|
35
31
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
countPreservedRemoteFiles,
|
|
33
|
+
formatPullSummary,
|
|
34
|
+
formatPushSummary,
|
|
35
|
+
formatRollbackSummary,
|
|
40
36
|
} from "../ui/sync-format.js";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
readSnapshotForHead,
|
|
44
|
-
requireCompatibleRemoteSelection,
|
|
45
|
-
} from "./remote-snapshot.js";
|
|
37
|
+
import { setSyncStatus } from "../ui/sync-status.js";
|
|
38
|
+
import { readRemoteSnapshot, readSnapshotForHead, requireCompatibleRemoteSelection } from "./remote-snapshot.js";
|
|
46
39
|
import { throwIfAborted } from "./signals.js";
|
|
47
40
|
import { createSyncDecision } from "./sync-decision.js";
|
|
48
41
|
import { errorMessage } from "./sync-errors.js";
|
|
49
42
|
import { inspectRemoteSelection } from "./sync-policy.js";
|
|
50
43
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
canPullRemoteSessionsOnFirstSync,
|
|
45
|
+
canPullRemoteSettingsOnFirstSync,
|
|
46
|
+
fileHashMap,
|
|
47
|
+
hasLocalChanges,
|
|
48
|
+
hasRemoteChanges,
|
|
49
|
+
remoteChangedSinceState,
|
|
50
|
+
sameHashes,
|
|
51
|
+
shouldRefreshSyncedState,
|
|
52
|
+
snapshotHashesMatchState,
|
|
53
|
+
snapshotsMatch,
|
|
54
|
+
syncPolicyChanged,
|
|
62
55
|
} from "./sync-state.js";
|
|
63
56
|
|
|
64
|
-
const STATUS_KEY = "sync";
|
|
65
57
|
const VERSION = 1;
|
|
66
58
|
const POST_LOCAL_COMMIT_TIMEOUT_MS = 30_000;
|
|
67
59
|
|
|
68
60
|
export class PublicationStatePersistenceError extends Error {
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
readonly head: RemoteHead;
|
|
62
|
+
readonly backupPath?: string;
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
constructor(head: RemoteHead, cause: unknown, backupPath?: string) {
|
|
65
|
+
super(
|
|
66
|
+
`Remote publication ${head.snapshotId} is active, but local sync state could not be saved${backupPath ? `; local backup: ${backupPath}` : ""}: ${errorMessage(cause)}`,
|
|
67
|
+
{ cause },
|
|
68
|
+
);
|
|
69
|
+
this.name = "PublicationStatePersistenceError";
|
|
70
|
+
this.head = head;
|
|
71
|
+
this.backupPath = backupPath;
|
|
72
|
+
}
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
export class RollbackPublicationError extends Error {
|
|
84
|
-
|
|
76
|
+
readonly backupPath: string;
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
78
|
+
constructor(backupPath: string, cause: unknown) {
|
|
79
|
+
super(`Rollback applied locally with backup ${backupPath}, but remote publication failed: ${errorMessage(cause)}`, {
|
|
80
|
+
cause,
|
|
81
|
+
});
|
|
82
|
+
this.name = "RollbackPublicationError";
|
|
83
|
+
this.backupPath = backupPath;
|
|
84
|
+
}
|
|
94
85
|
}
|
|
95
86
|
|
|
96
87
|
interface PushInput {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
config: AnySyncConfig;
|
|
89
|
+
state: SyncState;
|
|
90
|
+
local: Snapshot;
|
|
91
|
+
backend?: SyncBackend;
|
|
101
92
|
}
|
|
102
93
|
|
|
103
94
|
export async function push(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
96
|
+
options: CommandOptions,
|
|
97
|
+
input?: PushInput,
|
|
98
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
108
99
|
) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
(await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config)));
|
|
118
|
-
throwIfAborted(options.signal);
|
|
100
|
+
const config = input?.config ?? (await loadConfig(options.setup));
|
|
101
|
+
throwIfAborted(options.signal);
|
|
102
|
+
setSyncStatus(ctx, `pushing ${config.setupName}`);
|
|
103
|
+
const backend = input?.backend ?? (await factory(config));
|
|
104
|
+
const state = input?.state ?? (await readStateForConfig(config));
|
|
105
|
+
throwIfAborted(options.signal);
|
|
106
|
+
const local = input?.local ?? (await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config)));
|
|
107
|
+
throwIfAborted(options.signal);
|
|
119
108
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
kind: head ? "remote-or-policy-changed" : "remote-empty",
|
|
152
|
-
config,
|
|
153
|
-
state,
|
|
154
|
-
local,
|
|
155
|
-
remote: remoteForConflict,
|
|
156
|
-
localChanged: hasLocalChanges(local, state, config),
|
|
157
|
-
remoteChanged: true,
|
|
158
|
-
directMessage:
|
|
159
|
-
"Remote or sync policy changed since last sync. Run /sync pull first or /sync push --force.",
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
109
|
+
let head = await backend.readHead(options.signal);
|
|
110
|
+
let remoteForUpload = await readRemoteSnapshotForUpload(backend, config, head, state, options.signal);
|
|
111
|
+
if (
|
|
112
|
+
!options.force &&
|
|
113
|
+
!remoteForUpload &&
|
|
114
|
+
head?.selection &&
|
|
115
|
+
inspectRemoteSelection(config.include, { selection: head.selection, files: [] }).kind === "different"
|
|
116
|
+
) {
|
|
117
|
+
remoteForUpload = await readSnapshotForHead(backend, head, options.signal);
|
|
118
|
+
}
|
|
119
|
+
if (remoteForUpload && !options.force) {
|
|
120
|
+
requireCompatibleRemoteSelection(config, remoteForUpload);
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
remoteChangedSinceState(head, state, config, (left, right) => backend.sameRevision(left, right)) &&
|
|
124
|
+
!options.force
|
|
125
|
+
) {
|
|
126
|
+
const remoteForConflict = remoteForUpload ? filterSnapshotForConfigPolicy(remoteForUpload, config) : undefined;
|
|
127
|
+
if (!remoteForConflict || !snapshotHashesMatchState(remoteForConflict, state, config)) {
|
|
128
|
+
throw createSyncDecision({
|
|
129
|
+
kind: head ? "remote-or-policy-changed" : "remote-empty",
|
|
130
|
+
config,
|
|
131
|
+
state,
|
|
132
|
+
local,
|
|
133
|
+
remote: remoteForConflict,
|
|
134
|
+
localChanged: hasLocalChanges(local, state, config),
|
|
135
|
+
remoteChanged: true,
|
|
136
|
+
directMessage: "Remote or sync policy changed since last sync. Run /sync pull first or /sync push --force.",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
163
140
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
if (!config.skipSecretScan) {
|
|
173
|
-
const secrets = scanSnapshot(local);
|
|
174
|
-
if (secrets.length > 0) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Refusing to push possible secrets:\n${secrets.map((s) => `- ${s}`).join("\n")}`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
141
|
+
let upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
142
|
+
if (!config.skipSecretScan) {
|
|
143
|
+
const secrets = scanSnapshot(local);
|
|
144
|
+
if (secrets.length > 0) {
|
|
145
|
+
throw new Error(`Refusing to push possible secrets:\n${secrets.map((s) => `- ${s}`).join("\n")}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
180
148
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
if (!(await confirmPush(ctx, options, config, backend, local, upload, head, remoteForUpload))) {
|
|
150
|
+
return "cancelled" as const;
|
|
151
|
+
}
|
|
184
152
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
head,
|
|
209
|
-
remoteForUpload,
|
|
210
|
-
"Remote changed during review. Push the refreshed plan?",
|
|
211
|
-
))
|
|
212
|
-
) {
|
|
213
|
-
return "cancelled" as const;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
153
|
+
if (options.force) {
|
|
154
|
+
const refreshedHead = await backend.readHead(options.signal);
|
|
155
|
+
if (!sameRemoteHead(backend, head, refreshedHead)) {
|
|
156
|
+
head = refreshedHead;
|
|
157
|
+
remoteForUpload = head ? await backend.readSnapshot(head.snapshotRef, options.signal) : undefined;
|
|
158
|
+
upload = await snapshotForUpload(backend, config, local, head, remoteForUpload, options.signal);
|
|
159
|
+
if (
|
|
160
|
+
!(await confirmPush(
|
|
161
|
+
ctx,
|
|
162
|
+
options,
|
|
163
|
+
config,
|
|
164
|
+
backend,
|
|
165
|
+
local,
|
|
166
|
+
upload,
|
|
167
|
+
head,
|
|
168
|
+
remoteForUpload,
|
|
169
|
+
"Remote changed during review. Push the refreshed plan?",
|
|
170
|
+
))
|
|
171
|
+
) {
|
|
172
|
+
return "cancelled" as const;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
217
176
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
177
|
+
const result = await backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
178
|
+
signal: options.signal,
|
|
179
|
+
onCommit: options.onCommit,
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
await writeStateForConfig(config, {
|
|
183
|
+
version: VERSION,
|
|
184
|
+
profile: config.snapshotIdentity,
|
|
185
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
186
|
+
lastRemoteRevision: result.head.revision,
|
|
187
|
+
lastFileHashes: fileHashMap(local),
|
|
188
|
+
include: [...config.include],
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new PublicationStatePersistenceError(result.head, error);
|
|
192
|
+
}
|
|
193
|
+
if (options.signal?.aborted) return;
|
|
194
|
+
setSyncStatus(ctx, undefined);
|
|
195
|
+
if (!options.silent) {
|
|
196
|
+
ctx.ui.notify(
|
|
197
|
+
[
|
|
198
|
+
`Pushed ${upload.files.length} files from sync setup “${config.setupName}” as ${result.head.snapshotId}.`,
|
|
199
|
+
...result.warnings,
|
|
200
|
+
]
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.join("\n"),
|
|
203
|
+
result.warnings.length > 0 ? "warning" : "info",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return "applied" as const;
|
|
248
207
|
}
|
|
249
208
|
|
|
250
209
|
export async function pull(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
210
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
211
|
+
options: CommandOptions,
|
|
212
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
254
213
|
) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
directMessage: "Remote is empty. Run /sync push from a configured machine first.",
|
|
278
|
-
});
|
|
279
|
-
}
|
|
214
|
+
const config = await loadConfig(options.setup);
|
|
215
|
+
throwIfAborted(options.signal);
|
|
216
|
+
setSyncStatus(ctx, `pulling ${config.setupName}`);
|
|
217
|
+
const backend = await factory(config);
|
|
218
|
+
const state = await readStateForConfig(config);
|
|
219
|
+
throwIfAborted(options.signal);
|
|
220
|
+
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
221
|
+
throwIfAborted(options.signal);
|
|
222
|
+
const { head, snapshot: remote } = await readRemoteSnapshot(backend, config, options.signal);
|
|
223
|
+
throwIfAborted(options.signal);
|
|
224
|
+
const localChanged = hasLocalChanges(local, state, config);
|
|
225
|
+
if (!remote) {
|
|
226
|
+
throw createSyncDecision({
|
|
227
|
+
kind: "remote-empty",
|
|
228
|
+
config,
|
|
229
|
+
state,
|
|
230
|
+
local,
|
|
231
|
+
localChanged,
|
|
232
|
+
remoteChanged: false,
|
|
233
|
+
directMessage: "Remote is empty. Run /sync push from a configured machine first.",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
280
236
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
237
|
+
const remoteChanged = hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx));
|
|
238
|
+
if (localChanged && remoteChanged && state.lastAppliedSnapshot && !options.force) {
|
|
239
|
+
throw createSyncDecision({
|
|
240
|
+
kind: "both-changed",
|
|
241
|
+
config,
|
|
242
|
+
state,
|
|
243
|
+
local,
|
|
244
|
+
remote,
|
|
245
|
+
localChanged,
|
|
246
|
+
remoteChanged,
|
|
247
|
+
directMessage:
|
|
248
|
+
"Both local and remote changed since last sync. Run /sync diff, then choose /sync pull --force or /sync push --force.",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
295
251
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
))
|
|
308
|
-
) {
|
|
309
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
310
|
-
ctx.ui.notify("Pull cancelled.", "info");
|
|
311
|
-
return "cancelled" as const;
|
|
312
|
-
}
|
|
252
|
+
if (
|
|
253
|
+
!options.yes &&
|
|
254
|
+
!(await ctx.ui.confirm(
|
|
255
|
+
snapshotIncludesSessions(remote) ? "Pull pi settings and sessions?" : "Pull pi settings?",
|
|
256
|
+
formatPullSummary(config, backend.destination, local, remote, protectedSessionPaths(ctx).size),
|
|
257
|
+
))
|
|
258
|
+
) {
|
|
259
|
+
setSyncStatus(ctx, undefined);
|
|
260
|
+
ctx.ui.notify("Pull cancelled.", "info");
|
|
261
|
+
return "cancelled" as const;
|
|
262
|
+
}
|
|
313
263
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
options.auto &&
|
|
344
|
-
config.include.includes("sessions") &&
|
|
345
|
-
snapshotIncludesSessions(remote)
|
|
346
|
-
) {
|
|
347
|
-
ctx.ui.notify(
|
|
348
|
-
"Pulled Pi sessions after startup selected the current session. Restart Pi or resume a pulled session to use newly synced conversations.",
|
|
349
|
-
"warning",
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
if (options.reload) await maybeReload(ctx, options.signal);
|
|
353
|
-
return "applied" as const;
|
|
264
|
+
throwIfAborted(options.signal);
|
|
265
|
+
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
266
|
+
const applySessionDir = await sessionDirForApply(ctx, remote);
|
|
267
|
+
throwIfAborted(options.signal);
|
|
268
|
+
options.onCommit?.();
|
|
269
|
+
const lastFileHashes = await applySnapshot(remote, protectedSessionPaths(ctx), {
|
|
270
|
+
include: config.include,
|
|
271
|
+
sessionDir: applySessionDir,
|
|
272
|
+
});
|
|
273
|
+
await writeStateForConfig(config, {
|
|
274
|
+
version: VERSION,
|
|
275
|
+
profile: config.snapshotIdentity,
|
|
276
|
+
lastAppliedSnapshot: remote.id,
|
|
277
|
+
lastRemoteRevision: head?.revision,
|
|
278
|
+
lastFileHashes,
|
|
279
|
+
include: [...config.include],
|
|
280
|
+
});
|
|
281
|
+
if (options.signal?.aborted) return "applied" as const;
|
|
282
|
+
setSyncStatus(ctx, undefined);
|
|
283
|
+
if (!options.silent) {
|
|
284
|
+
ctx.ui.notify(`Pulled ${remote.files.length} files from ${remote.id}. Backup: ${backup}`, "info");
|
|
285
|
+
} else if (options.auto && config.include.includes("sessions") && snapshotIncludesSessions(remote)) {
|
|
286
|
+
ctx.ui.notify(
|
|
287
|
+
"Pulled Pi sessions after startup selected the current session. Restart Pi or resume a pulled session to use newly synced conversations.",
|
|
288
|
+
"warning",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (options.reload) await maybeReload(ctx, options.signal);
|
|
292
|
+
return "applied" as const;
|
|
354
293
|
}
|
|
355
294
|
|
|
356
295
|
export async function syncBoth(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
296
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
297
|
+
options: CommandOptions,
|
|
298
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
360
299
|
) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const localChanged = hasLocalChanges(local, state, config);
|
|
383
|
-
const remoteChanged = remote
|
|
384
|
-
? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx))
|
|
385
|
-
: false;
|
|
386
|
-
const firstSync = !state.lastAppliedSnapshot;
|
|
300
|
+
const config = await loadConfig(options.setup);
|
|
301
|
+
throwIfAborted(options.signal);
|
|
302
|
+
const backend = await factory(config);
|
|
303
|
+
const state = await readStateForConfig(config);
|
|
304
|
+
throwIfAborted(options.signal);
|
|
305
|
+
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
306
|
+
throwIfAborted(options.signal);
|
|
307
|
+
if (config.include.length === 0) {
|
|
308
|
+
if (!options.silent) {
|
|
309
|
+
ctx.ui.notify(
|
|
310
|
+
`Sync setup “${config.setupName}” includes no files. Choose included content in /sync Settings before syncing.`,
|
|
311
|
+
"warning",
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const { head, snapshot: remote } = await readRemoteSnapshot(backend, config, options.signal);
|
|
317
|
+
throwIfAborted(options.signal);
|
|
318
|
+
const localChanged = hasLocalChanges(local, state, config);
|
|
319
|
+
const remoteChanged = remote ? hasRemoteChanges(remote, state, config, protectedSessionPaths(ctx)) : false;
|
|
320
|
+
const firstSync = !state.lastAppliedSnapshot;
|
|
387
321
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
lastRemoteRevision: head?.revision,
|
|
474
|
-
lastFileHashes: fileHashMap(remote),
|
|
475
|
-
include: [...config.include],
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
322
|
+
if (firstSync && remote && remote.files.length > 0 && local.files.length > 0) {
|
|
323
|
+
if (!canPullRemoteSettingsOnFirstSync(local, remote)) {
|
|
324
|
+
throw createSyncDecision({
|
|
325
|
+
kind: "first-sync-settings-diverged",
|
|
326
|
+
config,
|
|
327
|
+
state,
|
|
328
|
+
local,
|
|
329
|
+
remote,
|
|
330
|
+
localChanged: true,
|
|
331
|
+
remoteChanged: true,
|
|
332
|
+
directMessage:
|
|
333
|
+
"Remote settings exist and this machine has different local Pi settings. Run /sync diff, then manually choose /sync pull or /sync push.",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (!sameHashes(fileHashMap(local), fileHashMap(remote))) {
|
|
337
|
+
if (!canPullRemoteSessionsOnFirstSync(local, remote)) {
|
|
338
|
+
throw createSyncDecision({
|
|
339
|
+
kind: "first-sync-sessions-diverged",
|
|
340
|
+
config,
|
|
341
|
+
state,
|
|
342
|
+
local,
|
|
343
|
+
remote,
|
|
344
|
+
localChanged: true,
|
|
345
|
+
remoteChanged: true,
|
|
346
|
+
directMessage:
|
|
347
|
+
"Remote settings match, but local and remote Pi sessions differ. Run /sync diff, then manually choose /sync pull or /sync push.",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
await pull(ctx, options, factory);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
await writeStateForConfig(config, {
|
|
354
|
+
version: VERSION,
|
|
355
|
+
profile: config.snapshotIdentity,
|
|
356
|
+
lastAppliedSnapshot: remote.id,
|
|
357
|
+
lastRemoteRevision: head?.revision,
|
|
358
|
+
lastFileHashes: fileHashMap(remote),
|
|
359
|
+
include: [...config.include],
|
|
360
|
+
});
|
|
361
|
+
if (!options.silent) ctx.ui.notify("pi-sync state initialized; local settings already match remote.", "info");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (localChanged && remoteChanged && remote && snapshotsMatch(local, remote)) {
|
|
365
|
+
await writeStateForConfig(config, {
|
|
366
|
+
version: VERSION,
|
|
367
|
+
profile: config.snapshotIdentity,
|
|
368
|
+
lastAppliedSnapshot: remote.id,
|
|
369
|
+
lastRemoteRevision: head?.revision,
|
|
370
|
+
lastFileHashes: fileHashMap(remote),
|
|
371
|
+
include: [...config.include],
|
|
372
|
+
});
|
|
373
|
+
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (localChanged && remoteChanged && state.lastAppliedSnapshot) {
|
|
377
|
+
throw createSyncDecision({
|
|
378
|
+
kind: "both-changed",
|
|
379
|
+
config,
|
|
380
|
+
state,
|
|
381
|
+
local,
|
|
382
|
+
remote,
|
|
383
|
+
localChanged,
|
|
384
|
+
remoteChanged,
|
|
385
|
+
directMessage: "Both local and remote changed. Run /sync diff and resolve with push --force or pull --force.",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (remoteChanged) {
|
|
389
|
+
await pull(ctx, options, factory);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (localChanged || !remote) {
|
|
393
|
+
await push(ctx, options, undefined, factory);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (shouldRefreshSyncedState(remote, head, state, config, (left, right) => backend.sameRevision(left, right))) {
|
|
397
|
+
await writeStateForConfig(config, {
|
|
398
|
+
version: VERSION,
|
|
399
|
+
profile: config.snapshotIdentity,
|
|
400
|
+
lastAppliedSnapshot: remote.id,
|
|
401
|
+
lastRemoteRevision: head?.revision,
|
|
402
|
+
lastFileHashes: fileHashMap(remote),
|
|
403
|
+
include: [...config.include],
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (!options.silent) ctx.ui.notify("pi-sync is already up to date.", "info");
|
|
479
407
|
}
|
|
480
408
|
|
|
481
409
|
export async function rollback(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
410
|
+
ctx: ExtensionCommandContext,
|
|
411
|
+
options: CommandOptions,
|
|
412
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
413
|
+
expectedSelection?: { backendIdentity: string; setup?: string },
|
|
486
414
|
) {
|
|
487
|
-
|
|
488
|
-
|
|
415
|
+
const target = options.args[0];
|
|
416
|
+
if (!target) throw new Error("Usage: /sync rollback <snapshot-id> [--yes]");
|
|
489
417
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const local = await createSnapshot(
|
|
509
|
-
config.snapshotIdentity,
|
|
510
|
-
snapshotOptionsForContext(ctx, config),
|
|
511
|
-
);
|
|
512
|
-
const expectedHead = await backend.readHead(options.signal);
|
|
513
|
-
throwIfAborted(options.signal);
|
|
418
|
+
const config = await loadConfig(options.setup);
|
|
419
|
+
throwIfAborted(options.signal);
|
|
420
|
+
const backend = await factory(config);
|
|
421
|
+
if (
|
|
422
|
+
expectedSelection &&
|
|
423
|
+
(backend.identity !== expectedSelection.backendIdentity || config.setupName !== expectedSelection.setup)
|
|
424
|
+
) {
|
|
425
|
+
throw new Error("Sync setup or storage location changed while history was open; reopen history and retry.");
|
|
426
|
+
}
|
|
427
|
+
const decoded = await backend.readSnapshot(target, options.signal);
|
|
428
|
+
const selected = filterSnapshotForConfigPolicy(
|
|
429
|
+
config.include.includes("sessions") ? decoded : snapshotWithoutSessions(decoded),
|
|
430
|
+
config,
|
|
431
|
+
);
|
|
432
|
+
const remote = regenerateSnapshotIdentity(selected);
|
|
433
|
+
const local = await createSnapshot(config.snapshotIdentity, snapshotOptionsForContext(ctx, config));
|
|
434
|
+
const expectedHead = await backend.readHead(options.signal);
|
|
435
|
+
throwIfAborted(options.signal);
|
|
514
436
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
remote,
|
|
526
|
-
target,
|
|
527
|
-
protectedSessionPaths(ctx).size,
|
|
528
|
-
),
|
|
529
|
-
))
|
|
530
|
-
) {
|
|
531
|
-
ctx.ui.notify("Rollback cancelled.", "info");
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
437
|
+
if (
|
|
438
|
+
!options.yes &&
|
|
439
|
+
!(await ctx.ui.confirm(
|
|
440
|
+
snapshotIncludesSessions(remote) ? "Rollback pi settings and sessions?" : "Rollback pi settings?",
|
|
441
|
+
formatRollbackSummary(config, backend.destination, local, remote, target, protectedSessionPaths(ctx).size),
|
|
442
|
+
))
|
|
443
|
+
) {
|
|
444
|
+
ctx.ui.notify("Rollback cancelled.", "info");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
534
447
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
ctx.ui.notify(
|
|
580
|
-
[
|
|
581
|
-
`Rolled back sync setup “${config.setupName}” to ${target}; latest: ${result.head.snapshotId}. Backup: ${backup}`,
|
|
582
|
-
...result.warnings,
|
|
583
|
-
]
|
|
584
|
-
.filter(Boolean)
|
|
585
|
-
.join("\n"),
|
|
586
|
-
result.warnings.length > 0 ? "warning" : "info",
|
|
587
|
-
);
|
|
588
|
-
await maybeReload(ctx, options.signal);
|
|
448
|
+
throwIfAborted(options.signal);
|
|
449
|
+
const backup = await backupLocal(config.snapshotIdentity, snapshotOptionsForContext(ctx, config), options.signal);
|
|
450
|
+
const applySessionDir = await sessionDirForApply(ctx, remote);
|
|
451
|
+
throwIfAborted(options.signal);
|
|
452
|
+
options.onCommit?.();
|
|
453
|
+
const lastFileHashes = await applySnapshot(remote, protectedSessionPaths(ctx), {
|
|
454
|
+
include: config.include,
|
|
455
|
+
sessionDir: applySessionDir,
|
|
456
|
+
});
|
|
457
|
+
let result: PublishSnapshotResult;
|
|
458
|
+
try {
|
|
459
|
+
const completionSignal = AbortSignal.timeout(POST_LOCAL_COMMIT_TIMEOUT_MS);
|
|
460
|
+
const upload = await snapshotForUpload(backend, config, remote, expectedHead, undefined, completionSignal, {
|
|
461
|
+
ignoreUnreadableRemote: true,
|
|
462
|
+
});
|
|
463
|
+
result = await backend.publishSnapshot(upload, expectedRemoteHead(expectedHead), {
|
|
464
|
+
signal: completionSignal,
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw new RollbackPublicationError(backup, error);
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await writeStateForConfig(config, {
|
|
471
|
+
version: VERSION,
|
|
472
|
+
profile: config.snapshotIdentity,
|
|
473
|
+
lastAppliedSnapshot: result.head.snapshotId,
|
|
474
|
+
lastRemoteRevision: result.head.revision,
|
|
475
|
+
lastFileHashes,
|
|
476
|
+
include: [...config.include],
|
|
477
|
+
});
|
|
478
|
+
} catch (error) {
|
|
479
|
+
throw new PublicationStatePersistenceError(result.head, error, backup);
|
|
480
|
+
}
|
|
481
|
+
if (options.signal?.aborted) return;
|
|
482
|
+
ctx.ui.notify(
|
|
483
|
+
[
|
|
484
|
+
`Rolled back sync setup “${config.setupName}” to ${target}; latest: ${result.head.snapshotId}. Backup: ${backup}`,
|
|
485
|
+
...result.warnings,
|
|
486
|
+
]
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.join("\n"),
|
|
489
|
+
result.warnings.length > 0 ? "warning" : "info",
|
|
490
|
+
);
|
|
491
|
+
await maybeReload(ctx, options.signal);
|
|
589
492
|
}
|
|
590
493
|
|
|
591
494
|
function protectedSessionPaths(ctx: ExtensionCommandContext | ExtensionContext) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
495
|
+
const getSessionFile = ctx.sessionManager.getSessionFile;
|
|
496
|
+
if (typeof getSessionFile !== "function") return new Set<string>();
|
|
497
|
+
const sessionFile = getSessionFile.call(ctx.sessionManager) as string | undefined;
|
|
498
|
+
const snapshotPath = sessionFile
|
|
499
|
+
? sessionSnapshotPathFromAbsolute(sessionFile, sessionDirFromContext(ctx))
|
|
500
|
+
: undefined;
|
|
501
|
+
return snapshotPath ? new Set([snapshotPath]) : new Set<string>();
|
|
599
502
|
}
|
|
600
503
|
|
|
601
504
|
async function maybeReload(ctx: ExtensionCommandContext | ExtensionContext, signal?: AbortSignal) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
505
|
+
if (signal?.aborted || !("reload" in ctx)) return;
|
|
506
|
+
if (
|
|
507
|
+
ctx.hasUI &&
|
|
508
|
+
(await ctx.ui.confirm(
|
|
509
|
+
"Reload Pi resources now?",
|
|
510
|
+
"This reloads extensions, skills, prompts, themes, and context files.",
|
|
511
|
+
))
|
|
512
|
+
) {
|
|
513
|
+
if (signal?.aborted) return;
|
|
514
|
+
await ctx.reload();
|
|
515
|
+
}
|
|
613
516
|
}
|
|
614
517
|
|
|
615
518
|
async function readRemoteSnapshotForUpload(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
519
|
+
backend: SyncBackend,
|
|
520
|
+
config: AnySyncConfig,
|
|
521
|
+
head: RemoteHead | undefined,
|
|
522
|
+
state: SyncState,
|
|
523
|
+
signal?: AbortSignal,
|
|
621
524
|
) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
525
|
+
if (
|
|
526
|
+
!head ||
|
|
527
|
+
(head.snapshotId === state.lastAppliedSnapshot &&
|
|
528
|
+
!syncPolicyChanged(state, config) &&
|
|
529
|
+
(!state.lastRemoteRevision || backend.sameRevision(head.revision, state.lastRemoteRevision)))
|
|
530
|
+
) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
return backend.readSnapshot(head.snapshotRef, signal);
|
|
631
534
|
}
|
|
632
535
|
|
|
633
536
|
async function snapshotForUpload(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
537
|
+
backend: SyncBackend,
|
|
538
|
+
config: AnySyncConfig,
|
|
539
|
+
local: Snapshot,
|
|
540
|
+
head: RemoteHead | undefined,
|
|
541
|
+
remote?: Snapshot,
|
|
542
|
+
signal?: AbortSignal,
|
|
543
|
+
options: { ignoreUnreadableRemote?: boolean } = {},
|
|
641
544
|
) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
545
|
+
if (!head) return local;
|
|
546
|
+
let snapshot = remote;
|
|
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
|
+
}
|
|
555
|
+
return mergeRemotePreservedFiles(local, snapshot, config);
|
|
653
556
|
}
|
|
654
557
|
|
|
655
558
|
async function confirmPush(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
559
|
+
ctx: ExtensionCommandContext | ExtensionContext,
|
|
560
|
+
options: CommandOptions,
|
|
561
|
+
config: AnySyncConfig,
|
|
562
|
+
backend: SyncBackend,
|
|
563
|
+
local: Snapshot,
|
|
564
|
+
upload: Snapshot,
|
|
565
|
+
head: RemoteHead | undefined,
|
|
566
|
+
remote: Snapshot | undefined,
|
|
567
|
+
title = snapshotIncludesSessions(upload) ? "Push pi settings and sessions?" : "Push pi settings?",
|
|
665
568
|
) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
),
|
|
678
|
-
);
|
|
679
|
-
throwIfAborted(options.signal);
|
|
680
|
-
if (confirmed) return true;
|
|
681
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
682
|
-
ctx.ui.notify("Push cancelled.", "info");
|
|
683
|
-
return false;
|
|
569
|
+
throwIfAborted(options.signal);
|
|
570
|
+
if (options.yes) return true;
|
|
571
|
+
const confirmed = await ctx.ui.confirm(
|
|
572
|
+
title,
|
|
573
|
+
formatPushSummary(config, backend.destination, upload, head, countPreservedRemoteFiles(local, upload), remote),
|
|
574
|
+
);
|
|
575
|
+
throwIfAborted(options.signal);
|
|
576
|
+
if (confirmed) return true;
|
|
577
|
+
setSyncStatus(ctx, undefined);
|
|
578
|
+
ctx.ui.notify("Push cancelled.", "info");
|
|
579
|
+
return false;
|
|
684
580
|
}
|
|
685
581
|
|
|
686
|
-
function sameRemoteHead(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
right: RemoteHead | undefined,
|
|
690
|
-
) {
|
|
691
|
-
if (!left || !right) return left === right;
|
|
692
|
-
return backend.sameRevision(left.revision, right.revision);
|
|
582
|
+
function sameRemoteHead(backend: SyncBackend, left: RemoteHead | undefined, right: RemoteHead | undefined) {
|
|
583
|
+
if (!left || !right) return left === right;
|
|
584
|
+
return backend.sameRevision(left.revision, right.revision);
|
|
693
585
|
}
|
|
694
586
|
|
|
695
|
-
export async function backupLocal(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const encoded = await encodeSnapshot(snapshot);
|
|
708
|
-
throwIfAborted(signal);
|
|
709
|
-
await fs.writeFile(backupPath, encoded, { signal });
|
|
710
|
-
return backupPath;
|
|
587
|
+
export async function backupLocal(profile: string, options: SnapshotOptions = {}, signal?: AbortSignal) {
|
|
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;
|
|
711
599
|
}
|