@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,684 +3,643 @@ import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
|
3
3
|
import { createSyncBackend, type SyncBackendFactory } from "../backends/backend-factory.js";
|
|
4
4
|
import type { RemoteHead, SyncBackend } from "../backends/sync-backend.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadPartialConfig,
|
|
8
|
+
type SyncSetupStorageReview,
|
|
9
|
+
syncConfigReviewFingerprint,
|
|
10
10
|
} from "../settings/config.js";
|
|
11
11
|
import { SyncSetupReviewChangedError, updateSyncSetup } from "../settings/settings-management.js";
|
|
12
12
|
import type { AnySyncConfig, PartialConfig } from "../settings/settings-types.js";
|
|
13
13
|
import { readSnapshotForHead } from "../sync/remote-snapshot.js";
|
|
14
14
|
import { errorMessage } from "../sync/sync-errors.js";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
compareSyncInclude,
|
|
17
|
+
inspectRemoteSelection,
|
|
18
|
+
type RemoteSelectionDecision,
|
|
19
|
+
type RemoteSelectionState,
|
|
20
|
+
sameSyncInclude,
|
|
21
21
|
} from "../sync/sync-policy.js";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
type RunRouteResult,
|
|
25
|
-
runCancellableOperation,
|
|
26
|
-
} from "./cancellable-operation.js";
|
|
22
|
+
import { type RunRoute, type RunRouteResult, runCancellableOperation } from "./cancellable-operation.js";
|
|
23
|
+
import { setSyncStatus } from "./sync-status.js";
|
|
27
24
|
import { safeTerminalText } from "./terminal-text.js";
|
|
28
25
|
|
|
29
|
-
const STATUS_KEY = "sync";
|
|
30
|
-
|
|
31
26
|
export type RemoteSelectionOrigin = "settings" | "sync" | "pull" | "push";
|
|
32
27
|
|
|
33
28
|
export interface RemoteSelectionReviewOptions {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
decision?: RemoteSelectionDecision;
|
|
30
|
+
origin?: RemoteSelectionOrigin;
|
|
31
|
+
runRoute?: RunRoute;
|
|
32
|
+
cancelLabel?: string;
|
|
33
|
+
/** The explicit mismatch is resolved or superseded; this does not imply file equality. */
|
|
34
|
+
onSelectionResolved?: () => void;
|
|
35
|
+
withStateAccess?: <T>(task: () => Promise<T>) => Promise<T>;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
export type RemoteSelectionReviewResult =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
| { kind: "back" }
|
|
40
|
+
| { kind: "closed" }
|
|
41
|
+
| { kind: "done" }
|
|
42
|
+
| { kind: "stale" }
|
|
43
|
+
| { kind: "route-result"; result: RunRouteResult; route: "sync" | "pull" | "push" };
|
|
49
44
|
|
|
50
45
|
export async function showRemoteSelectionReview(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
ctx: ExtensionContext,
|
|
47
|
+
setupName?: string,
|
|
48
|
+
signal?: AbortSignal,
|
|
49
|
+
factory: SyncBackendFactory = createSyncBackend,
|
|
50
|
+
options: RemoteSelectionReviewOptions = {},
|
|
56
51
|
): Promise<RemoteSelectionReviewResult> {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
52
|
+
try {
|
|
53
|
+
let decision = options.decision;
|
|
54
|
+
if (!decision) {
|
|
55
|
+
const inspected = await inspectConfiguredRemoteSelection(ctx, setupName, signal, factory);
|
|
56
|
+
if (!inspected || signal?.aborted) return { kind: "stale" };
|
|
57
|
+
// A successful fresh read can supersede an old explicit mismatch without
|
|
58
|
+
// changing local settings. This is not user cancellation or proof of file equality.
|
|
59
|
+
if (inspected.kind === "empty" || inspected.state.kind !== "different") {
|
|
60
|
+
options.onSelectionResolved?.();
|
|
61
|
+
}
|
|
62
|
+
if (inspected.kind === "empty") {
|
|
63
|
+
ctx.ui.notify("Remote storage has no snapshot or synced-content list yet.", "info");
|
|
64
|
+
return { kind: "back" };
|
|
65
|
+
}
|
|
66
|
+
if (inspected.state.kind === "same") {
|
|
67
|
+
ctx.ui.notify("Remote synced content already matches this sync setup.", "info");
|
|
68
|
+
return { kind: "back" };
|
|
69
|
+
}
|
|
70
|
+
if (inspected.state.kind === "legacy") {
|
|
71
|
+
if (ctx.mode !== "tui") {
|
|
72
|
+
ctx.ui.notify(formatLegacySummary(inspected.config, inspected.state.discovered), "info");
|
|
73
|
+
return { kind: "back" };
|
|
74
|
+
}
|
|
75
|
+
await showLegacyDiscovery(ctx, inspected.config, inspected.state.discovered, signal);
|
|
76
|
+
return signal?.aborted ? { kind: "stale" } : { kind: "back" };
|
|
77
|
+
}
|
|
78
|
+
decision = decisionFromState(inspected.config, inspected.state);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (ctx.mode !== "tui") {
|
|
82
|
+
ctx.ui.notify(formatRemoteSelectionSummary(decision), "warning");
|
|
83
|
+
return { kind: "back" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let currentDecision = decision;
|
|
87
|
+
for (;;) {
|
|
88
|
+
if (signal?.aborted) return { kind: "stale" };
|
|
89
|
+
const result = await showSelectionDifference(
|
|
90
|
+
ctx,
|
|
91
|
+
currentDecision,
|
|
92
|
+
options.origin ?? "settings",
|
|
93
|
+
options.runRoute,
|
|
94
|
+
signal,
|
|
95
|
+
factory,
|
|
96
|
+
options,
|
|
97
|
+
);
|
|
98
|
+
if (result.kind !== "refresh") return result;
|
|
99
|
+
const refreshed = await runWithOptionalStateAccess(options, () =>
|
|
100
|
+
inspectConfiguredRemoteSelection(ctx, currentDecision.setupName, signal, factory),
|
|
101
|
+
);
|
|
102
|
+
if (!refreshed || signal?.aborted) return { kind: "stale" };
|
|
103
|
+
if (refreshed.kind === "empty") {
|
|
104
|
+
options.onSelectionResolved?.();
|
|
105
|
+
ctx.ui.notify("Remote storage no longer has a snapshot or synced-content list.", "warning");
|
|
106
|
+
return { kind: "back" };
|
|
107
|
+
}
|
|
108
|
+
if (refreshed.state.kind !== "different") {
|
|
109
|
+
ctx.ui.notify(
|
|
110
|
+
refreshed.state.kind === "same"
|
|
111
|
+
? "Remote synced content now matches this sync setup."
|
|
112
|
+
: "The refreshed legacy snapshot has no authoritative synced-content list.",
|
|
113
|
+
"info",
|
|
114
|
+
);
|
|
115
|
+
options.onSelectionResolved?.();
|
|
116
|
+
return { kind: "done" };
|
|
117
|
+
}
|
|
118
|
+
currentDecision = decisionFromState(refreshed.config, refreshed.state);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (signal?.aborted) return { kind: "stale" };
|
|
122
|
+
ctx.ui.notify(`Could not review synced content: ${errorMessage(error)}`, "error");
|
|
123
|
+
return { kind: "back" };
|
|
124
|
+
} finally {
|
|
125
|
+
setSyncStatus(ctx, undefined);
|
|
126
|
+
}
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
type DifferenceFlowResult = RemoteSelectionReviewResult | { kind: "refresh" };
|
|
135
130
|
|
|
136
131
|
async function showSelectionDifference(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
RemoteSelectionReviewOptions,
|
|
145
|
-
"cancelLabel" | "onSelectionResolved" | "withStateAccess"
|
|
146
|
-
>,
|
|
132
|
+
ctx: ExtensionContext,
|
|
133
|
+
initialDecision: RemoteSelectionDecision,
|
|
134
|
+
origin: RemoteSelectionOrigin,
|
|
135
|
+
runRoute: RunRoute | undefined,
|
|
136
|
+
sessionSignal: AbortSignal | undefined,
|
|
137
|
+
factory: SyncBackendFactory,
|
|
138
|
+
options: Pick<RemoteSelectionReviewOptions, "cancelLabel" | "onSelectionResolved" | "withStateAccess">,
|
|
147
139
|
): Promise<DifferenceFlowResult> {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
onError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), "error"),
|
|
368
|
-
});
|
|
369
|
-
if (sessionSignal?.aborted || menuResult.kind === "stale") return { kind: "stale" };
|
|
370
|
-
if (refreshRequested) return { kind: "refresh" };
|
|
371
|
-
if (nextResult) return nextResult;
|
|
372
|
-
if (menuResult.kind === "closed") {
|
|
373
|
-
return menuResult.reason === "back" ? { kind: "back" } : { kind: "closed" };
|
|
374
|
-
}
|
|
375
|
-
return { kind: "closed" };
|
|
140
|
+
type Screen = "choice" | "review" | "saved";
|
|
141
|
+
type Action = "adopt" | "keep" | "cancel" | "continue" | "done";
|
|
142
|
+
interface FlowState {
|
|
143
|
+
decision: RemoteSelectionDecision;
|
|
144
|
+
saved: boolean;
|
|
145
|
+
}
|
|
146
|
+
let flowState: FlowState = { decision: initialDecision, saved: false };
|
|
147
|
+
let continuationReview: PartialConfig | undefined;
|
|
148
|
+
let nextResult: RemoteSelectionReviewResult | undefined;
|
|
149
|
+
let refreshRequested = false;
|
|
150
|
+
const route = origin === "settings" ? "sync" : origin;
|
|
151
|
+
const menu = defineMenu<FlowState, Screen, Action, ExtensionContext>({
|
|
152
|
+
start: "choice",
|
|
153
|
+
screens: {
|
|
154
|
+
choice: ({ state }) => ({
|
|
155
|
+
kind: "actions",
|
|
156
|
+
title: "Synced content differs",
|
|
157
|
+
lines: selectionSummaryLines(state.decision),
|
|
158
|
+
items: [
|
|
159
|
+
{
|
|
160
|
+
id: "review",
|
|
161
|
+
label: "Review all paths (recommended)",
|
|
162
|
+
description: "Compare exact remote-only, device-only, and ordered lists.",
|
|
163
|
+
to: "review",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "adopt",
|
|
167
|
+
label: "Use remote content list",
|
|
168
|
+
description: "Save the reviewed list on this device without pulling files.",
|
|
169
|
+
action: "adopt",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "keep",
|
|
173
|
+
label: "Keep this device's content list and update remote…",
|
|
174
|
+
description: "Open the existing exact force-push preview without skipping confirmation.",
|
|
175
|
+
action: "keep",
|
|
176
|
+
},
|
|
177
|
+
{ id: "cancel", label: options.cancelLabel ?? "Cancel", action: "cancel" },
|
|
178
|
+
],
|
|
179
|
+
hint: "back",
|
|
180
|
+
}),
|
|
181
|
+
review: ({ state }) => ({
|
|
182
|
+
kind: "review",
|
|
183
|
+
title: `Review synced content · ${safeTerminalText(state.decision.setupName)}`,
|
|
184
|
+
content: formatSelectionDifference(state.decision),
|
|
185
|
+
format: { kind: "text" },
|
|
186
|
+
viewportSize: "adaptive",
|
|
187
|
+
hint: "back",
|
|
188
|
+
}),
|
|
189
|
+
saved: ({ state }) => ({
|
|
190
|
+
kind: "actions",
|
|
191
|
+
title: "Remote content list saved",
|
|
192
|
+
lines: [
|
|
193
|
+
`Sync setup: ${safeTerminalText(state.decision.setupName)}`,
|
|
194
|
+
"Only the included-content setting was saved.",
|
|
195
|
+
"No files were pulled and sync state was not changed.",
|
|
196
|
+
],
|
|
197
|
+
items: [
|
|
198
|
+
...(runRoute
|
|
199
|
+
? [
|
|
200
|
+
{
|
|
201
|
+
id: "continue",
|
|
202
|
+
label: continueLabel(origin),
|
|
203
|
+
description: "Start a fresh check and exact preview for this sync setup.",
|
|
204
|
+
action: "continue" as const,
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
: []),
|
|
208
|
+
{ id: "done", label: "Done", action: "done" },
|
|
209
|
+
],
|
|
210
|
+
hint: "close",
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
actions: {
|
|
214
|
+
adopt: async ({ state, signal: actionSignal }) => {
|
|
215
|
+
const signal = combineSignals(sessionSignal, actionSignal);
|
|
216
|
+
try {
|
|
217
|
+
const currentConfig = await loadConfig(state.decision.setupName);
|
|
218
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
219
|
+
assertLocalSelectionCurrent(currentConfig, state.decision);
|
|
220
|
+
if (!currentConfig.include.includes("sessions") && state.decision.remoteInclude.includes("sessions")) {
|
|
221
|
+
const acknowledged = await ctx.ui.confirm(
|
|
222
|
+
"Use a content list that includes session conversations?",
|
|
223
|
+
"Session JSONL may contain prompts, tool output, file paths, images, and secrets. This saves the list only; it does not pull files.",
|
|
224
|
+
{ signal },
|
|
225
|
+
);
|
|
226
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
227
|
+
if (!acknowledged) return { kind: "stay" as const };
|
|
228
|
+
}
|
|
229
|
+
const review = await runWithOptionalStateAccess(options, async () => {
|
|
230
|
+
const loaded = await loadAdoptionReview(state.decision, signal, factory);
|
|
231
|
+
if (signal.aborted) throw signal.reason;
|
|
232
|
+
await revalidateAndAdopt(loaded, state.decision, signal);
|
|
233
|
+
return loaded;
|
|
234
|
+
});
|
|
235
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
236
|
+
options.onSelectionResolved?.();
|
|
237
|
+
continuationReview = {
|
|
238
|
+
...review.storageReview,
|
|
239
|
+
setupName: state.decision.setupName,
|
|
240
|
+
include: [...state.decision.remoteInclude],
|
|
241
|
+
automatic: review.config.automatic,
|
|
242
|
+
onSwitch: review.config.onSwitch,
|
|
243
|
+
showStatus: review.config.showStatus,
|
|
244
|
+
};
|
|
245
|
+
flowState = { decision: state.decision, saved: true };
|
|
246
|
+
return { kind: "to" as const, screen: "saved" as const };
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
249
|
+
if (isStaleReviewError(error)) {
|
|
250
|
+
ctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, "warning");
|
|
251
|
+
refreshRequested = true;
|
|
252
|
+
return { kind: "close" as const };
|
|
253
|
+
}
|
|
254
|
+
ctx.ui.notify(`Could not save the remote content list: ${errorMessage(error)}`, "error");
|
|
255
|
+
return { kind: "stay" as const };
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
keep: async ({ state, signal: actionSignal }) => {
|
|
259
|
+
if (!runRoute) {
|
|
260
|
+
ctx.ui.notify("The reviewed update-remote route is unavailable.", "error");
|
|
261
|
+
return { kind: "stay" as const };
|
|
262
|
+
}
|
|
263
|
+
const signal = combineSignals(sessionSignal, actionSignal);
|
|
264
|
+
try {
|
|
265
|
+
const latest = await loadConfig(state.decision.setupName);
|
|
266
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
267
|
+
assertLocalSelectionCurrent(latest, state.decision);
|
|
268
|
+
const result = await runCancellableOperation(
|
|
269
|
+
ctx,
|
|
270
|
+
"Preparing this device's push preview…",
|
|
271
|
+
"push --force",
|
|
272
|
+
runRoute,
|
|
273
|
+
{
|
|
274
|
+
commitAware: true,
|
|
275
|
+
cancelledMessage: "Push preparation cancelled; no remote files were changed.",
|
|
276
|
+
target: state.decision.setupName,
|
|
277
|
+
signal,
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
return handleNestedRouteResult(result, "push");
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
283
|
+
if (isStaleReviewError(error)) {
|
|
284
|
+
ctx.ui.notify(`${errorMessage(error)} Refreshing the comparison.`, "warning");
|
|
285
|
+
refreshRequested = true;
|
|
286
|
+
return { kind: "close" as const };
|
|
287
|
+
}
|
|
288
|
+
ctx.ui.notify(`Could not prepare the remote update: ${errorMessage(error)}`, "error");
|
|
289
|
+
return { kind: "stay" as const };
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
continue: async ({ state, signal: actionSignal }) => {
|
|
293
|
+
if (!runRoute || !continuationReview) return { kind: "stay" as const };
|
|
294
|
+
const signal = combineSignals(sessionSignal, actionSignal);
|
|
295
|
+
try {
|
|
296
|
+
const latest = await loadPartialConfig(state.decision.setupName);
|
|
297
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
298
|
+
if (!sameContinuationReview(latest, continuationReview)) {
|
|
299
|
+
throw new StaleRemoteSelectionReviewError(
|
|
300
|
+
`Sync setup “${safeTerminalText(state.decision.setupName)}” changed after the remote content list was saved.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const result = await runCancellableOperation(ctx, continueBusyLabel(origin), route, runRoute, {
|
|
304
|
+
commitAware: true,
|
|
305
|
+
cancelledMessage: continuationCancelledMessage(route),
|
|
306
|
+
target: state.decision.setupName,
|
|
307
|
+
signal,
|
|
308
|
+
});
|
|
309
|
+
return handleNestedRouteResult(result, route);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (signal.aborted) return { kind: "close" as const };
|
|
312
|
+
ctx.ui.notify(`Could not continue: ${errorMessage(error)}`, "error");
|
|
313
|
+
return { kind: "stay" as const };
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
cancel: async () => ({ kind: "back" }),
|
|
317
|
+
done: async () => {
|
|
318
|
+
nextResult = { kind: "done" };
|
|
319
|
+
return { kind: "close" };
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
function handleNestedRouteResult(
|
|
325
|
+
result: Awaited<ReturnType<typeof runCancellableOperation>>,
|
|
326
|
+
nestedRoute: "sync" | "pull" | "push",
|
|
327
|
+
) {
|
|
328
|
+
if (result.kind === "completed" && result.outcome === "applied") {
|
|
329
|
+
options.onSelectionResolved?.();
|
|
330
|
+
}
|
|
331
|
+
if (result.kind === "closed") {
|
|
332
|
+
nextResult = { kind: "closed" };
|
|
333
|
+
return { kind: "close" as const };
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
result.kind === "cancelled" ||
|
|
337
|
+
(result.kind === "completed" && result.outcome === "cancelled") ||
|
|
338
|
+
result.kind === "failed"
|
|
339
|
+
) {
|
|
340
|
+
return { kind: "stay" as const };
|
|
341
|
+
}
|
|
342
|
+
nextResult = { kind: "route-result", result, route: nestedRoute };
|
|
343
|
+
return { kind: "close" as const };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const menuResult = await runMenu(ctx, menu, {
|
|
347
|
+
getState: () => flowState,
|
|
348
|
+
signal: sessionSignal,
|
|
349
|
+
isCurrent: () => !sessionSignal?.aborted,
|
|
350
|
+
onError: (_menuCtx, error) => ctx.ui.notify(errorMessage(error), "error"),
|
|
351
|
+
});
|
|
352
|
+
if (sessionSignal?.aborted || menuResult.kind === "stale") return { kind: "stale" };
|
|
353
|
+
if (refreshRequested) return { kind: "refresh" };
|
|
354
|
+
if (nextResult) return nextResult;
|
|
355
|
+
if (menuResult.kind === "closed") {
|
|
356
|
+
return menuResult.reason === "back" ? { kind: "back" } : { kind: "closed" };
|
|
357
|
+
}
|
|
358
|
+
return { kind: "closed" };
|
|
376
359
|
}
|
|
377
360
|
|
|
378
361
|
interface AdoptionReview {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
362
|
+
config: AnySyncConfig;
|
|
363
|
+
storageReview: SyncSetupStorageReview;
|
|
364
|
+
backend: SyncBackend;
|
|
365
|
+
reviewedHead: RemoteHead;
|
|
383
366
|
}
|
|
384
367
|
|
|
385
368
|
async function loadAdoptionReview(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
369
|
+
decision: RemoteSelectionDecision,
|
|
370
|
+
signal: AbortSignal,
|
|
371
|
+
factory: SyncBackendFactory,
|
|
389
372
|
): Promise<AdoptionReview> {
|
|
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
|
-
"Remote synced content changed while the comparison was open.",
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
return { config, storageReview: partial, backend, reviewedHead };
|
|
373
|
+
const config = await loadConfig(decision.setupName);
|
|
374
|
+
throwIfAborted(signal);
|
|
375
|
+
assertLocalSelectionCurrent(config, decision);
|
|
376
|
+
const partial = await loadPartialConfig(decision.setupName);
|
|
377
|
+
throwIfAborted(signal);
|
|
378
|
+
if (!sameSyncInclude(partial.include, decision.localInclude)) {
|
|
379
|
+
throw new StaleRemoteSelectionReviewError(
|
|
380
|
+
`Sync setup “${safeTerminalText(decision.setupName)}” changed while the comparison was open.`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const backend = await factory(config);
|
|
384
|
+
throwIfAborted(signal);
|
|
385
|
+
const reviewedHead = await backend.readHead(signal);
|
|
386
|
+
throwIfAborted(signal);
|
|
387
|
+
if (!reviewedHead) {
|
|
388
|
+
throw new StaleRemoteSelectionReviewError("Remote storage changed while the comparison was open.");
|
|
389
|
+
}
|
|
390
|
+
const snapshot = await readSnapshotForHead(backend, reviewedHead, signal);
|
|
391
|
+
throwIfAborted(signal);
|
|
392
|
+
const state = inspectRemoteSelection(config.include, snapshot);
|
|
393
|
+
if (state.kind !== "different" || !sameSyncInclude(state.include, decision.remoteInclude)) {
|
|
394
|
+
throw new StaleRemoteSelectionReviewError("Remote synced content changed while the comparison was open.");
|
|
395
|
+
}
|
|
396
|
+
return { config, storageReview: partial, backend, reviewedHead };
|
|
418
397
|
}
|
|
419
398
|
|
|
420
|
-
async function revalidateAndAdopt(
|
|
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
|
-
await updateSyncSetup(
|
|
447
|
-
decision.setupName,
|
|
448
|
-
(setup) => ({
|
|
449
|
-
...setup,
|
|
450
|
-
sync: { ...setup.sync, include: [...decision.remoteInclude] },
|
|
451
|
-
}),
|
|
452
|
-
{
|
|
453
|
-
expectedStorage: review.storageReview,
|
|
454
|
-
expectedInclude: decision.localInclude,
|
|
455
|
-
signal,
|
|
456
|
-
},
|
|
457
|
-
);
|
|
399
|
+
async function revalidateAndAdopt(review: AdoptionReview, decision: RemoteSelectionDecision, signal: AbortSignal) {
|
|
400
|
+
const currentHead = await review.backend.readHead(signal);
|
|
401
|
+
throwIfAborted(signal);
|
|
402
|
+
if (!currentHead || !review.backend.sameRevision(review.reviewedHead.revision, currentHead.revision)) {
|
|
403
|
+
throw new StaleRemoteSelectionReviewError("Remote storage changed while the comparison was open.");
|
|
404
|
+
}
|
|
405
|
+
const currentSnapshot = await readSnapshotForHead(review.backend, currentHead, signal);
|
|
406
|
+
throwIfAborted(signal);
|
|
407
|
+
const currentState = inspectRemoteSelection(review.config.include, currentSnapshot);
|
|
408
|
+
if (currentState.kind !== "different" || !sameSyncInclude(currentState.include, decision.remoteInclude)) {
|
|
409
|
+
throw new StaleRemoteSelectionReviewError("Remote synced content changed while the comparison was open.");
|
|
410
|
+
}
|
|
411
|
+
await updateSyncSetup(
|
|
412
|
+
decision.setupName,
|
|
413
|
+
(setup) => ({
|
|
414
|
+
...setup,
|
|
415
|
+
sync: { ...setup.sync, include: [...decision.remoteInclude] },
|
|
416
|
+
}),
|
|
417
|
+
{
|
|
418
|
+
expectedStorage: review.storageReview,
|
|
419
|
+
expectedInclude: decision.localInclude,
|
|
420
|
+
signal,
|
|
421
|
+
},
|
|
422
|
+
);
|
|
458
423
|
}
|
|
459
424
|
|
|
460
425
|
async function inspectConfiguredRemoteSelection(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
426
|
+
ctx: ExtensionContext,
|
|
427
|
+
setupName: string | undefined,
|
|
428
|
+
signal: AbortSignal | undefined,
|
|
429
|
+
factory: SyncBackendFactory,
|
|
465
430
|
): Promise<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
431
|
+
| { kind: "empty"; config: AnySyncConfig }
|
|
432
|
+
| { kind: "selection"; config: AnySyncConfig; state: RemoteSelectionState }
|
|
433
|
+
| undefined
|
|
469
434
|
> {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
435
|
+
const config = await loadConfig(setupName);
|
|
436
|
+
if (signal?.aborted) return undefined;
|
|
437
|
+
setSyncStatus(ctx, `checking synced content for ${safeTerminalText(config.setupName)}`);
|
|
438
|
+
const backend = await factory(config);
|
|
439
|
+
if (signal?.aborted) return undefined;
|
|
440
|
+
const head = await backend.readHead(signal);
|
|
441
|
+
if (signal?.aborted) return undefined;
|
|
442
|
+
if (!head) return { kind: "empty", config };
|
|
443
|
+
const snapshot = await readSnapshotForHead(backend, head, signal);
|
|
444
|
+
if (signal?.aborted) return undefined;
|
|
445
|
+
return {
|
|
446
|
+
kind: "selection",
|
|
447
|
+
config,
|
|
448
|
+
state: inspectRemoteSelection(config.include, snapshot),
|
|
449
|
+
};
|
|
485
450
|
}
|
|
486
451
|
|
|
487
452
|
async function showLegacyDiscovery(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
453
|
+
ctx: ExtensionContext,
|
|
454
|
+
config: AnySyncConfig,
|
|
455
|
+
discovered: string[],
|
|
456
|
+
signal?: AbortSignal,
|
|
492
457
|
) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
458
|
+
const menu = defineMenu<undefined, "choice" | "review", "back", ExtensionContext>({
|
|
459
|
+
start: "choice",
|
|
460
|
+
screens: {
|
|
461
|
+
choice: () => ({
|
|
462
|
+
kind: "actions",
|
|
463
|
+
title: `Compare synced content · ${safeTerminalText(config.setupName)}`,
|
|
464
|
+
lines: [
|
|
465
|
+
"This legacy snapshot has no portable synced-content list.",
|
|
466
|
+
"Discovered paths are partial and read-only; preserved files may not have been selected.",
|
|
467
|
+
],
|
|
468
|
+
items: [
|
|
469
|
+
{ id: "review", label: "Review discovered paths", to: "review" },
|
|
470
|
+
{ id: "back", label: "Back", action: "back" },
|
|
471
|
+
],
|
|
472
|
+
hint: "close",
|
|
473
|
+
}),
|
|
474
|
+
review: () => ({
|
|
475
|
+
kind: "review",
|
|
476
|
+
title: "Partial discovery from legacy snapshot",
|
|
477
|
+
content: [
|
|
478
|
+
"Partial discovery only — not an authoritative selection.",
|
|
479
|
+
"",
|
|
480
|
+
...(discovered.length > 0
|
|
481
|
+
? discovered.map((item) => `Discovered: ${safeTerminalText(item)}`)
|
|
482
|
+
: ["No safe paths were discovered."]),
|
|
483
|
+
"",
|
|
484
|
+
"Use Add custom path… in the local Included Content editor if needed.",
|
|
485
|
+
].join("\n"),
|
|
486
|
+
format: { kind: "text" },
|
|
487
|
+
viewportSize: "adaptive",
|
|
488
|
+
hint: "back",
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
actions: { back: async () => ({ kind: "close" }) },
|
|
492
|
+
});
|
|
493
|
+
await runMenu(ctx, menu, {
|
|
494
|
+
getState: () => undefined,
|
|
495
|
+
signal,
|
|
496
|
+
isCurrent: () => !signal?.aborted,
|
|
497
|
+
});
|
|
533
498
|
}
|
|
534
499
|
|
|
535
500
|
function selectionSummaryLines(decision: RemoteSelectionDecision) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
]),
|
|
545
|
-
];
|
|
501
|
+
const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
|
|
502
|
+
return [
|
|
503
|
+
`Sync setup: ${safeTerminalText(decision.setupName)}`,
|
|
504
|
+
"Nothing changed. Review both lists before choosing what happens next.",
|
|
505
|
+
...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
|
|
506
|
+
? ["Only the ordering differs; membership is the same."]
|
|
507
|
+
: [`Remote-only paths: ${comparison.remoteOnly.length} · Device-only paths: ${comparison.localOnly.length}`]),
|
|
508
|
+
];
|
|
546
509
|
}
|
|
547
510
|
|
|
548
511
|
function formatSelectionDifference(decision: RemoteSelectionDecision) {
|
|
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
|
-
|
|
512
|
+
const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
|
|
513
|
+
return [
|
|
514
|
+
...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
|
|
515
|
+
? ["Only ordering differs; both lists contain the same paths.", ""]
|
|
516
|
+
: []),
|
|
517
|
+
"Remote-only paths:",
|
|
518
|
+
...(comparison.remoteOnly.length > 0
|
|
519
|
+
? comparison.remoteOnly.map((item) => `+ ${safeTerminalText(item)}`)
|
|
520
|
+
: ["(none)"]),
|
|
521
|
+
"",
|
|
522
|
+
"Device-only paths:",
|
|
523
|
+
...(comparison.localOnly.length > 0
|
|
524
|
+
? comparison.localOnly.map((item) => `- ${safeTerminalText(item)}`)
|
|
525
|
+
: ["(none)"]),
|
|
526
|
+
"",
|
|
527
|
+
"Remote ordered list:",
|
|
528
|
+
...(decision.remoteInclude.length > 0
|
|
529
|
+
? decision.remoteInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`)
|
|
530
|
+
: ["(none)"]),
|
|
531
|
+
"",
|
|
532
|
+
"This device's ordered list:",
|
|
533
|
+
...(decision.localInclude.length > 0
|
|
534
|
+
? decision.localInclude.map((item, index) => `${index + 1}. ${safeTerminalText(item)}`)
|
|
535
|
+
: ["(none)"]),
|
|
536
|
+
"",
|
|
537
|
+
"Using the remote list saves settings only and does not pull files.",
|
|
538
|
+
].join("\n");
|
|
576
539
|
}
|
|
577
540
|
|
|
578
541
|
function formatRemoteSelectionSummary(decision: RemoteSelectionDecision) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
542
|
+
const comparison = compareSyncInclude(decision.localInclude, decision.remoteInclude);
|
|
543
|
+
return [
|
|
544
|
+
`Synced content for “${safeTerminalText(decision.setupName)}” differs from this device.`,
|
|
545
|
+
`Remote-only: ${safeList(comparison.remoteOnly)}`,
|
|
546
|
+
`Device-only: ${safeList(comparison.localOnly)}`,
|
|
547
|
+
...(comparison.remoteOnly.length === 0 && comparison.localOnly.length === 0
|
|
548
|
+
? [
|
|
549
|
+
"Only ordering differs.",
|
|
550
|
+
`Remote order: ${safeList(decision.remoteInclude)}`,
|
|
551
|
+
`Device order: ${safeList(decision.localInclude)}`,
|
|
552
|
+
]
|
|
553
|
+
: []),
|
|
554
|
+
"Run /sync in TUI to choose a content list; RPC review is read-only.",
|
|
555
|
+
].join("\n");
|
|
593
556
|
}
|
|
594
557
|
|
|
595
558
|
function formatLegacySummary(config: AnySyncConfig, discovered: readonly string[]) {
|
|
596
|
-
|
|
559
|
+
return `Remote snapshot for “${safeTerminalText(config.setupName)}” has no portable synced-content list; ${discovered.length} safe path${discovered.length === 1 ? " was" : "s were"} discovered, but the result is partial and read-only.`;
|
|
597
560
|
}
|
|
598
561
|
|
|
599
562
|
function decisionFromState(
|
|
600
|
-
|
|
601
|
-
|
|
563
|
+
config: AnySyncConfig,
|
|
564
|
+
state: Extract<RemoteSelectionState, { kind: "different" }>,
|
|
602
565
|
): RemoteSelectionDecision {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
566
|
+
return {
|
|
567
|
+
setupName: config.setupName,
|
|
568
|
+
configIdentity: syncConfigReviewFingerprint(config),
|
|
569
|
+
localInclude: [...config.include],
|
|
570
|
+
remoteInclude: [...state.include],
|
|
571
|
+
};
|
|
609
572
|
}
|
|
610
573
|
|
|
611
574
|
function assertLocalSelectionCurrent(config: AnySyncConfig, decision: RemoteSelectionDecision) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
575
|
+
if (
|
|
576
|
+
syncConfigReviewFingerprint(config) === decision.configIdentity &&
|
|
577
|
+
sameSyncInclude(config.include, decision.localInclude)
|
|
578
|
+
) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
throw new StaleRemoteSelectionReviewError(
|
|
582
|
+
`Sync setup “${safeTerminalText(config.setupName)}” changed while the comparison was open.`,
|
|
583
|
+
);
|
|
621
584
|
}
|
|
622
585
|
|
|
623
586
|
function sameContinuationReview(left: PartialConfig, right: PartialConfig) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
587
|
+
return (
|
|
588
|
+
left.setupName === right.setupName &&
|
|
589
|
+
left.connectionName === right.connectionName &&
|
|
590
|
+
left.storageKind === right.storageKind &&
|
|
591
|
+
left.storagePath === right.storagePath &&
|
|
592
|
+
left.bucket === right.bucket &&
|
|
593
|
+
left.branch === right.branch &&
|
|
594
|
+
sameSyncInclude(left.include, right.include)
|
|
595
|
+
);
|
|
633
596
|
}
|
|
634
597
|
|
|
635
598
|
function continueLabel(origin: RemoteSelectionOrigin) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
599
|
+
if (origin === "pull") return "Continue Pull now…";
|
|
600
|
+
if (origin === "push") return "Continue Push now…";
|
|
601
|
+
return "Continue Sync now…";
|
|
639
602
|
}
|
|
640
603
|
|
|
641
604
|
function continueBusyLabel(origin: RemoteSelectionOrigin) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
605
|
+
if (origin === "pull") return "Checking remote changes…";
|
|
606
|
+
if (origin === "push") return "Preparing push preview…";
|
|
607
|
+
return "Checking current sync setup…";
|
|
645
608
|
}
|
|
646
609
|
|
|
647
610
|
function continuationCancelledMessage(route: "sync" | "pull" | "push") {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
611
|
+
if (route === "pull") return "Pull check cancelled; no local files were changed.";
|
|
612
|
+
if (route === "push") return "Push preparation cancelled; no remote files were changed.";
|
|
613
|
+
return "Sync check cancelled; no settings or files were changed.";
|
|
651
614
|
}
|
|
652
615
|
|
|
653
616
|
function safeList(values: readonly string[]) {
|
|
654
|
-
|
|
617
|
+
return values.length > 0 ? values.map(safeTerminalText).join(", ") : "none";
|
|
655
618
|
}
|
|
656
619
|
|
|
657
620
|
class StaleRemoteSelectionReviewError extends Error {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
621
|
+
constructor(message: string) {
|
|
622
|
+
super(message);
|
|
623
|
+
this.name = "StaleRemoteSelectionReviewError";
|
|
624
|
+
}
|
|
662
625
|
}
|
|
663
626
|
|
|
664
627
|
function isStaleReviewError(error: unknown) {
|
|
665
|
-
|
|
666
|
-
error instanceof StaleRemoteSelectionReviewError || error instanceof SyncSetupReviewChangedError
|
|
667
|
-
);
|
|
628
|
+
return error instanceof StaleRemoteSelectionReviewError || error instanceof SyncSetupReviewChangedError;
|
|
668
629
|
}
|
|
669
630
|
|
|
670
631
|
function runWithOptionalStateAccess<T>(
|
|
671
|
-
|
|
672
|
-
|
|
632
|
+
options: Pick<RemoteSelectionReviewOptions, "withStateAccess">,
|
|
633
|
+
task: () => Promise<T>,
|
|
673
634
|
) {
|
|
674
|
-
|
|
635
|
+
return options.withStateAccess ? options.withStateAccess(task) : task();
|
|
675
636
|
}
|
|
676
637
|
|
|
677
638
|
function combineSignals(sessionSignal: AbortSignal | undefined, actionSignal: AbortSignal) {
|
|
678
|
-
|
|
639
|
+
return sessionSignal ? AbortSignal.any([sessionSignal, actionSignal]) : actionSignal;
|
|
679
640
|
}
|
|
680
641
|
|
|
681
642
|
function throwIfAborted(signal: AbortSignal) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
? signal.reason
|
|
685
|
-
: new DOMException("The operation was aborted", "AbortError");
|
|
643
|
+
if (!signal.aborted) return;
|
|
644
|
+
throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
686
645
|
}
|