@narumitw/pi-sync 0.50.3 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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
|
@@ -2,23 +2,15 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
addStorageConnection,
|
|
6
|
+
removeStorageConnection,
|
|
7
|
+
updateStorageConnection,
|
|
8
8
|
} from "../settings/settings-management.js";
|
|
9
9
|
import { readLocalConfigObject } from "../settings/settings-store.js";
|
|
10
|
-
import {
|
|
11
|
-
isCloudflareR2Endpoint,
|
|
12
|
-
normalizeS3Endpoint,
|
|
13
|
-
ownRecord,
|
|
14
|
-
} from "../settings/settings-validation.js";
|
|
10
|
+
import { isCloudflareR2Endpoint, normalizeS3Endpoint, ownRecord } from "../settings/settings-validation.js";
|
|
15
11
|
import { syncErrorGuidance } from "../sync/sync-error-guidance.js";
|
|
16
12
|
import { showAddGitStorageProfile, showEditGitStorageProfile } from "./setup/git-ui.js";
|
|
17
|
-
import {
|
|
18
|
-
applyS3CredentialUpdate,
|
|
19
|
-
chooseS3Credentials,
|
|
20
|
-
chooseS3CredentialUpdate,
|
|
21
|
-
} from "./setup/s3-credentials-ui.js";
|
|
13
|
+
import { applyS3CredentialUpdate, chooseS3Credentials, chooseS3CredentialUpdate } from "./setup/s3-credentials-ui.js";
|
|
22
14
|
import { promptResourceName } from "./setup/setup-prompts.js";
|
|
23
15
|
import { saveReviewedDraft } from "./setup/setup-review.js";
|
|
24
16
|
import { requiredInput, requiredValueInput } from "./setup/text-input.js";
|
|
@@ -26,387 +18,343 @@ import { showAddWebDavStorageProfile, showEditWebDavStorageProfile } from "./set
|
|
|
26
18
|
import { safeTerminalText } from "./terminal-text.js";
|
|
27
19
|
|
|
28
20
|
export async function showStorageConnections(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
back: async () => {
|
|
142
|
-
selectedName = undefined;
|
|
143
|
-
return { kind: "back" };
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
await runMenu(ctx, menu, {
|
|
148
|
-
getState: () => loadStorageMenuState(selectedName, signal),
|
|
149
|
-
signal,
|
|
150
|
-
isCurrent: () => !signal?.aborted,
|
|
151
|
-
});
|
|
21
|
+
let selectedName: string | undefined;
|
|
22
|
+
const nameById = new Map<string, string>();
|
|
23
|
+
type Screen = "list" | "detail";
|
|
24
|
+
type Action = "add" | "select" | "edit" | "remove" | "back";
|
|
25
|
+
const menu = defineMenu<Awaited<ReturnType<typeof loadStorageMenuState>>, Screen, Action, ExtensionCommandContext>({
|
|
26
|
+
start: "list",
|
|
27
|
+
screens: {
|
|
28
|
+
list: ({ state }) => {
|
|
29
|
+
nameById.clear();
|
|
30
|
+
const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
|
|
31
|
+
return {
|
|
32
|
+
kind: "actions",
|
|
33
|
+
title: "Storage connections",
|
|
34
|
+
lines: state.version3
|
|
35
|
+
? ["Server addresses and sign-in details, reusable across sync setups."]
|
|
36
|
+
: ["Create version 3 settings before managing storage connections."],
|
|
37
|
+
items: state.version3
|
|
38
|
+
? [
|
|
39
|
+
{ id: "add", label: "Add storage connection", action: "add" as const },
|
|
40
|
+
...names.map((name, index) => {
|
|
41
|
+
const id = `connection:${index}`;
|
|
42
|
+
nameById.set(id, name);
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
label: safeTerminalText(name),
|
|
46
|
+
description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
|
|
47
|
+
action: "select" as const,
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
]
|
|
51
|
+
: [],
|
|
52
|
+
hint: "back",
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
detail: ({ state }) => ({
|
|
56
|
+
kind: "actions",
|
|
57
|
+
title: state.selected ? `Storage connection “${safeTerminalText(state.selected.name)}”` : "Storage connection",
|
|
58
|
+
lines: state.selected?.lines ?? ["This storage connection no longer exists."],
|
|
59
|
+
items: state.selected
|
|
60
|
+
? [
|
|
61
|
+
{ id: "edit", label: "Edit storage connection…", action: "edit" },
|
|
62
|
+
...(state.selected.usedBy.length === 0
|
|
63
|
+
? [
|
|
64
|
+
{
|
|
65
|
+
id: "remove",
|
|
66
|
+
label: "Remove storage connection…",
|
|
67
|
+
action: "remove" as const,
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
{ id: "back", label: "Back", action: "back" },
|
|
72
|
+
]
|
|
73
|
+
: [{ id: "back", label: "Back", action: "back" }],
|
|
74
|
+
hint: "back",
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
actions: {
|
|
78
|
+
add: async () => {
|
|
79
|
+
try {
|
|
80
|
+
await showAddStorageConnection(ctx, signal);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!signal?.aborted) {
|
|
83
|
+
ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { kind: "stay" };
|
|
87
|
+
},
|
|
88
|
+
select: async ({ itemId }) => {
|
|
89
|
+
selectedName = nameById.get(itemId);
|
|
90
|
+
return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
|
|
91
|
+
},
|
|
92
|
+
edit: async ({ state }) => {
|
|
93
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
94
|
+
try {
|
|
95
|
+
await editStorageConnection(ctx, state.selected.name, state.selected.profile, state.selected.usedBy, signal);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
notifyConnectionError(ctx, state.selected.name, error, signal);
|
|
98
|
+
}
|
|
99
|
+
return { kind: "stay" };
|
|
100
|
+
},
|
|
101
|
+
remove: async ({ state }) => {
|
|
102
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
103
|
+
const name = state.selected.name;
|
|
104
|
+
const confirmed = await ctx.ui.confirm(
|
|
105
|
+
"Remove storage connection?",
|
|
106
|
+
`Remove local storage connection “${safeTerminalText(name)}”? Remote data and history are not deleted.`,
|
|
107
|
+
{ signal },
|
|
108
|
+
);
|
|
109
|
+
if (!confirmed || signal?.aborted) return { kind: "rejected" };
|
|
110
|
+
try {
|
|
111
|
+
await removeStorageConnection(name, signal);
|
|
112
|
+
ctx.ui.notify(`Removed storage connection “${safeTerminalText(name)}”.`, "info");
|
|
113
|
+
selectedName = undefined;
|
|
114
|
+
return { kind: "back" };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
notifyConnectionError(ctx, name, error, signal);
|
|
117
|
+
return { kind: "stay" };
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
back: async () => {
|
|
121
|
+
selectedName = undefined;
|
|
122
|
+
return { kind: "back" };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await runMenu(ctx, menu, {
|
|
127
|
+
getState: () => loadStorageMenuState(selectedName, signal),
|
|
128
|
+
signal,
|
|
129
|
+
isCurrent: () => !signal?.aborted,
|
|
130
|
+
});
|
|
152
131
|
}
|
|
153
132
|
|
|
154
133
|
async function loadStorageMenuState(selectedName: string | undefined, signal?: AbortSignal) {
|
|
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
|
-
|
|
134
|
+
const raw = await readLocalConfigObject();
|
|
135
|
+
if (signal?.aborted) throw signal.reason;
|
|
136
|
+
const profiles = ownRecord(raw?.storageConnections) ?? {};
|
|
137
|
+
const profile = selectedName ? ownRecord(profiles[selectedName]) : undefined;
|
|
138
|
+
if (!selectedName || !profile) {
|
|
139
|
+
return { version3: raw?.version === 3, profiles, selected: undefined };
|
|
140
|
+
}
|
|
141
|
+
const usedBy = referencingSetups(raw, selectedName);
|
|
142
|
+
return {
|
|
143
|
+
version3: raw?.version === 3,
|
|
144
|
+
profiles,
|
|
145
|
+
selected: {
|
|
146
|
+
name: selectedName,
|
|
147
|
+
profile,
|
|
148
|
+
usedBy,
|
|
149
|
+
lines: [
|
|
150
|
+
`Type: ${connectionType(profile)}`,
|
|
151
|
+
`Endpoint: ${connectionEndpoint(profile)}`,
|
|
152
|
+
`Credentials: ${credentialSource(profile)}`,
|
|
153
|
+
`Used by: ${usedBy.length > 0 ? usedBy.map(safeTerminalText).join(", ") : "No sync setups"}`,
|
|
154
|
+
...(usedBy.length > 0
|
|
155
|
+
? [
|
|
156
|
+
"Remove unavailable: remove the listed sync setups first. Switch away from a current setup before removing it if other setups remain.",
|
|
157
|
+
]
|
|
158
|
+
: []),
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
183
162
|
}
|
|
184
163
|
|
|
185
|
-
function notifyConnectionError(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
error: unknown,
|
|
189
|
-
signal?: AbortSignal,
|
|
190
|
-
) {
|
|
191
|
-
if (signal?.aborted) return;
|
|
192
|
-
ctx.ui.notify(
|
|
193
|
-
`Storage connection “${safeTerminalText(name)}” was not changed: ${syncErrorGuidance(error)}`,
|
|
194
|
-
"error",
|
|
195
|
-
);
|
|
164
|
+
function notifyConnectionError(ctx: ExtensionCommandContext, name: string, error: unknown, signal?: AbortSignal) {
|
|
165
|
+
if (signal?.aborted) return;
|
|
166
|
+
ctx.ui.notify(`Storage connection “${safeTerminalText(name)}” was not changed: ${syncErrorGuidance(error)}`, "error");
|
|
196
167
|
}
|
|
197
168
|
|
|
198
169
|
async function editStorageConnection(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
170
|
+
ctx: ExtensionCommandContext,
|
|
171
|
+
name: string,
|
|
172
|
+
profile: Record<string, unknown>,
|
|
173
|
+
usedBy: string[],
|
|
174
|
+
signal?: AbortSignal,
|
|
204
175
|
) {
|
|
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
|
-
usedBy,
|
|
278
|
-
saveSignal,
|
|
279
|
-
),
|
|
280
|
-
signal,
|
|
281
|
-
);
|
|
282
|
-
if (!saved || signal?.aborted) return;
|
|
283
|
-
ctx.ui.notify(`Saved storage connection “${safeTerminalText(name)}”.`, "info");
|
|
176
|
+
if (ctx.mode !== "tui") {
|
|
177
|
+
ctx.ui.notify(
|
|
178
|
+
"Editing storage connections requires TUI mode for safe credential handling. Edit the private version 3 settings file instead.",
|
|
179
|
+
"warning",
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (profile.type === "webdav") {
|
|
184
|
+
await showEditWebDavStorageProfile(
|
|
185
|
+
ctx,
|
|
186
|
+
name,
|
|
187
|
+
{ ...profile, kind: "webdav", ...(ownRecord(profile.credentials) ?? {}) },
|
|
188
|
+
signal,
|
|
189
|
+
usedBy,
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (profile.type === "git") {
|
|
194
|
+
await showEditGitStorageProfile(ctx, name, { ...profile, kind: "git" }, signal, usedBy);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const endpoint = await requiredInput(
|
|
198
|
+
ctx,
|
|
199
|
+
"Endpoint",
|
|
200
|
+
String(profile.endpoint ?? "https://s3.example.com"),
|
|
201
|
+
signal,
|
|
202
|
+
normalizeS3Endpoint,
|
|
203
|
+
);
|
|
204
|
+
if (!endpoint || signal?.aborted) return;
|
|
205
|
+
const region = await requiredInput(
|
|
206
|
+
ctx,
|
|
207
|
+
"Region\n\nUse your bucket's region; Cloudflare R2 uses auto.",
|
|
208
|
+
String(profile.region ?? "auto"),
|
|
209
|
+
signal,
|
|
210
|
+
);
|
|
211
|
+
if (!region || signal?.aborted) return;
|
|
212
|
+
const storedCredentials = ownRecord(profile.credentials) ?? {};
|
|
213
|
+
const credentials = await chooseS3CredentialUpdate(ctx, { ...profile, ...storedCredentials }, signal);
|
|
214
|
+
if (!credentials || signal?.aborted) return;
|
|
215
|
+
const saved = await saveReviewedDraft(
|
|
216
|
+
ctx,
|
|
217
|
+
"Review storage connection",
|
|
218
|
+
[
|
|
219
|
+
`Storage connection: ${safeTerminalText(name)}`,
|
|
220
|
+
`Endpoint: ${safeTerminalText(String(profile.endpoint ?? "missing"))} → ${safeTerminalText(endpoint)}`,
|
|
221
|
+
`Region: ${safeTerminalText(String(profile.region ?? "auto"))} → ${safeTerminalText(region)}`,
|
|
222
|
+
`Credentials: ${safeTerminalText(credentials.summary)}`,
|
|
223
|
+
`Affected sync setups: ${usedBy.length > 0 ? usedBy.map(safeTerminalText).join(", ") : "None"}`,
|
|
224
|
+
"Saving changes future storage access for every affected setup; it does not move remote data.",
|
|
225
|
+
],
|
|
226
|
+
"Save storage connection",
|
|
227
|
+
(saveSignal) =>
|
|
228
|
+
updateStorageConnection(
|
|
229
|
+
name,
|
|
230
|
+
(current) => {
|
|
231
|
+
if (current.type !== "s3" || !isDeepStrictEqual(current, profile)) {
|
|
232
|
+
throw new Error("Storage connection changed while it was open; reopen it.");
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
...current,
|
|
236
|
+
endpoint,
|
|
237
|
+
region,
|
|
238
|
+
credentials: applyS3CredentialUpdate(current.credentials, credentials) as typeof current.credentials,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
usedBy,
|
|
242
|
+
saveSignal,
|
|
243
|
+
),
|
|
244
|
+
signal,
|
|
245
|
+
);
|
|
246
|
+
if (!saved || signal?.aborted) return;
|
|
247
|
+
ctx.ui.notify(`Saved storage connection “${safeTerminalText(name)}”.`, "info");
|
|
284
248
|
}
|
|
285
249
|
|
|
286
250
|
export async function showAddStorageConnection(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
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
|
-
signal,
|
|
358
|
-
);
|
|
359
|
-
if (!saved) return false;
|
|
360
|
-
if (signal?.aborted) return true;
|
|
361
|
-
ctx.ui.notify(`Added storage connection “${safeTerminalText(name)}”.`, "info");
|
|
362
|
-
return true;
|
|
251
|
+
if (ctx.mode !== "tui") {
|
|
252
|
+
ctx.ui.notify(
|
|
253
|
+
"Adding storage connections requires TUI mode for safe credential handling. Edit the private version 3 settings file instead.",
|
|
254
|
+
"warning",
|
|
255
|
+
);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
const preset = await ctx.ui.select(
|
|
259
|
+
"Storage type",
|
|
260
|
+
["Cloudflare R2", "Other S3-compatible storage", "WebDAV", "Git", "Cancel"],
|
|
261
|
+
{ signal },
|
|
262
|
+
);
|
|
263
|
+
if (signal?.aborted || !preset || preset === "Cancel") return false;
|
|
264
|
+
if (preset === "WebDAV") return showAddWebDavStorageProfile(ctx, signal);
|
|
265
|
+
if (preset === "Git") return showAddGitStorageProfile(ctx, signal);
|
|
266
|
+
const name = await promptResourceName(ctx, "storage connection", preset === "Cloudflare R2" ? "r2" : "s3", signal);
|
|
267
|
+
if (!name || signal?.aborted) return false;
|
|
268
|
+
const endpoint = await requiredValueInput(
|
|
269
|
+
ctx,
|
|
270
|
+
"S3 API endpoint\n\nUse the API URL from your storage provider, not its web console.",
|
|
271
|
+
preset === "Cloudflare R2" ? "https://<account-id>.r2.cloudflarestorage.com" : "https://s3.example.com",
|
|
272
|
+
signal,
|
|
273
|
+
normalizeS3Endpoint,
|
|
274
|
+
);
|
|
275
|
+
if (!endpoint || signal?.aborted) return false;
|
|
276
|
+
const region =
|
|
277
|
+
preset === "Cloudflare R2"
|
|
278
|
+
? "auto"
|
|
279
|
+
: await requiredInput(
|
|
280
|
+
ctx,
|
|
281
|
+
"Region\n\nUse the region assigned to your bucket by the provider.",
|
|
282
|
+
"us-east-1",
|
|
283
|
+
signal,
|
|
284
|
+
);
|
|
285
|
+
if (!region || signal?.aborted) return false;
|
|
286
|
+
const credentials = await chooseS3Credentials(ctx, signal);
|
|
287
|
+
if (!credentials || signal?.aborted) return false;
|
|
288
|
+
const saved = await saveReviewedDraft(
|
|
289
|
+
ctx,
|
|
290
|
+
"Review storage connection",
|
|
291
|
+
[
|
|
292
|
+
`Name: ${safeTerminalText(name)}`,
|
|
293
|
+
`Type: ${preset}`,
|
|
294
|
+
`Endpoint: ${safeTerminalText(endpoint)}`,
|
|
295
|
+
`Region: ${safeTerminalText(region)}`,
|
|
296
|
+
`Credentials: ${safeTerminalText(credentials.summary)}`,
|
|
297
|
+
"Adding a connection does not contact remote storage or start syncing.",
|
|
298
|
+
],
|
|
299
|
+
"Add storage connection",
|
|
300
|
+
(saveSignal) =>
|
|
301
|
+
addStorageConnection(
|
|
302
|
+
name,
|
|
303
|
+
{
|
|
304
|
+
type: "s3",
|
|
305
|
+
endpoint,
|
|
306
|
+
region,
|
|
307
|
+
credentials: {
|
|
308
|
+
accessKeyId: credentials.profileFields.accessKeyId ?? "",
|
|
309
|
+
secretAccessKey: credentials.profileFields.secretAccessKey ?? "",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
saveSignal,
|
|
313
|
+
),
|
|
314
|
+
signal,
|
|
315
|
+
);
|
|
316
|
+
if (!saved) return false;
|
|
317
|
+
if (signal?.aborted) return true;
|
|
318
|
+
ctx.ui.notify(`Added storage connection “${safeTerminalText(name)}”.`, "info");
|
|
319
|
+
return true;
|
|
363
320
|
}
|
|
364
321
|
|
|
365
322
|
function referencingSetups(raw: Record<string, unknown> | undefined, connection: string) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
323
|
+
return Object.entries(ownRecord(raw?.syncSetups) ?? {})
|
|
324
|
+
.filter(([, value]) => ownRecord(ownRecord(value)?.storage)?.connection === connection)
|
|
325
|
+
.map(([name]) => name)
|
|
326
|
+
.sort((left, right) => left.localeCompare(right));
|
|
370
327
|
}
|
|
371
328
|
|
|
372
329
|
export function connectionSummary(profile: Record<string, unknown>) {
|
|
373
|
-
|
|
330
|
+
return `${connectionType(profile)} · ${connectionEndpoint(profile)}`;
|
|
374
331
|
}
|
|
375
332
|
|
|
376
333
|
function connectionType(profile: Record<string, unknown>) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
) {
|
|
384
|
-
return "Cloudflare R2";
|
|
385
|
-
}
|
|
386
|
-
return "S3-compatible";
|
|
334
|
+
if (profile.type === "git") return "Git";
|
|
335
|
+
if (profile.type === "webdav") return "WebDAV";
|
|
336
|
+
if (profile.type === "s3" && typeof profile.endpoint === "string" && isCloudflareR2Endpoint(profile.endpoint)) {
|
|
337
|
+
return "Cloudflare R2";
|
|
338
|
+
}
|
|
339
|
+
return "S3-compatible";
|
|
387
340
|
}
|
|
388
341
|
|
|
389
342
|
function connectionEndpoint(profile: Record<string, unknown>) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const url = new URL(value);
|
|
400
|
-
return safeTerminalText(`${url.origin}${url.pathname}`);
|
|
401
|
-
} catch {
|
|
402
|
-
return "Invalid";
|
|
403
|
-
}
|
|
343
|
+
const value = profile.type === "git" ? profile.remote : profile.type === "webdav" ? profile.url : profile.endpoint;
|
|
344
|
+
if (typeof value !== "string" || value.length === 0) return "Missing";
|
|
345
|
+
if (profile.type === "git") return safeTerminalText(value);
|
|
346
|
+
try {
|
|
347
|
+
const url = new URL(value);
|
|
348
|
+
return safeTerminalText(`${url.origin}${url.pathname}`);
|
|
349
|
+
} catch {
|
|
350
|
+
return "Invalid";
|
|
351
|
+
}
|
|
404
352
|
}
|
|
405
353
|
|
|
406
354
|
function credentialSource(profile: Record<string, unknown>) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
355
|
+
if (profile.type === "git") return "Git credential helper or SSH configuration";
|
|
356
|
+
const credentials = ownRecord(profile.credentials);
|
|
357
|
+
if (profile.type === "webdav") return credentials?.password ? "Settings file" : "Missing";
|
|
358
|
+
if (credentials?.accessKeyId && credentials.secretAccessKey) return "Settings file";
|
|
359
|
+
return "Missing";
|
|
412
360
|
}
|